Graph builder implementation plan#
Direction#
Bring graph authoring into ezmsg-dashboard, reusing suitable document, serialization, collection and runtime-construction code from ezmsg-flow. The destination should not depend on ezmsg-flow or its Qt/GPU UI dependencies. Retire ezmsg-flow after the required authoring workflows have moved across.
Use a shared React Flow canvas with Build and Monitor workspace presets. Build shows the component library and settings editor; Monitor shows runtime inspectors, profiling and plots. Preserve selection, collection scope, positions and viewport when switching presets. Share node/port rendering and navigation; allow monitoring detail to expand beyond the authoring representation.
Workspace layout and execution state are separate concerns. Switching to Build must not mutate a running graph. Maintain a versioned authoring document and a separate runtime snapshot, with a mapping from document node IDs to runtime component addresses. Show unapplied changes explicitly. Attached external graphs remain inspectable without implying that their source document can be recovered.
First milestone#
Migrate the existing monitoring canvas to React Flow 12. Preserve current behavior and pass unit, browser interaction and existing screenshot tests.
Add a Python component catalog using the proposed
ezmsg.componentsentry point group. Enumerate registrations in the execution interpreter; return stable IDs, distribution/version provenance and availability diagnostics. Load component metadata separately from enumeration, without instantiating Units or connecting hardware. Handle inherited streams and required settings. Keep broken registrations visible. Adapt legacy__ez_flow__declarations explicitly during migration rather than importing every installed module.Introduce a versioned graph document independent of React Flow state. Reuse ezmsg-flow’s document/serialization/runtime concepts after checking their dependencies and compatibility. Store component references, settings and connections separately from canvas layout. Preserve unavailable components on load/save so opening a document in another environment does not lose data.
Add component insertion, connection editing, settings forms, validation and save/load. Use the same node identities and navigation across workspace presets.
Run a saved document through ezmsg and inspect it with the existing dashboard. Initially use explicit validate/run/stop; defer live topology changes until their apply/restart behavior is defined. Nested collection live editing is not supported by the current ezmsg-flow engine.
Acceptance: discover registered components from representative extensions, configure a source → processor → sink graph, save/reload it, execute it, and inspect its messages and settings. Missing components and invalid settings must produce useful diagnostics without preventing document recovery.
Settings compatibility#
The core feature/json-schema-settings branch adds an optional ezmsg[schema]
extra (pydantic>=2.7), lazy TypeAdapter loading, optional json_schema metadata,
and validation/coercion of dynamic field updates in the owning process. It leaves
Settings as dataclasses. This is a suitable integration point without making
Pydantic a mandatory core dependency.
The branch is not part of the current prerelease used here. Detect schema capabilities and retain the existing field metadata fallback. Installing Pydantic alone does not add the unmerged metadata/control implementation to core. Unsupported settings types may still return no schema; make this visible rather than promising complete form coverage. Field coercion is not strict validation of an entire settings object or cross-field constraints: validate the complete settings object before graph execution too.
Use JSON Schema for value structure and constraints, with separate presentation metadata/custom widgets for paths, channels and devices. Keep Python validation authoritative and define round-trip encodings for scientific values explicitly.
React Flow 12 foundation#
The migration uses @xyflow/react, its named ReactFlow export and stylesheet,
and parentId for nested nodes. The current layout supplies explicit dimensions
through node styles; it does not consume the old measured node.width/height
fields. Future code reading measured dimensions must use node.measured.
This step retains the monitoring interaction model. Component registration, authoring presets and document execution are subsequent work, not features enabled by the library migration itself.
Current implementation#
Build and Monitor share the existing topology canvas. Build replaces the right
inspector with Components, Configure and Connections tabs. Components is the
starting point: add any registered Unit or Collection, including acquisition Units
such as lsl.LSLInletUnit. Configure edits the selected component’s settings.
Connections pairs any draft component output with another component’s input,
using either canvas handles or selectors. Draft nodes follow the pointer during
dragging and save their positions on release. Ports use pill styling with the
payload type below the name, input labels at the left and output labels at the
right, matching the live topology. No pre-existing data source or running
pipeline is required. GraphService itself still needs to be running for execution.
Managed draft nodes have a diagonal texture in Build. Their labels distinguish
unstarted drafts from definitions with an applied version running. Runtime Units
owned by this dashboard are also textured in Build, including Units removed from
the draft that will stop on Apply. Other runtime nodes remain external. Drafts
form one managed processing group.
GET /api/components enumerates entry-point metadata in the dashboard server’s
Python environment without importing extensions. GET /api/components/{id}
loads and inspects one registration in a disposable Python process with a
30-second timeout. It returns availability/error, inherited ports, settings
fields and optional Pydantic JSON Schema. Inspection never constructs Units or
settings, and does not call default factories. Duplicate names from different
packages remain distinct. No GraphService connection is needed for these routes.
The server environment is the only supported catalog environment for now.
Draft documents use version 1 JSON with component registration/provenance, settings, ports, positions and connections. They persist in browser local storage and can be saved/loaded as files. Missing installed packages do not erase nodes when loading a document. Use the node handles to connect managed draft nodes; self-links, duplicate edges and cycles are rejected. Build reuses Monitor’s settings widgets, populated from the catalog’s Pydantic JSON Schema with field metadata as a fallback. Save each field to the draft; Run/Apply validates the complete settings object in Python before execution. Nullable fields have an explicit None checkbox. Arrays, objects and complex unions use JSON editors; the full settings JSON remains available under Advanced settings JSON. Widget edits never issue live settings patches. A draft connection is structurally valid, not proof of message-type or scientific-data compatibility.
Not implemented yet: device/path-specific settings widgets, undo/redo, editing Collection internals/custom process layout controls, and import of an arbitrary live graph as executable definitions. Draft documents do not currently include the external graph being inspected.
Managed execution#
Build exposes Run, Stop and Apply changes. The backend supervises one group of
registered Units and Collections in its own Python environment and attaches it to the same
GraphService the dashboard inspects (auto_start=False). GraphService must
already be running. Units share a backend process by default; Collections retain their declared
process_components() layout. The supervisor worker is separate. Collection
boundaries expose directional topics, relays or legacy streams. Generic
undirected topics/relays cannot be wired in the editor yet. Main-thread Units,
including those nested inside Collections, require a future execution policy.
Run and Apply first validate the document in a disposable process. Validation resolves installed entry points, checks saved package versions, validates complete settings through Pydantic, rejects unknown setting fields, checks real inherited stream definitions/directions and declared message types, and rejects cycles. Preflight also constructs the components and runs Collection configuration and network setup without starting their processing tasks. Collection authors should keep hardware acquisition in Unit initialization. Saved port metadata is never authoritative. This does not validate scientific properties such as AxisArray shape, channel labels or sampling rate.
If preflight fails, the existing group keeps running. Apply then stops the old group and starts its replacement; it is not an atomic or lossless handover. State/buffers reset and samples may be lost. Startup failure leaves the group stopped with a failed status and bounded diagnostic logs. There is no automatic rollback or state transfer. The last successfully applied document remains available through Restore last applied graph. Layout-only changes do not require Apply. Managed settings are edited through Build/Apply for now; their monitoring inspector is read-only so live patches cannot silently diverge from the saved definition. External settings controls remain available. A revision token rejects concurrent stale execution commands.
Component names are edited in Build → Configure → Component name. New components
receive unique defaults (ButterworthFilter, ButterworthFilter_2, …). Names
are case-sensitive, 1–64 ASCII letters/digits/underscores, beginning with a letter
or underscore. They must be unique among top-level components in the managed
graph; Collection children retain their own scope. Python preflight enforces
uniqueness as well, including collisions with legacy generated names.
Connections reference immutable document IDs, so renaming preserves wiring. Names are used in runtime addresses under the dashboard’s unique root. Renaming a running component requires Apply and restarts the managed group. External clients referencing its old runtime topic must update their binding. Older unnamed documents keep their generated runtime names until explicitly renamed. Runtime addresses map document node IDs to live component addresses. The Build canvas shows draft definitions beside the live topology; the live topology retains its existing monitoring inspectors. Stop and server shutdown target only the supervisor’s owned process group, never the GraphService or unrelated pipelines. Closing a browser tab does not stop a run. A dashboard server restart creates a new execution session; it does not automatically relaunch the browser’s saved draft. Run a single backend server worker; multi-worker deployment requires a shared supervisor service first.
API: GET /api/execution returns state, revision, last applied document, runtime
addresses and recent logs. POST /api/execution/run and /apply accept
document and expected_revision; /stop accepts expected_revision.
Real-process regression tests publish messages through a source/sink graph, replace it, reject invalid edits without stopping it, test startup failure and verify that another attached graph and GraphService survive managed Stop.
Optional connections to existing pipelines#
In Build, drag from the visible handle on an existing Unit’s output pill to a new component’s input handle. When the existing topic is visible, the new edge attaches directly to that topic; reference cards are used when it is outside the current view. Run/Apply validates that the referenced output is live and typed. Collection output topics and output relays are supported when reachable from a live publisher; discovery follows directed topic wiring.
Alternatively, in Connections, opt into “Include components running elsewhere” to select an output from any stage of an existing pipeline, including a downstream transform. This is optional; discovery is not requested for a new, self-contained graph. A source has no special editor role: it is another component with output ports.
New source and processing Units run together by default. Existing processes
retain ownership of their components; attaching to them crosses a process
boundary and does not move them into the managed process. The connection picker
and dashed canvas edges identify this boundary. Reference cards show the existing
component address and output port with a “Running elsewhere” ownership label. References are
saved as optional external_inputs in version 1 documents; older documents remain
compatible. Offline publishers stay in saved documents and are marked unavailable.
Removing a target node also removes its bindings.
GET /api/execution/sources discovers active publisher endpoints with declared
output-stream metadata. It excludes this dashboard’s managed root. Run/Apply
recheck live sources in preflight and again at startup. Remote type names are
never imported or evaluated: external bindings require an exact declared type
name, or a target accepting Any/object. Subclass/union compatibility across
external metadata, untyped publishers are not yet supported. Collection aliases retain their declared payload type.
The runner creates external-to-managed edges through its own GraphContext session. Apply/Stop remove that session’s edges and preserve the external pipeline and its other connections. Applying changes can lose incoming samples during the gap; a publisher disappearing after preflight can still cause startup to fail. Normal ezmsg backpressure rules apply while connected. Bindings from managed outputs to external consumers are not exposed yet.
Integration coverage includes an external publisher feeding a two-process Collection, Apply with new sink settings, rejected missing-source/configuration edits, and ownership cleanup while the external publisher continues running.
Keyboard editing and generic stream metadata#
In Build, Delete/Backspace removes the selected draft node or draft connection. Removing a node also removes its incident draft/external bindings. This edits the document; a running group changes only after Apply. Keyboard deletion is disabled in Monitor and cannot remove externally owned components or connections. Name, settings and other text inputs retain their normal keyboard behavior.
Core 3.10.0b3 advertised inherited TypeVars literally (~MessageInType,
~MessageOutType), even for concrete generic subclasses such as AsArray and
Flatten. The core fix on fix/resolve-generic-stream-metadata resolves explicit
generic base arguments before emitting stream/settings metadata, without mutating
shared stream declarations. Unspecialized variables remain unresolved.
The local fix lives in the sibling ezmsg-stream-types worktree, based on
v3.10.0b3, and is installed editable into the dashboard and INTENT pipeline venvs.
Restart a pipeline to publish corrected metadata. When testing through uv run,
use --no-sync to retain this local installation rather than restoring the locked
release. The INTENT project source and lockfile were not changed.
Applied drafts use the actual runtime nodes in Monitor, including activity animation and inspector selection. Build shows the editable cards and hides the corresponding runtime copies. Draft cards share live Unit sizing and topic-pill geometry; striping identifies managed ownership. Invert has no configurable settings; Scale exposes its numeric scale field.
Review and release prerequisites#
The Build execution path requires ezmsg 3.10 APIs; ezmsg 3.9 fails when starting
GraphRunner. The package minimum is now 3.10.0b3. Correct inherited generic stream
metadata additionally requires core PR #270. PR #271 integrates that fix with the
optional JSON Schema settings feature. Dashboard CI temporarily installs the
combined core commit 8442e35bd1b9cb703f1b4506b4563821cae28d2c and runs tests with
uv run --no-sync to preserve it.
For local review after syncing the dashboard environment:
uv pip install "ezmsg @ git+https://github.com/ezmsg-org/ezmsg.git@8442e35bd1b9cb703f1b4506b4563821cae28d2c"
uv pip install -e ../ezmsg-lsl -e ../ezmsg-sigproc
uv run --no-sync ezmsg dashboard --graph-address 127.0.0.1:25978
The extension checkouts need their ezmsg.components entry-point changes; the
catalog does not discover arbitrary unregistered classes. Before publishing the
dashboard, publish a core beta containing the PR stack, raise this package’s core
minimum to that beta, and remove the temporary CI Git pin. Publish the extension
registrations as well for package-only component discovery. No core release is
required just to review this PR.
The component browser groups registrations by installed distribution, then by
nested Python module path (for example, ezmsg-sigproc → math → scale → Scale).
Large catalogs start collapsed. Search matches registration names, extensions,
and full Python paths; matching branches open automatically without changing the
user’s expansion choices. Grouping uses entry-point metadata only, so browsing
never imports every package or requires optional hardware/ML dependencies.