Source code for hiphopsllm.architecture.extract
"""A configurable, reusable extractor for LangGraph architectures.
:func:`~hiphopsllm.architecture.model.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. :class:`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.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, Optional, Tuple
from .acyclic import CycleReport, make_acyclic
from .model import Role, SystemModel, extract_architecture
__all__ = ["LangGraphExtractor"]
[docs]
@dataclass
class LangGraphExtractor:
"""Turn LangGraph objects into :class:`SystemModel` objects, under fixed conventions.
Parameters
----------
globals_ns
Pass ``globals()`` 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
Explicit ``{node_id: function}`` mapping, for when the functions are not
in ``globals_ns`` (a script, a class, an imported module). Takes
precedence over ``globals_ns``.
role_overrides
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
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
Iterations of each feedback loop represented explicitly (default 1).
materialise_routers
Turn ``add_conditional_edges`` into an explicit router component with its
own failure logic, rather than an anonymous branch (default ``True``).
"""
globals_ns: Optional[Dict[str, Any]] = None
node_functions: Optional[Dict[str, Callable[..., Any]]] = None
role_overrides: Dict[str, Role | str] = field(default_factory=dict)
resource_overrides: Dict[str, Dict[str, str]] = field(default_factory=dict)
unroll: int = 1
materialise_routers: bool = True
# -- fluent configuration ----------------------------------------------- #
[docs]
def with_roles(self, **roles: Role | str) -> "LangGraphExtractor":
"""Return a copy with additional role overrides."""
merged = {**self.role_overrides, **roles}
return self._replace(role_overrides=merged)
[docs]
def with_resources(self, **resources: Dict[str, str]) -> "LangGraphExtractor":
"""Return a copy with additional shared-resource declarations."""
merged = {**self.resource_overrides, **resources}
return self._replace(resource_overrides=merged)
def _replace(self, **changes: Any) -> "LangGraphExtractor":
base = {
"globals_ns": self.globals_ns,
"node_functions": self.node_functions,
"role_overrides": dict(self.role_overrides),
"resource_overrides": dict(self.resource_overrides),
"unroll": self.unroll,
"materialise_routers": self.materialise_routers,
}
base.update(changes)
return LangGraphExtractor(**base)
# -- extraction ---------------------------------------------------------- #
[docs]
def extract(self, graph: Any, name: str = "langgraph_system") -> SystemModel:
"""Read the architecture as it is, feedback loops and all.
``graph`` may be a compiled LangGraph, the drawable from
``graph.get_graph()``, mermaid text, a dict specification, or an
already-built :class:`SystemModel` (returned unchanged).
"""
return extract_architecture(
graph,
name=name,
role_overrides=self.role_overrides or None,
resource_overrides=self.resource_overrides or None,
globals_ns=self.globals_ns,
node_functions=self.node_functions,
materialise_routers=self.materialise_routers,
)
[docs]
def extract_acyclic(
self, graph: Any, name: str = "langgraph_system"
) -> Tuple[SystemModel, CycleReport]:
"""Extract, then unroll feedback loops to :attr:`unroll` iterations.
Returns the analysable (acyclic) model and the :class:`CycleReport`
recording what was cut, so the loop handling is visible in the report
rather than hidden in the tree.
"""
return make_acyclic(self.extract(graph, name=name), unroll=self.unroll)
__call__ = extract