#Disclaimer: The code was originally written by me using AI as a tool. After I finnished, in order to make it more user-friendlier, I asked an AI tool to clean it and do the commentary, which I have not double checked. But is still working as it should be.
An automated pipeline that converts a natural-language system description into structured MBSE (Model-Based Systems Engineering) diagrams following the Arcadia methodology.
The pipeline uses an LLM (Claude) to translate a free-text description into a JSON model, then renders that model as interactive Cytoscape.js diagrams displayed in a Streamlit web application.
- A system description in natural language is sent to an LLM as a prompt.
- The LLM returns a structured JSON that follows
system_template.json. main.pyreads the JSON, validates the model, and renders all diagrams as self-contained HTML files underrendering/.app.py(Streamlit) lets you navigate and interact with the diagrams. Clicking a node shows its metadata in a side panel.
| Diagram | Description |
|---|---|
| Stakeholder & Context / Component Architecture | Actors, entities, and the system boundary |
| Use Case / Capability | Capabilities and the actors that support them |
| Activity / Function | Functions allocated to their performing actors (compound graph) |
| Sequence | Step-by-step scenario execution across swimlane columns |
| Phase | ID | Description |
|---|---|---|
| Operational Analysis | OA |
Stakeholders, operational entities, activities, and scenarios from an end-user perspective |
| System Analysis | SA |
System boundary, external actors, system functions, and scenarios |
| Logical Architecture | LA |
Internal decomposition into logical components with allocated functions |
project/
├── main.py # Entry point: load JSON → build graphs → render HTML
├── app.py # Streamlit viewer
├── system_template.json # JSON schema / template for LLM prompt
├── system_hmi.json # Example model: HMI system (OA + SA)
├── system_ife.json # Example model: IFE system (OA + SA + LA)
│
├── data_model/ # Domain model (Python dataclasses)
│ ├── common.py # Base classes: Element, Actor, Function, Scenario, …
│ ├── oa.py # Operational Analysis phase container and subtypes
│ ├── sa.py # System Analysis phase container
│ ├── la.py # Logical Architecture phase container
│ ├── parser.py # JSON → dataclass parsers (OA / SA / LA)
│ └── loader.py # load_model_json() + ID validation + catalog writer
│
├── graph/ # Graph building and HTML rendering
│ ├── common.py # GraphNode, GraphEdge, Graph primitives
│ ├── config.py # CDN URLs, layout configs, stylesheets, geometry constants
│ └── builder.py # build_*_graph() and render_*_graph() functions
│
├── gui/
│ └── graph_listener.py # Streamlit v2 component: embeds diagram + forwards node clicks
│
├── utils/
│ └── aux.py # create_id() (stable SHA-256 hash), get_ids_element_list()
│
└── rendering/ # Generated output (created by main.py, not tracked in git)
├── OA/
│ ├── catalog.json
│ ├── graph_oa_actors.html
│ ├── graph_oa_cap.html
│ ├── graph_oa_fun.html
│ └── graph_oa_scen_*.html
├── SA/
└── LA/
- Python 3.10+
- Streamlit —
pip install streamlit
No other Python packages are needed. The Cytoscape.js library and its layout extensions (ELK) are loaded from CDN at diagram view time.
# main.py (or a Jupyter notebook cell)
from data_model.loader import load_model_json, phase_catalog
from graph.builder import (
build_actors_graph, render_actors_graph,
build_capabilities_graph, render_capabilities_graph,
build_functions_graph, render_functions_graph,
build_scenario_graph, render_scenarios_graph,
)
OUTPUT = "rendering"
model = load_model_json("system_ife.json")
for phase in model.phases:
phase_catalog(phase)
out = f"{OUTPUT}/{phase.id.upper()}"
render_actors_graph( build_actors_graph(phase), out)
render_capabilities_graph( build_capabilities_graph(phase), out)
render_functions_graph( build_functions_graph(phase), out)
render_scenarios_graph( build_scenario_graph(phase), out)cd project
streamlit run app.pyOpen the URL printed in the terminal (default: http://localhost:8501).
The model JSON has a meta block and a phases block. See
system_template.json for the full annotated template.
{
"meta": { "name": "My System", "description": "..." },
"phases": {
"OA": { "actors": [...], "entities": [...], "capabilities": [...],
"activities": [...], "interactions": [...], "scenarios": {...} },
"SA": { "system": [...], "actors": [...], "missions": [...],
"functions": [...], "interactions": [...], "scenarios": {...} },
"LA": { "components": [...], "actors": [...], "capabilities": [...],
"functions": [...], "interactions": [...], "scenarios": {...} }
}
}Element IDs follow the convention <phase>_<type>_<name>, e.g.
oa_actor_pilot, sa_fun_process_sensor_data. IDs are used to express
cross-references (e.g. a function's performer points to an actor ID).
If an element does not carry an explicit id field the loader generates a
stable hash-based ID from the class name and label via utils.aux.create_id.
When asking an LLM to generate a model JSON:
- Provide the contents of
system_template.jsonas the schema reference. - Describe the system in natural language, specifying the scope (OA / SA / LA).
- Ask the LLM to return a valid JSON that matches the template exactly, including all required fields and consistent cross-reference IDs.
- Validate the result with
load_model_json()— the loader raises descriptive errors for broken cross-references or duplicate IDs.
-
Rendering is decoupled from the viewer.
main.pywrites static HTML files;app.pyjust reads them. This means diagrams can be generated offline or in a headless environment and then served later. -
Stable IDs.
create_id()hashesClassName:labelwith SHA-256 so IDs are reproducible across runs as long as labels stay the same. You can also provide explicit IDs in the JSON to override this. -
Swimlane resolution for LA. LA functions are allocated to leaf components inside a hierarchy. The
_resolve_lane()helper walks theparent_idchain upward to find the top-level component that appears inscenarios_order, so each function lands in the correct swimlane column even when it belongs to a deeply nested component. -
Node click events. The Streamlit viewer uses a custom v2 component (
gui/graph_listener.py) that injects the Cytoscape.js HTML into an iframe and polls forwindow.cyto become available before attaching atapevent listener. Tapped node IDs are forwarded to Python viasetTriggerValue.