Feature/bpmn#7
Conversation
…nd routing
Implements the BPMN handler (guide 01) alongside the existing ClassDiagram /
StateMachine / etc. handlers. Emits inject_complete_system and modify_model
payloads consumed by the WME BPMN injector (guide 17). Base BPMN only:
start/end events, tasks, exclusive/parallel/inclusive gateways, sequence flows.
No pools, lanes, or agentic concepts. Positions are not generated — the WME
lays out the process left-to-right.
New files
- src/schemas/bpmn.py Pydantic schemas: SystemBPMNSpec,
BPMNModificationResponse and friends
- src/diagram_handlers/types/ BPMNDiagramHandler: generate_complete_system
bpmn_diagram_handler.py (two-pass structured output + validation),
generate_modification, fallbacks
- tests/test_bpmn.py Routing, schema defaults, validation repair,
fallback envelope, model summary, suggestions
Registry / routing changes (existing files)
- schemas/__init__.py Export BPMN schemas
- factory.py Register BPMNDiagramHandler
- metadata.py Add "BPMN" metadata entry
- protocol/types.py Add "BPMN" to SUPPORTED_DIAGRAM_TYPES
- workspace_orchestrator.py Explicit keyword targets + implicit regex rule
+ FALLBACK_PRIORITY entry
- generation_handler.py Add BPMN tokens to diagram-type guard lists
- base_handler.py BPMNModificationResponse in _SMALL_OUTPUT_SCHEMAS;
BPMN action labels; nodeName in _build_mod_target_name
- model_context.py _summarize_bpmn + BPMN branch in
detailed_model_summary; BPMN in compact summary
- suggestions.py _BPMN_SUGGESTIONS pool + builder + registry entry
Wire contract: diagramType token is "BPMN" (storage-bucket name, not Apollon
model.type "BPMNDiagram"). Counterpart: WME guide 17.
Bug fixes applied on top
- Unnamed-node references: context now serialises every node as [apollon-id]
Name (type); BPMNModificationTarget gains nodeId; modification prompt
instructs the LLM to use the id for unnamed nodes
- "Regenerate with more detail" chip: was emitting an empty prompt; now sends
"Regenerate the current BPMN process with more detail and intermediate steps."
- Parallel gateway: generation prompt hardened to require ≥2 outgoing flows
from a split and ≥2 incoming flows into a join (prevents linear chains)
… and ordinals
Replace the generic "_build_mod_target_name" output with BPMN-aware sentences
built from the live context model.
- add_task / add_gateway / add_event: shows the element name from
target.nodeName; falls back to a subtype label ("Parallel Gateway",
"Send Task", "End Event") when the name is empty
- remove_element: resolves the element by nodeId/nodeName in the context
model and shows the subtype label for unnamed nodes
- add_flow / remove_flow: "Added flow: **Ship Order** → **Send Invoice**."
using the same context lookup for both endpoints
- modify_node: "Renamed **Check Inventory** to **Verify Stock**." when a
name change is detected; "Updated **X**." otherwise
- Multiple unnamed elements of the same type in one batch get 1-based
ordinals ("Parallel Gateway 1", "Parallel Gateway 2") so each item in
the message is unambiguous
Implementation: three new methods on BPMNDiagramHandler
_bpmn_el_type_label — Apollon element dict → readable label
_bpmn_resolve — look up by id (exact key) then by name (case-insensitive)
_bpmn_mod_message — orchestrates ordinal pre-counting and per-mod formatting
generate_modification now calls _bpmn_mod_message to replace the base-handler
message after _execute_modification returns.
Tests: 6 new cases in test_bpmn.py (15/15 pass)
… error Add elementFound and message fields to BPMNModificationResponse so the LLM can signal when a remove_element or modify_node target doesn't exist in the diagram. _execute_modification short-circuits on elementFound=false, returning an assistant_message action with the LLM's explanation rather than hitting validate_modification_spec with an empty modifications list (which would raise).
Since the agent did not understand the "Undo" command, I refined a bit the prompt
ArmenSl
left a comment
There was a problem hiding this comment.
Thanks for this. It is a clean, well-structured addition that mostly follows the existing per-diagram pattern closely: the handler implements the full BaseDiagramHandler contract, the schema mirrors the sibling Modification/Target/Changes triad (Pydantic v2, exports complete), and the registry wiring (factory, metadata, protocol types, suggestions, generation_handler, model_context) is filled in everywhere the other diagram types appear. The _validate_and_refine generation path is a nice touch. A few things I would like to work through before merge, mostly around the not-found guardrail and staying close to the base class.
1. The "element does not exist" guardrail is not actually enforced on the modification path. This is the main one. Right now the only existence check for add_flow/remove_flow/modify_node/remove_element is the LLM self-reporting elementFound (base_handler.py:372). Nothing validates that changes.source/changes.target/target.nodeId actually exist in the current diagram before the spec is forwarded to the WME, so a model that hallucinates an id and still says elementFound: true will ship a bogus modification. The generation path validates refs, but generate_modification does not. Two related points:
getattr(parsed, 'elementFound', True)defaults to "found" when the field is missing, so a malformed response fails open. For the destructive actions (remove/modify) it would be safer to default to not-found.- The good news is the fix is already half-written:
_bpmn_resolvecan resolve every endpoint/target againstcurrent_model["elements"]and drop or refuse modifications whose refs do not exist, which is exactly the guarantee the PR description is going for.
2. The handler re-implements a fair amount that the base class and model_context already provide. _bpmn_mod_message, _bpmn_single_mod_sentence, _bpmn_resolve and the subtype-label maps (~150 lines) hand-roll message building and element lookup that the siblings get from the base _friendly_batch_message/_build_mod_target_name path and from model_context. StateMachineHandler.generate_modification just returns _execute_modification(...) and lets the base build the message. Routing the BPMN handler through the same path (and extending the base target-naming only where flow endpoints genuinely need it) would cut the custom code and keep it consistent. It also dovetails with point 1, since the resolver you already have becomes the validation hook.
3. The tests cover the message builder, but not the behaviour or the guardrail. test_bpmn.py exercises _bpmn_mod_message in isolation, but nothing drives generate_modification/generate_complete_system with a mocked predict_structured the way test_prompt_caching.py does, so the actual add/modify/remove application and the elementFound short-circuit are untested, and remove_flow has no test at all. Since the guardrail is the headline feature, a test that returns BPMNModificationResponse(elementFound=False, modifications=[]) and asserts the plain assistant reply would be worth adding.
4. A couple of diagram-type lists were not updated for BPMN. The live routing path is fully BPMN-aware, but the request planner's allowed-type prompt still hardcodes the six other types and omits BPMN (orchestrator/request_planner.py:752), so the planner can be told BPMN is not a valid output even when the keyword hint points at it. That file is not in this PR, but it looks like the one place that could stop BPMN from being selected in a multi-step request, so worth adding |BPMN there (ideally built from SUPPORTED_DIAGRAM_TYPES). Smaller siblings of the same kind: no _HEURISTIC_BPMN fast-path in _SINGLE_DIAGRAM_HEURISTICS, and the modeling-help branch in state_bodies.py:339 has a tailored block for Quantum but BPMN falls to the generic else.
Minor:
schemas/bpmn.py:182: thedefault_factory=listonmodifications(vs the siblings'min_length=1) is the correct choice for theelementFound=falseempty-list case, but a one-line comment noting why would help, since it reads as an inconsistency otherwise.base_handler.py:679: full prompt content (including user diagram text) is logged at INFO on every structured call; worth gating behind DEBUG.- User node names are interpolated verbatim into the prompt; output is schema-constrained so the blast radius is small, but fencing the diagram context as an untrusted block would be tidy.
- The dead
IMPLICIT_TARGET_RULESdict omits BPMN (harmless, it is unused), and.gitignoreis missing a trailing newline.
None of the minors block anything. Items 1 to 4 are the ones I would want addressed, and 1 is the only thing I would call a must-fix. Really solid groundwork overall, and the pattern-matching to the other handlers is most of the way there. Happy to look again once the guardrail has a server-side check and the tests drive the real path.
💣 Describe the issue or problem you are addressing with this Pull Request
📋 Describe the solution you implemented
🤔 If any, describe alternatives you've considered
NA