hiphopsllm.io¶
Getting systems and outcomes in: the bundled examples, and the n8n importer.
Bundled examples¶
Example architectures and outcome data that ship with the package.
Every example in the documentation runs from these, so the docs are executable without LangGraph, without a GPU and without a network connection. The graph specifications are the same architectures the source notebook analyses, recorded as plain JSON; the outcome table is synthetic and says so.
- hiphopsllm.io.examples.load_example(key='parallel_aggregator')[source]¶
Return one bundled architecture as a graph specification.
The result goes straight into anything that accepts a graph:
from hiphopsllm import AgenticReliabilityStudy, load_example study = AgenticReliabilityStudy(load_example("parallel_aggregator"))
- hiphopsllm.io.examples.load_outcomes(as_frame=True)[source]¶
Synthetic per-agent outcomes for the
parallel_aggregatorexample.240 items labelled with a StrategyQA-shaped stratum (
short/medium/long), one correctness column per agent, and asplitcolumn so the calibration/evaluation separation can be demonstrated.1means the agent answered that item correctly.The data is generated, not measured: agent accuracy falls with decomposition length, the two agents’ errors are correlated at 0.55 because they share a model snapshot, and the aggregator selects correctly 85% of the time. Use it to learn the API, never as evidence about any real model.
n8n workflows¶
Reads an n8n JSON export into the same architecture model a LangGraph application produces, so everything downstream is unchanged. See Analysing an n8n workflow for the three modelling decisions this involves and why each one changes the fault tree.
hiphopsllm.io.n8n — read an n8n workflow export into the architecture model.
An n8n workflow export is a JSON document with three parts that matter here:
{"name": ...,
"nodes": [{"name": ..., "type": ..., "parameters": {...},
"credentials": {...}}, ...],
"connections": {"<source node name>": {"<kind>": [[{"node": ...}, ...]]}}}
The translation performed here is not a syntactic rename. Three decisions shape every fault tree that comes out of it, and each is recorded on the block it applies to so the analyst can see it and argue with it:
Not every n8n node is a component. A sticky note has no runtime behaviour, and a language-model sub-node is not a step in the flow: it is the resource an agent generates with. Folding
lmChatOpenAiinto the agent’sresources["llm"]is what lets two agents that share one model form a common-cause group instead of looking independent (SystemModel.common_cause_groups()). Modelled as separate LLM components they would each carry their own hallucination event and the shared snapshot would vanish from the analysis.The ``ai_*`` connections run backwards. n8n draws a tool, a memory or a model into the agent, so the arrow in the JSON points from the sub-node to the agent. For a memory or a parser that is also the direction failures propagate, and the edge is kept as-is. For a tool it is not: the agent decides to call the tool, so the invocation runs agent to tool, the observation comes back (a loop, cut by
make_acyclic()), and an outward action such as sending an email is delivered at the system boundary.A branching n8n node keeps its own branch logic. In LangGraph the routing function is anonymous and is materialised as a
<node>::routercomponent. An n8nIforSwitchis the router, so it is givenRole.ROUTERdirectly and its outgoing edges are not marked conditional, which would otherwise create a second, empty router beside it.
Everything else follows the ordinary pipeline: annotate, synthesise, quantify.
>>> from hiphopsllm.io.n8n import load_n8n
>>> wf = load_n8n("Gmail Agent.json")
>>> print(wf.ledger_markdown())
>>> report = wf.analyse()
>>> report.cut_sets("H2")
- class hiphopsllm.io.n8n.N8nBlock[source]¶
Bases:
objectOne n8n node, with the modelling decision taken for it and its reason.
-
rule:
Rule¶
- __init__(name, node_type, type_version, rule, kind, role, resources=<factory>, credentials=<factory>, attached_to=<factory>, connection_kind='main', side_effect=False, model_authored_args=False, notes=<factory>, source_code='')¶
- Parameters:
- Return type:
None
-
rule:
- class hiphopsllm.io.n8n.N8nWorkflow[source]¶
Bases:
objectAn n8n export, translated into a specification the analysis can read.
- extra_logic(system)[source]¶
Commission logic for a workflow that can act on the world.
The library’s annotations describe a system that answers. An n8n workflow with a Gmail tool also does things, and the failure that matters most there has no analogue in a question-answering pipeline: the agent acts when it should have stayed silent. That is a commission, and commission is the one guideword the default annotations leave empty for an agent, so it is added here rather than left to be discovered later.
- Return type:
- Parameters:
system (SystemModel)
- hazards(system)[source]¶
The hazard list, reworded for a workflow that acts on the world.
The library generates one commission top event per tool and words it as unsafe code execution, because that is what commission means for a LangGraph tool. In n8n it usually means an outward action: an email sent, a row written, a message posted. Two changes are made here, both before synthesis so that the FMEA and the effect columns carry them:
a commission hazard on a node that acts outside the workflow is renamed and marked critical;
a commission hazard on a node with no commission logic behind it is dropped. Reporting it at P = 0 would read as a quantified claim when it is an absence of modelling.
- Return type:
- Parameters:
system (SystemModel)
- analyse(unroll=1, **kwargs)[source]¶
Extract, annotate (with the n8n commission logic) and synthesise.
- __init__(name, raw, blocks=<factory>, edges=<factory>, notes=<factory>, tool_feedback=True, host_resource=True)¶
- hiphopsllm.io.n8n.load_n8n(source, name=None, *, tool_feedback=True, host_resource=True, role_overrides=None)[source]¶
Read an n8n workflow export.
- Parameters:
source (
Any) – Path to the exported.json, the JSON text itself, or the parsed dict.tool_feedback (
bool) – Keep the observation edge from a tool back to its agent. It closes a loop, whichmake_acyclic()cuts, adding the two failures a bounded loop introduces on its own: not converging, and the latency of iterating. SetFalsefor a strictly feed-forward reading.host_resource (
bool) – Give every component a sharedruntimeresource named after the n8n instance. It is true (one process, one host) and it makes the host a visible common cause instead of an unstated assumption.role_overrides (
Optional[Dict[str,Role|str]]) – Force the role of a node the rule table gets wrong.name (str | None)
- Return type:
- hiphopsllm.io.n8n.n8n_to_spec(source, **kwargs)[source]¶
Read an n8n export straight to a graph specification dict.
- hiphopsllm.io.n8n.analyse_n8n(source, **kwargs)[source]¶
Read an n8n export and run the whole structural analysis on it.
- hiphopsllm.io.n8n.n8n_study(source, **kwargs)[source]¶
Read an n8n export into a study ready for
observe().
- hiphopsllm.io.n8n.RULES: Tuple[Rule, ...] = (Rule(name='sticky note', test=<function _is.<locals>.<lambda>>, kind='excluded', role=None, resource_kind=None, why='A sticky note is documentation drawn on the canvas. It has no inputs, no outputs and no runtime behaviour, so it cannot fail and it is excluded from the architecture rather than given empty logic.', acts=None), Rule(name='trigger', test=<function <lambda>>, kind='component', role=<Role.SOURCE: 'source'>, resource_kind=None, why='The trigger is the system boundary: it is where a task enters. Its failure modes are boundary modes (no task arrives when one should, or an ill-posed task arrives), not internal ones, so it is given the SOURCE annotation and no processing logic.', acts=None), Rule(name='language model sub-node', test=<function <lambda>>, kind='resource', role=None, resource_kind='llm', why="A model sub-node is not a step in the flow, it is the resource the agent generates with. Folding it into resources['llm'] is what makes two agents on one model a common-cause group; modelled as its own component it would carry a second hallucination event and the shared snapshot would disappear from the analysis.", acts=None), Rule(name='embeddings sub-node', test=<function _startswith.<locals>.<lambda>>, kind='resource', role=None, resource_kind='embedding', why='Same argument as the language model: an embedding model is a shared resource of whatever retrieves with it, and two retrievers on one embedding model are not independent.', acts=None), Rule(name='memory sub-node', test=<function _startswith.<locals>.<lambda>>, kind='component', role=<Role.TOOL: 'tool'>, resource_kind=None, why='Memory is a retrieval step the agent depends on. It has two distinct modes: the store is unreachable and nothing comes back (omission), or the wrong conversation comes back because the session key collides (a plausible, undetectable value deviation). The TOOL annotation carries exactly that pair.', acts=False), Rule(name='output parser sub-node', test=<function _startswith.<locals>.<lambda>>, kind='component', role=<Role.TRANSFORM: 'transform'>, resource_kind=None, why='A parser is deterministic: it either fails loudly or it corrupts the payload. That is the TRANSFORM annotation.', acts=False), Rule(name='vector store / retriever sub-node', test=<function _startswith.<locals>.<lambda>>, kind='component', role=<Role.TOOL: 'tool'>, resource_kind=None, why='Retrieval supplies evidence the agent will trust. Returning nothing is an omission; returning the wrong passage is a subtle value deviation the agent cannot detect, which is the TOOL annotation.', acts=False), Rule(name='agent / LLM chain', test=<function <lambda>>, kind='component', role=<Role.LLM_AGENT: 'llm_agent'>, resource_kind=None, why='A node whose output is produced by a language model. It gets the full LLM annotation: hallucination and sampling non-determinism as subtle value deviations, format violation and truncation as coarse ones, empty generation and context overflow as omissions, plus latency. It is also transparent to a subtle deviation arriving at its input: it has no way to detect one.', acts=None), Rule(name='tool node', test=<function _endswith.<locals>.<lambda>>, kind='component', role=<Role.TOOL: 'tool'>, resource_kind=None, why='A node attached to an agent by an ai_tool connection. The agent, not the flow, decides when to call it, so the invocation edge runs from the agent to the tool and the observation comes back as a loop.', acts=None), Rule(name='code node', test=<function _is.<locals>.<lambda>>, kind='component', role=<Role.TOOL: 'tool'>, resource_kind=None, why="Hand-written code executed inside the workflow. Parse failure, raised exception and 'runs cleanly but computes the wrong thing' are the three TOOL modes, and the third is the one nothing downstream can catch.", acts=None), Rule(name='branch node', test=<function _is.<locals>.<lambda>>, kind='component', role=<Role.ROUTER: 'router'>, resource_kind=None, why='The branch decision lives inside this node, unlike LangGraph where it lives in an anonymous callable. It is therefore given the ROUTER annotation directly (no branch matched, wrong branch taken, early termination) and its outgoing edges are not marked conditional, which would create a second empty router beside it.', acts=None), Rule(name='merge node', test=<function _is.<locals>.<lambda>>, kind='component', role=<Role.AGGREGATOR: 'aggregator'>, resource_kind=None, why='A fan-in node. This is the only annotation in the library that expresses redundancy: omission and value deviations need ALL inputs to deviate, unless the selection itself is wrong. Any common cause shared by the inputs collapses that AND gate back to a single point of failure.', acts=None), Rule(name='HTTP request', test=<function _is.<locals>.<lambda>>, kind='component', role=<Role.TOOL: 'tool'>, resource_kind=None, why='An external call. It can return nothing, return an error body that the flow reads as data, or return a well-formed wrong answer.', acts=None), Rule(name='data shaping', test=<function _is.<locals>.<lambda>>, kind='component', role=<Role.TRANSFORM: 'transform'>, resource_kind=None, why='Deterministic shaping of the payload. Two modes: it throws (omission) or it writes a malformed value (coarse value deviation).', acts=None), Rule(name='service node', test=<function <lambda>>, kind='component', role=<Role.TOOL: 'tool'>, resource_kind=None, why="A node that talks to an outside service. Modelled as a tool because its failure modes are a tool's: no result, an error surfaced as a result, or a clean call that did the wrong thing.", acts=None))¶
gmailToolmust be read as a tool before it is read as a Gmail node, and a trigger must be read as the boundary before anything else claims it.- Type:
Ordered; the first rule that matches wins. The order matters
- hiphopsllm.io.n8n.SIDE_EFFECT_SERVICES = ('gmail', 'emailsend', 'microsoftoutlook', 'slack', 'telegram', 'discord', 'whatsapp', 'twilio', 'googlesheets', 'googledrive', 'googlecalendar', 'notion', 'airtable', 'postgres', 'mysql', 'mongodb', 'redis', 's3', 'awss3', 'hubspot', 'salesforce', 'jira', 'github', 'gitlab', 'trello', 'asana', 'clickup', 'stripe', 'shopify', 'webhookresponse', 'respondtowebhook', 'executecommand', 'ssh', 'ftp', 'http request')¶
Services whose nodes act on the world. Reaching one of these is not an internal state change: an email leaves, a row is written, a message is posted.
- hiphopsllm.io.n8n.READ_ONLY_OPERATIONS = ('get', 'getall', 'getmany', 'read', 'search', 'lookup', 'download', 'list', 'query', 'select', 'find')¶
Operations that only read. An n8n node’s
operationparameter is the only honest signal available without executing the workflow; anything not on this list is treated as acting on the world, because under-calling a side effect is the more dangerous mistake.