- Graph definitions (v3, v4) now declare MODELS mapping role → model string - engine.py extracts MODELS and applies to nodes during instantiation - frame_engine.process_message() accepts model_overrides for per-request swaps (restored via try/finally after processing) - 11/11 engine tests green Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
4.1 KiB
Python
115 lines
4.1 KiB
Python
"""Graph Engine: loads graph definitions, instantiates nodes, executes pipelines."""
|
|
|
|
import importlib
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from .nodes import NODE_REGISTRY
|
|
from .process import ProcessManager
|
|
|
|
log = logging.getLogger("runtime")
|
|
|
|
GRAPHS_DIR = Path(__file__).parent / "graphs"
|
|
|
|
|
|
def list_graphs() -> list[dict]:
|
|
"""List all available graph definitions."""
|
|
graphs = []
|
|
for f in sorted(GRAPHS_DIR.glob("*.py")):
|
|
if f.name.startswith("_"):
|
|
continue
|
|
mod = _load_graph_module(f.stem)
|
|
if mod:
|
|
graphs.append({
|
|
"name": getattr(mod, "NAME", f.stem),
|
|
"description": getattr(mod, "DESCRIPTION", ""),
|
|
"file": f.name,
|
|
})
|
|
return graphs
|
|
|
|
|
|
def load_graph(name: str) -> dict:
|
|
"""Load a graph definition by name. Returns the module's attributes as a dict."""
|
|
# Try matching by NAME attribute first, then by filename
|
|
for f in GRAPHS_DIR.glob("*.py"):
|
|
if f.name.startswith("_"):
|
|
continue
|
|
mod = _load_graph_module(f.stem)
|
|
if mod and getattr(mod, "NAME", "") == name:
|
|
return _graph_from_module(mod)
|
|
# Fallback: match by filename stem
|
|
mod = _load_graph_module(name)
|
|
if mod:
|
|
return _graph_from_module(mod)
|
|
raise ValueError(f"Graph '{name}' not found")
|
|
|
|
|
|
def _load_graph_module(stem: str):
|
|
"""Import a graph module by stem name."""
|
|
try:
|
|
return importlib.import_module(f".graphs.{stem}", package="agent")
|
|
except (ImportError, ModuleNotFoundError) as e:
|
|
log.error(f"[engine] failed to load graph '{stem}': {e}")
|
|
return None
|
|
|
|
|
|
def _graph_from_module(mod) -> dict:
|
|
"""Extract graph definition from a module."""
|
|
return {
|
|
"name": getattr(mod, "NAME", "unknown"),
|
|
"description": getattr(mod, "DESCRIPTION", ""),
|
|
"nodes": getattr(mod, "NODES", {}),
|
|
"edges": getattr(mod, "EDGES", []),
|
|
"conditions": getattr(mod, "CONDITIONS", {}),
|
|
"audit": getattr(mod, "AUDIT", {}),
|
|
"engine": getattr(mod, "ENGINE", "imperative"),
|
|
"models": getattr(mod, "MODELS", {}),
|
|
}
|
|
|
|
|
|
def instantiate_nodes(graph: dict, send_hud, process_manager: ProcessManager = None) -> dict:
|
|
"""Create node instances from a graph definition. Returns {role: node_instance}."""
|
|
nodes = {}
|
|
for role, impl_name in graph["nodes"].items():
|
|
cls = NODE_REGISTRY.get(impl_name)
|
|
if not cls:
|
|
log.error(f"[engine] node class not found: {impl_name}")
|
|
continue
|
|
# Thinker and Expert nodes accept process_manager
|
|
if impl_name.startswith("thinker") or impl_name.endswith("_expert"):
|
|
nodes[role] = cls(send_hud=send_hud, process_manager=process_manager)
|
|
else:
|
|
nodes[role] = cls(send_hud=send_hud)
|
|
# Apply model from graph config (overrides class default)
|
|
model = graph.get("models", {}).get(role)
|
|
if model and hasattr(nodes[role], "model"):
|
|
nodes[role].model = model
|
|
log.info(f"[engine] {role} = {impl_name} ({cls.__name__}) model={model}")
|
|
else:
|
|
log.info(f"[engine] {role} = {impl_name} ({cls.__name__})")
|
|
return nodes
|
|
|
|
|
|
def get_graph_for_cytoscape(graph: dict) -> dict:
|
|
"""Convert graph definition to Cytoscape-compatible elements for frontend."""
|
|
elements = {"nodes": [], "edges": []}
|
|
for role in graph["nodes"]:
|
|
elements["nodes"].append({"data": {"id": role, "label": role}})
|
|
for edge in graph["edges"]:
|
|
src = edge["from"]
|
|
targets = edge["to"] if isinstance(edge["to"], list) else [edge["to"]]
|
|
edge_type = edge.get("type", "data")
|
|
for tgt in targets:
|
|
elements["edges"].append({
|
|
"data": {
|
|
"id": f"e-{src}-{tgt}",
|
|
"source": src,
|
|
"target": tgt,
|
|
"edge_type": edge_type,
|
|
"condition": edge.get("condition", ""),
|
|
"carries": edge.get("carries", ""),
|
|
"method": edge.get("method", ""),
|
|
},
|
|
})
|
|
return elements
|