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.

class hiphopsllm.io.examples.ExampleSpec[source]

Bases: object

One bundled architecture.

key: str
filename: str
title: str
summary: str
__init__(key, filename, title, summary)
Parameters:
Return type:

None

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"))
Return type:

Dict[str, Any]

Parameters:

key (str)

hiphopsllm.io.examples.load_outcomes(as_frame=True)[source]

Synthetic per-agent outcomes for the parallel_aggregator example.

240 items labelled with a StrategyQA-shaped stratum (short / medium / long), one correctness column per agent, and a split column so the calibration/evaluation separation can be demonstrated. 1 means 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.

Return type:

Any

Parameters:

as_frame (bool)

hiphopsllm.io.examples.describe_examples()[source]

A printable catalogue of what is bundled.

Return type:

str

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:

  1. 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 lmChatOpenAi into the agent’s resources["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.

  2. 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.

  3. A branching n8n node keeps its own branch logic. In LangGraph the routing function is anonymous and is materialised as a <node>::router component. An n8n If or Switch is the router, so it is given Role.ROUTER directly 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: object

One n8n node, with the modelling decision taken for it and its reason.

name: str
node_type: str
type_version: str
rule: Rule
kind: str
role: Optional[Role]
resources: Dict[str, str]
credentials: Dict[str, str]
attached_to: List[str]
connection_kind: str = 'main'
side_effect: bool = False
model_authored_args: bool = False
notes: List[str]
source_code: str = ''
property role_name: str
flags()[source]
Return type:

List[str]

__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

class hiphopsllm.io.n8n.N8nWorkflow[source]

Bases: object

An n8n export, translated into a specification the analysis can read.

name: str
raw: Dict[str, Any]
blocks: Dict[str, N8nBlock]
edges: List[Tuple[str, str, str, bool]]
notes: List[str]
tool_feedback: bool = True
host_resource: bool = True
to_spec()[source]

The dict specification consumed by extract_architecture().

Return type:

Dict[str, Any]

system(**kwargs)[source]
Return type:

SystemModel

Parameters:

kwargs (Any)

ledger()[source]

One row per n8n node: what it was modelled as, and why.

Return type:

List[Dict[str, str]]

ledger_frame()[source]
ledger_markdown()[source]
Return type:

str

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:

Dict[str, ComponentFailureLogic]

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:

List[Any]

Parameters:

system (SystemModel)

analyse(unroll=1, **kwargs)[source]

Extract, annotate (with the n8n commission logic) and synthesise.

Return type:

Any

Parameters:
study(unroll=1, **kwargs)[source]

An AgenticReliabilityStudy with the analysis already run.

Return type:

Any

Parameters:
summary()[source]
Return type:

str

__init__(name, raw, blocks=<factory>, edges=<factory>, notes=<factory>, tool_feedback=True, host_resource=True)
Parameters:
Return type:

None

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, which make_acyclic() cuts, adding the two failures a bounded loop introduces on its own: not converging, and the latency of iterating. Set False for a strictly feed-forward reading.

  • host_resource (bool) – Give every component a shared runtime resource 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:

N8nWorkflow

hiphopsllm.io.n8n.n8n_to_spec(source, **kwargs)[source]

Read an n8n export straight to a graph specification dict.

Return type:

Dict[str, Any]

Parameters:
hiphopsllm.io.n8n.analyse_n8n(source, **kwargs)[source]

Read an n8n export and run the whole structural analysis on it.

Return type:

Any

Parameters:
hiphopsllm.io.n8n.n8n_study(source, **kwargs)[source]

Read an n8n export into a study ready for observe().

Return type:

Any

Parameters:
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))

gmailTool must 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 operation parameter 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.