hiphopsllm.architecture¶
Architecture extraction: a LangGraph application as an analysable system model.
The first HiP-HOPS phase is model the system: components with ports, and the
connections between them. Here that model is read out of the object a LangGraph
notebook already renders — graph.get_graph() — rather than being drawn by
hand, so the analysis cannot drift away from the code it describes.
Agent graphs contain feedback (ReAct loops); fault trees cannot. acyclic
unrolls each loop to a stated depth and closes it with a feedback-cut component,
which keeps the loop’s contribution in the tree instead of silently deleting it.
The model¶
hiphopsllm.architecture.model — architecture meta-model and extraction from a LangGraph graph.
This is Phase 0 of the HiP-HOPS process (“model the architecture”). HiP-HOPS analyses a hierarchical model of components connected through ports; before any failure logic can be attached we need that model. In a LangGraph application the architecture is already there — it is exactly the object the notebook renders with:
display(Image(graph.get_graph().draw_mermaid_png()))
graph.get_graph() returns a drawable graph carrying .nodes and .edges.
This module turns it into a SystemModel: typed components, explicit
input/output ports, and directed connections between ports. Three input
routes are supported so the analysis works inside the notebook and offline:
a compiled LangGraph / any object exposing
get_graph();the mermaid source string (
graph.get_graph().draw_mermaid());a plain dict specification (
SystemModel.from_spec()).
Two modelling decisions are worth stating explicitly because they shape every fault tree produced downstream:
Conditional edges become a component. In LangGraph a router function is not a node — it is a callable attached to
add_conditional_edges. It is nevertheless a real piece of software with its own failure modes (a regular expression that matches the wrong branch, or no branch at all). We therefore materialise it as a distinctROUTERcomponent sitting between the deciding node and its successors.Fan-in is a shared-state hazard. Where two nodes write the same LangGraph state channel in one super-step, a non-reducer channel raises
InvalidUpdateErrorand the run dies. Fan-in connections are flagged here (Connection.fan_in) so the failure library can attach channel-contention events to them.
- class hiphopsllm.architecture.model.Role[source]¶
-
Archetype of a component, which selects its default failure logic.
- SOURCE = 'source'¶
- SINK = 'sink'¶
- LLM_AGENT = 'llm_agent'¶
- TOOL = 'tool'¶
- ROUTER = 'router'¶
- AGGREGATOR = 'aggregator'¶
- TRANSFORM = 'transform'¶
- FEEDBACK = 'feedback_cut'¶
- __new__(value)¶
- class hiphopsllm.architecture.model.Component[source]¶
Bases:
objectOne architectural block with ports, akin to a HiP-HOPS component.
-
resources:
Dict[str,str]¶ shared physical/logical resources — the basis for common-cause grouping e.g.
{"llm": "Qwen/Qwen2.5-Math-1.5B-Instruct", "runtime": "gpu:0"}
- __init__(id, label, role, ports_in=<factory>, ports_out=<factory>, resources=<factory>, source_code='', branches=<factory>, notes=<factory>, metadata=<factory>)¶
-
resources:
- class hiphopsllm.architecture.model.Connection[source]¶
Bases:
objectA directed port-to-port link (a HiP-HOPS ‘connection’/channel).
- class hiphopsllm.architecture.model.SystemModel[source]¶
Bases:
objectThe full architecture: components + connections, plus lookup helpers.
-
connections:
List[Connection]¶
- common_cause_groups()[source]¶
Group components that share a resource.
A shared resource (the same model snapshot, the same GPU, the same tokenizer, the same prompt template) is a common-cause failure (CCF) candidate: redundancy built from components in one group does not deliver the failure independence the architecture appears to promise.
- classmethod from_spec(spec, **kwargs)[source]¶
Build a model from a plain dict (offline / unit-test route).
specschema:{"name": str, "nodes": {node_id: {"role": str|Role, "resources": {...}, "source_code": str, "label": str}}, "edges": [(src, dst) | (src, dst, label, conditional)]}
kwargsare forwarded tobuild_system_model(), so role and resource overrides work on a specification exactly as they do on a live LangGraph.- Return type:
- Parameters:
-
connections:
- class hiphopsllm.architecture.model.RawGraph[source]¶
Bases:
objectNode ids/labels and raw edges, before ports and roles are assigned.
- hiphopsllm.architecture.model.parse_mermaid(mermaid, name='system')[source]¶
Parse the mermaid source produced by
draw_mermaid().Used when the drawable graph object is unavailable (e.g. offline analysis of a notebook, or a mermaid diagram pasted from a report).
- hiphopsllm.architecture.model.extract_architecture(graph_like, name='langgraph_system', role_overrides=None, resource_overrides=None, globals_ns=None, node_functions=None, materialise_routers=True)[source]¶
Extract a
SystemModelfrom a LangGraph object or mermaid text.This is the entry point that replaces
graph.get_graph().draw_mermaid_png()as the source of truth for the analysis:model = extract_architecture(graph, globals_ns=globals())
- hiphopsllm.architecture.model.raw_from_spec(spec)[source]¶
Read a plain dict specification into a
RawGraph.Kept separate from
SystemModel.from_spec()so the specification route goes through exactly the samebuild_system_model()call as a live LangGraph, and therefore honours the same overrides.
- hiphopsllm.architecture.model.source_of_function(fn)[source]¶
inspect.getsource, but only when the text really is that function.In a notebook whose cell has been re-executed,
linecachecan hand back stale or unrelated lines. Classifying a component from someone else’s source is worse than classifying it from none, so the result is discarded unless it contains the function’s owndef.
Extraction¶
A configurable, reusable extractor for LangGraph architectures.
extract_architecture() is a function that
takes ten keyword arguments. That is fine for a single call in a notebook, but
awkward when the same conventions — the same role overrides, the same shared
model snapshots, the same unroll depth — have to be applied to several graphs
and compared. LangGraphExtractor holds those conventions as state and
applies them to any number of graphs:
extractor = LangGraphExtractor(
globals_ns=globals(),
role_overrides={"coder": "tool"},
unroll=2,
)
approach_1 = extractor.extract(graph_1, name="Approach 1")
approach_2 = extractor.extract(graph_2, name="Approach 2")
The extractor also carries the loop-elimination step, so extract_acyclic
returns the model the fault tree synthesiser can actually consume.
- class hiphopsllm.architecture.extract.LangGraphExtractor[source]¶
Bases:
objectTurn LangGraph objects into
SystemModelobjects, under fixed conventions.- Parameters:
globals_ns (
Optional[Dict[str,Any]]) – Passglobals()from a notebook. Node functions are then found by name and the actual model objects are interrogated, which is what makes shared-snapshot (common-cause) detection reliable. Without it, role classification falls back to node names and edge topology alone.node_functions (
Optional[Dict[str,Callable[...,Any]]]) – Explicit{node_id: function}mapping, for when the functions are not inglobals_ns(a script, a class, an imported module). Takes precedence overglobals_ns.role_overrides (
Dict[str,Role|str]) – Force a component’s archetype, e.g.{"verifier": Role.AGGREGATOR}. Use this when a node’s name and source do not reveal what it really is.resource_overrides (
Dict[str,Dict[str,str]]) – Declare shared resources the source does not name, e.g.{"critic": {"llm": "gpt-4o-2024-11-20"}}. Components sharing a resource become a common-cause group, which is usually the difference between a redundant architecture and one that only looks redundant.unroll (
int) – Iterations of each feedback loop represented explicitly (default 1).materialise_routers (
bool) – Turnadd_conditional_edgesinto an explicit router component with its own failure logic, rather than an anonymous branch (defaultTrue).
- with_resources(**resources)[source]¶
Return a copy with additional shared-resource declarations.
- Return type:
- Parameters:
- extract(graph, name='langgraph_system')[source]¶
Read the architecture as it is, feedback loops and all.
graphmay be a compiled LangGraph, the drawable fromgraph.get_graph(), mermaid text, a dict specification, or an already-builtSystemModel(returned unchanged).- Return type:
- Parameters:
- extract_acyclic(graph, name='langgraph_system')[source]¶
Extract, then unroll feedback loops to
unrolliterations.Returns the analysable (acyclic) model and the
CycleReportrecording what was cut, so the loop handling is visible in the report rather than hidden in the tree.- Return type:
- Parameters:
- __init__(globals_ns=None, node_functions=None, role_overrides=<factory>, resource_overrides=<factory>, unroll=1, materialise_routers=True)¶
Loop elimination¶
hiphopsllm.architecture.acyclic — turning a cyclic agent graph into a loop-free analysis model.
Fault trees are, by definition, acyclic: a top event is refined into causes, and
no event may be its own cause. Agentic LangGraph applications are not acyclic
— the ReAct pattern is a feedback loop (generator -> coder -> generator).
Classical HiP-HOPS meets the same problem with control loops in engineered
systems and resolves it by breaking the circular dependency explicitly rather
than letting the synthesis algorithm recurse for ever.
This module implements that step and records it, so the resulting fault tree can be read as a static structure without hiding what was done to obtain it.
Two policies are offered, both producing a directed acyclic graph:
unroll=k(defaultk = 1)Iterations
1..kof the loop body are represented explicitly as distinct component instances (generator#1,generator#2, …). This exposes iteration-dependent behaviour (e.g. prompt growth) at the cost of a larger tree.- Feedback cut with contribution preserved (always applied at depth
k) The last back edge is replaced by a
FEEDBACKpseudo-component that consumes the deviations the loop would have carried and delivers them to the system boundary, together with a loop-exhaustion basic event. This is the conservative choice: simply deleting the back edge would silently remove the tool’s contribution from the tree and produce an optimistic — that is, unsafe — result.
The intent is that no analysis result depends on an arbitrary recursion cut-off: every loop is either unrolled a stated number of times or represented by a named basic event that appears in the cut sets.
- class hiphopsllm.architecture.acyclic.CycleReport[source]¶
Bases:
objectWhat was found and what was done about it — carried into the report.
- __init__(cycles=<factory>, back_edges=<factory>, unroll=1, feedback_components=<factory>, replicated=<factory>, notes=<factory>)¶
- hiphopsllm.architecture.acyclic.find_cycles(model)[source]¶
Components that lie on a cycle, grouped by strongly connected component.
- Return type:
- Parameters:
model (SystemModel)
- hiphopsllm.architecture.acyclic.find_back_edges(nodes, edges)[source]¶
Return the back edges of a depth-first forest (iterative DFS).
A back edge points to a node currently on the DFS stack; removing the set of back edges is sufficient to make the graph acyclic.
- hiphopsllm.architecture.acyclic.make_acyclic(model, unroll=1, boundary=None)[source]¶
Return a loop-free copy of
modelplus aCycleReport.- Parameters:
unroll (
int) – Number of loop iterations represented explicitly (>= 1).boundary (
Optional[str]) – Component that receives the feedback-cut output. Defaults to the graph’s sink (__end__), because an unresolved loop manifests at the system boundary as “no answer” or “answer too late”.model (SystemModel)
- Return type:
- hiphopsllm.architecture.acyclic.is_acyclic(model)[source]¶
- Return type:
- Parameters:
model (SystemModel)