1. From a LangGraph to a fault tree¶
We will take the ReAct-plus-calculator architecture — one agent, one tool, one router, and a feedback loop — and turn it into fault trees without drawing anything.
from hiphopsllm import extract_architecture, load_example
import pandas as pd
model = extract_architecture(load_example("react_calculator"), name="ReAct + calculator")
pd.DataFrame(model.architecture_table()).set_index("component")
role in_ports out_ports resources
component
__end__ sink in@generator::router, in@…:end - -
__start__ source - out -
coder tool in out -
generator llm_agent in@__start__, in@coder out llm=Qwen/…, runtime=cuda:0
generator::router router in out@coder, out@error, out@end -
Five components from a three-node graph. Two of them are worth stopping on.
The router is a component¶
generator::router does not exist in the LangGraph node list. It is the callable
passed to add_conditional_edges, and the extractor materialises it, because a
regular expression that matches the wrong branch is a failure mode with
consequences:
model.components["generator::router"].branches
# ['coder', 'error', 'end']
Two of those branches reach __end__, and they get separate input ports
(in@generator::router and in@generator::router:end). A shared port would
collapse them into one deviation and miscount the fan-in.
The router’s source is not the node’s source. LangGraph keeps the routing function separately, so pass it explicitly if you want it analysed:
extract_architecture(graph, node_functions={"generator::router": route_fn})
The resources are read, not declared¶
model.common_cause_groups()
# {('llm', 'Qwen/Qwen2.5-Math-1.5B-Instruct'): [...], ('runtime', 'cuda:0'): [...]}
Those came out of the node function’s source text. In a notebook, pass
globals_ns=globals() and the extractor interrogates the live model objects
instead, which is more reliable:
extract_architecture(graph, globals_ns=globals())
Declare what the source does not name:
from hiphopsllm import LangGraphExtractor
extractor = LangGraphExtractor(globals_ns=globals()).with_resources(
critic={"llm": "gpt-4o-2024-11-20"},
drafter={"llm": "gpt-4o-2024-11-20"}, # same snapshot → a CCF group
)
The loop¶
from hiphopsllm import find_cycles, make_acyclic
find_cycles(model)
# [['coder', 'generator', 'generator::router']]
Fault trees are acyclic. Cutting the back edge outright would delete the tool’s contribution and understate risk, so instead the loop is unrolled and closed with a feedback-cut component:
acyclic, report = make_acyclic(model, unroll=1)
print(report.summary())
1 feedback loop(s) found; unrolled to depth 1 and closed with 1 feedback-cut
component(s).
loop: coder -> generator -> generator::router
back edge cut: coder -> generator
Deleting a back edge outright would remove the feedback path's contribution
from the fault tree and understate risk; the feedback-cut component preserves it.
unroll=1: a single pass through the loop body is modelled. Increase unroll to
expose iteration-dependent effects such as prompt growth.
unroll=2 models two passes and produces a larger tree. Use it when the loop’s
iterations differ — a growing prompt, an accumulating scratchpad — and unroll=1
when they do not.
Synthesis¶
from hiphopsllm import AgenticReliabilityStudy
study = AgenticReliabilityStudy(load_example("react_calculator"),
name="ReAct + calculator", unroll=1)
print(study.analyse().summary())
components: 6 connections: 7 basic events: 24
1 feedback loop(s) found; unrolled to depth 1 …
hazard sev P(top) MCS SPOF name
----------------------------------------------------
H1 major 0.3185 12 12 No answer delivered
H2 critical 0.3335 4 4 Incorrect answer delivered and accepted as correct
H3 minor 0.2361 4 4 Malformed answer delivered
H4 minor 0.1450 2 2 Answer too late / budget exhausted
H5-coder catastrophic 0.0200 1 1 Unsafe execution of model-authored code in coder
H5-coder appears because coder’s source contains eval(. It is
catastrophic, and its single cut set is BE-coder-UNSAFE — nothing else has to
go wrong.
Every cut set here is order 1. There is no redundancy anywhere in this architecture; any single fault reaches the boundary.
Looking at the tree¶
study.plot("H2") # matplotlib
print(study.report.mermaid("H2")) # mermaid source
study.report.display("H2") # inline in a notebook
Exports:
from hiphopsllm import to_dot, to_json, to_openpsa_xml
tree = study.report.tree("H2")
to_dot(tree) # Graphviz
to_json(tree, study.report.analysis("H2"))
to_openpsa_xml(tree, name="H2") # Open-PSA MEF, for XFTA / SCRAM
Next¶
Tutorial 2 reads the structural result: which cut sets matter, which events to fix first, and what the generated FMEA says.