chorusgraph 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chorusgraph/__init__.py +57 -0
- chorusgraph/adapter/__init__.py +5 -0
- chorusgraph/adapter/wrap.py +249 -0
- chorusgraph/agents/__init__.py +43 -0
- chorusgraph/agents/agent.py +69 -0
- chorusgraph/agents/agent_node.py +82 -0
- chorusgraph/agents/loop.py +137 -0
- chorusgraph/agents/plan_solve.py +50 -0
- chorusgraph/agents/plan_utils.py +98 -0
- chorusgraph/agents/policy.py +100 -0
- chorusgraph/agents/react.py +48 -0
- chorusgraph/agents/react_utils.py +31 -0
- chorusgraph/agents/reflection.py +54 -0
- chorusgraph/agents/strategies/__init__.py +20 -0
- chorusgraph/agents/strategies/base.py +53 -0
- chorusgraph/agents/strategies/plan_solve_strategy.py +141 -0
- chorusgraph/agents/strategies/react_strategy.py +141 -0
- chorusgraph/agents/strategies/reflection_strategy.py +64 -0
- chorusgraph/cache_gate/__init__.py +23 -0
- chorusgraph/cache_gate/backend.py +251 -0
- chorusgraph/cache_gate/decision.py +33 -0
- chorusgraph/cache_gate/gate.py +168 -0
- chorusgraph/cache_gate/scope.py +34 -0
- chorusgraph/cache_gate/seed_policy.py +56 -0
- chorusgraph/cache_gate/sidecar.py +220 -0
- chorusgraph/cache_gate/thresholds.py +49 -0
- chorusgraph/checkpoint/__init__.py +27 -0
- chorusgraph/checkpoint/prism.py +93 -0
- chorusgraph/compat/__init__.py +0 -0
- chorusgraph/compat/checkpoint_import.py +75 -0
- chorusgraph/compat/langgraph.py +160 -0
- chorusgraph/compat/otel_exporter.py +36 -0
- chorusgraph/compat/tool_node.py +24 -0
- chorusgraph/compose/__init__.py +47 -0
- chorusgraph/compose/adapters/__init__.py +13 -0
- chorusgraph/compose/adapters/keyword_retrieval.py +51 -0
- chorusgraph/compose/adapters/memory.py +47 -0
- chorusgraph/compose/adapters/prism_cache.py +100 -0
- chorusgraph/compose/adapters/prismrag_retrieval.py +228 -0
- chorusgraph/compose/adapters/redis_cache.py +178 -0
- chorusgraph/compose/defaults.py +58 -0
- chorusgraph/compose/ports.py +129 -0
- chorusgraph/compose/stack.py +172 -0
- chorusgraph/core/__init__.py +22 -0
- chorusgraph/core/bus.py +93 -0
- chorusgraph/core/cache_interceptor.py +136 -0
- chorusgraph/core/channels.py +255 -0
- chorusgraph/core/constants.py +6 -0
- chorusgraph/core/envelope.py +59 -0
- chorusgraph/core/graph.py +261 -0
- chorusgraph/core/ir.py +155 -0
- chorusgraph/core/node.py +172 -0
- chorusgraph/core/pending_writes.py +128 -0
- chorusgraph/core/persistence.py +324 -0
- chorusgraph/core/scheduler.py +1613 -0
- chorusgraph/core/send.py +177 -0
- chorusgraph/core/subgraph.py +180 -0
- chorusgraph/core/subgraph_transport.py +208 -0
- chorusgraph/core/trace.py +226 -0
- chorusgraph/core/transport_router.py +140 -0
- chorusgraph/embedders.py +51 -0
- chorusgraph/examples/__init__.py +0 -0
- chorusgraph/examples/demo_graph.py +129 -0
- chorusgraph/examples/finance_agent/__init__.py +0 -0
- chorusgraph/examples/finance_agent/gemini_client.py +108 -0
- chorusgraph/examples/finance_agent/graph.py +98 -0
- chorusgraph/examples/finance_agent/nodes.py +480 -0
- chorusgraph/examples/finance_agent/pattern_nodes.py +194 -0
- chorusgraph/examples/finance_agent/patterns_graph.py +201 -0
- chorusgraph/examples/finance_agent/run.py +64 -0
- chorusgraph/examples/finance_agent/run_memory_demo.py +193 -0
- chorusgraph/examples/finance_agent/run_patterns_demo.py +92 -0
- chorusgraph/examples/finance_agent/runtime.py +126 -0
- chorusgraph/examples/multi_agent_graph.py +54 -0
- chorusgraph/func.py +84 -0
- chorusgraph/ledger/__init__.py +15 -0
- chorusgraph/ledger/instrument.py +57 -0
- chorusgraph/ledger/models.py +46 -0
- chorusgraph/ledger/query.py +24 -0
- chorusgraph/ledger/sink.py +267 -0
- chorusgraph/memory/__init__.py +16 -0
- chorusgraph/memory/async_digest.py +80 -0
- chorusgraph/memory/cortex_compat.py +52 -0
- chorusgraph/memory/cortex_service.py +195 -0
- chorusgraph/memory/recall.py +28 -0
- chorusgraph/memory/structured_recall.py +59 -0
- chorusgraph/nodes/__init__.py +33 -0
- chorusgraph/nodes/retrieve.py +110 -0
- chorusgraph/nodes/roles.py +102 -0
- chorusgraph/nodes/tool.py +207 -0
- chorusgraph/observability/__init__.py +18 -0
- chorusgraph/observability/health.py +60 -0
- chorusgraph/observability/logging.py +58 -0
- chorusgraph/observability/metrics.py +51 -0
- chorusgraph/observability/otel.py +44 -0
- chorusgraph/persistence/__init__.py +22 -0
- chorusgraph/persistence/backup.py +63 -0
- chorusgraph/persistence/cortex_factory.py +66 -0
- chorusgraph/persistence/lifecycle.py +107 -0
- chorusgraph/persistence/migrations.py +114 -0
- chorusgraph/persistence/sqlite_graph_store.py +126 -0
- chorusgraph/policy/__init__.py +5 -0
- chorusgraph/policy/embedder_guard.py +74 -0
- chorusgraph/public.py +62 -0
- chorusgraph/resilience/__init__.py +23 -0
- chorusgraph/resilience/circuit_breaker.py +104 -0
- chorusgraph/resilience/errors.py +72 -0
- chorusgraph/resilience/executor.py +71 -0
- chorusgraph/resilience/idempotency.py +29 -0
- chorusgraph/resilience/partial.py +34 -0
- chorusgraph/resilience/policy.py +42 -0
- chorusgraph/sections/__init__.py +5 -0
- chorusgraph/sections/models.py +46 -0
- chorusgraph/sections/profiles.py +46 -0
- chorusgraph/security/__init__.py +21 -0
- chorusgraph/security/auth.py +42 -0
- chorusgraph/security/cache.py +47 -0
- chorusgraph/security/pii.py +35 -0
- chorusgraph/security/tools.py +71 -0
- chorusgraph/security/transport.py +43 -0
- chorusgraph/shadow/__init__.py +19 -0
- chorusgraph/shadow/dataset/__init__.py +0 -0
- chorusgraph/shadow/harness.py +182 -0
- chorusgraph/shadow/replay/__init__.py +19 -0
- chorusgraph/shadow/replay/cli.py +43 -0
- chorusgraph/shadow/replay/ingest.py +30 -0
- chorusgraph/shadow/replay/policies.py +18 -0
- chorusgraph/shadow/replay/replay.py +166 -0
- chorusgraph/shadow/replay/report.py +116 -0
- chorusgraph/shadow/replay/schema.py +31 -0
- chorusgraph/shadow/replay/stats.py +69 -0
- chorusgraph/shadow/report.py +96 -0
- chorusgraph/shadow/results_store.py +107 -0
- chorusgraph/tenant/__init__.py +17 -0
- chorusgraph/tenant/context.py +27 -0
- chorusgraph/tenant/isolation.py +33 -0
- chorusgraph/tenant/limits.py +59 -0
- chorusgraph/transforms/__init__.py +17 -0
- chorusgraph/transforms/cortex_projector.py +61 -0
- chorusgraph/transforms/intent.py +59 -0
- chorusgraph/transforms/projector.py +49 -0
- chorusgraph/transforms/templates.py +129 -0
- chorusgraph/transport/__init__.py +30 -0
- chorusgraph/transport/chorus.py +222 -0
- chorusgraph/transport/context.py +63 -0
- chorusgraph/transport/envelope.py +79 -0
- chorusgraph/transport/inproc.py +61 -0
- chorusgraph/transport/modes.py +23 -0
- chorusgraph/transport/prismapi.py +164 -0
- chorusgraph/transport/spine.py +66 -0
- chorusgraph-1.0.1.dist-info/METADATA +283 -0
- chorusgraph-1.0.1.dist-info/RECORD +156 -0
- chorusgraph-1.0.1.dist-info/WHEEL +5 -0
- chorusgraph-1.0.1.dist-info/entry_points.txt +9 -0
- chorusgraph-1.0.1.dist-info/licenses/LICENSE +17 -0
- chorusgraph-1.0.1.dist-info/top_level.txt +1 -0
chorusgraph/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""ChorusGraph — native Prism execution engine with cache, memory, and Route Ledger."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.1"
|
|
4
|
+
|
|
5
|
+
from chorusgraph.adapter import RunnableWithLedger, wrap
|
|
6
|
+
from chorusgraph.cache_gate import Decision, DecisionKind, SidecarStore, gate, seed_cache_entry
|
|
7
|
+
from chorusgraph.checkpoint import PrismCheckpointer, create_checkpointer, sqlite_checkpointer
|
|
8
|
+
from chorusgraph.compose import ChorusStack, RedisCacheBackend
|
|
9
|
+
from chorusgraph.core import END, START, CompiledGraph, Graph, NodeContext, NodeFn
|
|
10
|
+
from chorusgraph.transport import TransportMode, publish_hop
|
|
11
|
+
from chorusgraph.ledger import (
|
|
12
|
+
LedgerStep,
|
|
13
|
+
LedgerSink,
|
|
14
|
+
PostgresLedgerSink,
|
|
15
|
+
RouteLedger,
|
|
16
|
+
SqliteLedgerSink,
|
|
17
|
+
get_run,
|
|
18
|
+
list_runs,
|
|
19
|
+
)
|
|
20
|
+
from chorusgraph.memory import CortexMemoryService, get_cortex_service
|
|
21
|
+
from chorusgraph.sections import CachePolicy, Section
|
|
22
|
+
from chorusgraph.shadow import run_shadow_measurement
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"CachePolicy",
|
|
27
|
+
"CompiledGraph",
|
|
28
|
+
"CortexMemoryService",
|
|
29
|
+
"ChorusStack",
|
|
30
|
+
"Decision",
|
|
31
|
+
"DecisionKind",
|
|
32
|
+
"END",
|
|
33
|
+
"Graph",
|
|
34
|
+
"NodeContext",
|
|
35
|
+
"NodeFn",
|
|
36
|
+
"LedgerSink",
|
|
37
|
+
"LedgerStep",
|
|
38
|
+
"TransportMode",
|
|
39
|
+
"PrismCheckpointer",
|
|
40
|
+
"RedisCacheBackend",
|
|
41
|
+
"RouteLedger",
|
|
42
|
+
"RunnableWithLedger",
|
|
43
|
+
"Section",
|
|
44
|
+
"SidecarStore",
|
|
45
|
+
"SqliteLedgerSink",
|
|
46
|
+
"START",
|
|
47
|
+
"create_checkpointer",
|
|
48
|
+
"gate",
|
|
49
|
+
"get_cortex_service",
|
|
50
|
+
"get_run",
|
|
51
|
+
"list_runs",
|
|
52
|
+
"publish_hop",
|
|
53
|
+
"run_shadow_measurement",
|
|
54
|
+
"seed_cache_entry",
|
|
55
|
+
"sqlite_checkpointer",
|
|
56
|
+
"wrap",
|
|
57
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Adapter — native CompiledGraph ledger + legacy LangGraph fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from chorusgraph.ledger.models import LedgerStep, RouteLedger
|
|
11
|
+
from chorusgraph.ledger.sink import LedgerSink, SqliteLedgerSink
|
|
12
|
+
|
|
13
|
+
_APPEND_KEYS = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"rule_chain",
|
|
16
|
+
"prism_sequence",
|
|
17
|
+
"hop_metrics",
|
|
18
|
+
"vector_hops",
|
|
19
|
+
"pipeline_trace",
|
|
20
|
+
"tool_calls",
|
|
21
|
+
"conversation_history",
|
|
22
|
+
"agent_trace",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _merge_state(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
|
|
28
|
+
merged = dict(base)
|
|
29
|
+
for key, value in update.items():
|
|
30
|
+
if key in _APPEND_KEYS and key in merged:
|
|
31
|
+
left = merged[key]
|
|
32
|
+
if isinstance(left, list) and isinstance(value, list):
|
|
33
|
+
merged[key] = left + value
|
|
34
|
+
continue
|
|
35
|
+
if (
|
|
36
|
+
key not in _APPEND_KEYS
|
|
37
|
+
and key in merged
|
|
38
|
+
and isinstance(merged[key], list)
|
|
39
|
+
and isinstance(value, list)
|
|
40
|
+
):
|
|
41
|
+
merged[key] = merged[key] + value
|
|
42
|
+
else:
|
|
43
|
+
merged[key] = value
|
|
44
|
+
return merged
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_edge_from_triggers(triggers: Optional[List[str]]) -> Optional[str]:
|
|
48
|
+
if not triggers:
|
|
49
|
+
return None
|
|
50
|
+
for trigger in triggers:
|
|
51
|
+
if trigger.startswith("branch:to:"):
|
|
52
|
+
return trigger.split(":", 2)[-1]
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_grounding_score(
|
|
57
|
+
result: Dict[str, Any],
|
|
58
|
+
node_name: str,
|
|
59
|
+
*,
|
|
60
|
+
merged_state: Optional[Dict[str, Any]] = None,
|
|
61
|
+
) -> Optional[float]:
|
|
62
|
+
if node_name != "writer":
|
|
63
|
+
return None
|
|
64
|
+
if result.get("memory_confidence") is not None:
|
|
65
|
+
return float(result["memory_confidence"])
|
|
66
|
+
if merged_state is not None and merged_state.get("memory_confidence") is not None:
|
|
67
|
+
return float(merged_state["memory_confidence"])
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _append_agent_trace_steps(
|
|
72
|
+
ledger: RouteLedger,
|
|
73
|
+
node: str,
|
|
74
|
+
result: Dict[str, Any],
|
|
75
|
+
*,
|
|
76
|
+
timestamp: datetime,
|
|
77
|
+
) -> None:
|
|
78
|
+
trace = result.get("agent_trace") or []
|
|
79
|
+
for step in trace:
|
|
80
|
+
if isinstance(step, dict):
|
|
81
|
+
kind = step.get("kind") or "step"
|
|
82
|
+
content = step.get("content") or ""
|
|
83
|
+
else:
|
|
84
|
+
kind = getattr(step, "kind", "step")
|
|
85
|
+
if hasattr(kind, "value"):
|
|
86
|
+
kind = kind.value
|
|
87
|
+
content = getattr(step, "content", str(step))
|
|
88
|
+
ledger.add_step(
|
|
89
|
+
LedgerStep(
|
|
90
|
+
node=f"{node}/{kind}",
|
|
91
|
+
rule_chain=[content[:500] if content else kind],
|
|
92
|
+
timestamp=timestamp,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _extract_rule_chain(
|
|
98
|
+
result: Dict[str, Any],
|
|
99
|
+
node_name: str,
|
|
100
|
+
*,
|
|
101
|
+
merged_state: Optional[Dict[str, Any]] = None,
|
|
102
|
+
) -> Optional[List[str]]:
|
|
103
|
+
chain = result.get("rule_chain")
|
|
104
|
+
if chain:
|
|
105
|
+
return list(chain)
|
|
106
|
+
|
|
107
|
+
sequence = result.get("prism_sequence") or []
|
|
108
|
+
if not sequence and merged_state is not None:
|
|
109
|
+
sequence = merged_state.get("prism_sequence") or []
|
|
110
|
+
for envelope in reversed(sequence):
|
|
111
|
+
if envelope.get("agent_id") == node_name and envelope.get("rule_chain"):
|
|
112
|
+
return list(envelope["rule_chain"])
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RunnableWithLedger:
|
|
117
|
+
"""Wraps a compiled graph and ensures Route Ledger persistence."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
compiled_graph: Any,
|
|
122
|
+
*,
|
|
123
|
+
tenant_id: str,
|
|
124
|
+
graph_id: str,
|
|
125
|
+
sink: Optional[LedgerSink] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
self._graph = compiled_graph
|
|
128
|
+
self._tenant_id = tenant_id
|
|
129
|
+
self._graph_id = graph_id
|
|
130
|
+
self._sink = sink or SqliteLedgerSink()
|
|
131
|
+
self.last_ledger: Optional[RouteLedger] = None
|
|
132
|
+
|
|
133
|
+
if getattr(compiled_graph, "_native", False):
|
|
134
|
+
compiled_graph.attach_ledger(
|
|
135
|
+
tenant_id=tenant_id,
|
|
136
|
+
graph_id=graph_id,
|
|
137
|
+
sink=self._sink,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def graph(self) -> Any:
|
|
142
|
+
return self._graph
|
|
143
|
+
|
|
144
|
+
def invoke(self, input: Dict[str, Any], /, **kwargs: Any) -> Dict[str, Any]:
|
|
145
|
+
if getattr(self._graph, "_native", False):
|
|
146
|
+
result = self._graph.invoke(input, **kwargs)
|
|
147
|
+
self.last_ledger = self._graph.last_ledger
|
|
148
|
+
if self.last_ledger and result.get("agent_trace"):
|
|
149
|
+
ts = datetime.now(timezone.utc)
|
|
150
|
+
for node_name in ("react_agent", "plan_solve", "validator"):
|
|
151
|
+
if any(s.node == node_name for s in self.last_ledger.steps):
|
|
152
|
+
_append_agent_trace_steps(self.last_ledger, node_name, result, timestamp=ts)
|
|
153
|
+
break
|
|
154
|
+
return result
|
|
155
|
+
return self._run_legacy(input, kwargs, async_mode=False)
|
|
156
|
+
|
|
157
|
+
async def ainvoke(self, input: Dict[str, Any], /, **kwargs: Any) -> Dict[str, Any]:
|
|
158
|
+
if getattr(self._graph, "_native", False):
|
|
159
|
+
result = await self._graph.ainvoke(input, **kwargs)
|
|
160
|
+
self.last_ledger = self._graph.last_ledger
|
|
161
|
+
return result
|
|
162
|
+
return await self._run_legacy_async(input, kwargs)
|
|
163
|
+
|
|
164
|
+
def _run_legacy(self, input: Dict[str, Any], kwargs: Dict[str, Any], *, async_mode: bool) -> Dict[str, Any]:
|
|
165
|
+
run_id = str(uuid4())
|
|
166
|
+
turn_id = input.get("turn_id") or input.get("request_id")
|
|
167
|
+
ledger = RouteLedger(
|
|
168
|
+
run_id=run_id,
|
|
169
|
+
turn_id=str(turn_id) if turn_id else None,
|
|
170
|
+
tenant_id=self._tenant_id,
|
|
171
|
+
graph_id=self._graph_id,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
state = dict(input)
|
|
175
|
+
final_state: Dict[str, Any] = state
|
|
176
|
+
pending: Dict[str, float] = {}
|
|
177
|
+
|
|
178
|
+
stream_kwargs = dict(kwargs)
|
|
179
|
+
stream_kwargs["stream_mode"] = "debug"
|
|
180
|
+
|
|
181
|
+
for event in self._graph.stream(state, **stream_kwargs):
|
|
182
|
+
if not isinstance(event, dict):
|
|
183
|
+
continue
|
|
184
|
+
event_type = event.get("type")
|
|
185
|
+
payload = event.get("payload") or {}
|
|
186
|
+
|
|
187
|
+
if event_type == "task":
|
|
188
|
+
node = payload.get("name")
|
|
189
|
+
if not node:
|
|
190
|
+
continue
|
|
191
|
+
pending[node] = time.perf_counter()
|
|
192
|
+
edge_taken = _parse_edge_from_triggers(payload.get("triggers"))
|
|
193
|
+
if edge_taken and ledger.steps:
|
|
194
|
+
ledger.steps[-1].edge_taken = edge_taken
|
|
195
|
+
|
|
196
|
+
elif event_type == "task_result":
|
|
197
|
+
node = payload.get("name")
|
|
198
|
+
if not node:
|
|
199
|
+
continue
|
|
200
|
+
started = pending.pop(node, time.perf_counter())
|
|
201
|
+
duration_ms = int((time.perf_counter() - started) * 1000)
|
|
202
|
+
result = payload.get("result") or {}
|
|
203
|
+
if isinstance(result, dict):
|
|
204
|
+
state = _merge_state(state, result)
|
|
205
|
+
final_state = state
|
|
206
|
+
ts_raw = event.get("timestamp")
|
|
207
|
+
timestamp = (
|
|
208
|
+
datetime.fromisoformat(ts_raw)
|
|
209
|
+
if isinstance(ts_raw, str)
|
|
210
|
+
else datetime.now(timezone.utc)
|
|
211
|
+
)
|
|
212
|
+
ledger.add_step(
|
|
213
|
+
LedgerStep(
|
|
214
|
+
node=node,
|
|
215
|
+
edge_taken=None,
|
|
216
|
+
rule_chain=_extract_rule_chain(result, node, merged_state=state),
|
|
217
|
+
duration_ms=duration_ms,
|
|
218
|
+
timestamp=timestamp,
|
|
219
|
+
grounding_score=_extract_grounding_score(result, node, merged_state=state),
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
if isinstance(result, dict) and result.get("agent_trace"):
|
|
223
|
+
_append_agent_trace_steps(ledger, node, result, timestamp=timestamp)
|
|
224
|
+
|
|
225
|
+
self._sink.write(ledger)
|
|
226
|
+
self.last_ledger = ledger
|
|
227
|
+
return final_state
|
|
228
|
+
|
|
229
|
+
async def _run_legacy_async(self, input: Dict[str, Any], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
230
|
+
return self._run_legacy(input, kwargs, async_mode=True)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def wrap(
|
|
234
|
+
compiled_graph: Any,
|
|
235
|
+
*,
|
|
236
|
+
tenant_id: str,
|
|
237
|
+
graph_id: str,
|
|
238
|
+
sink: Optional[LedgerSink] = None,
|
|
239
|
+
) -> RunnableWithLedger:
|
|
240
|
+
"""Attach Route Ledger tracking to a compiled graph."""
|
|
241
|
+
return RunnableWithLedger(
|
|
242
|
+
compiled_graph,
|
|
243
|
+
tenant_id=tenant_id,
|
|
244
|
+
graph_id=graph_id,
|
|
245
|
+
sink=sink,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
__all__ = ["RunnableWithLedger", "wrap"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Agent execution patterns — unified Agent + strategies."""
|
|
2
|
+
|
|
3
|
+
from chorusgraph.agents.agent import Agent
|
|
4
|
+
from chorusgraph.agents.agent_node import AgentNode, agent_result_to_state, promote_to_agent
|
|
5
|
+
from chorusgraph.agents.loop import AgentTraceStep, LoopOutcome, TraceKind, run_agent_loop
|
|
6
|
+
from chorusgraph.agents.plan_solve import PlanSolveResult, run_plan_solve
|
|
7
|
+
from chorusgraph.agents.plan_utils import PlanStep, plan_tasks
|
|
8
|
+
from chorusgraph.agents.policy import (
|
|
9
|
+
BeliefPolicy,
|
|
10
|
+
BeliefPolicyNotCalibratedError,
|
|
11
|
+
PlanPolicy,
|
|
12
|
+
PlanSolveOpts,
|
|
13
|
+
ReActOpts,
|
|
14
|
+
ReflectionOpts,
|
|
15
|
+
)
|
|
16
|
+
from chorusgraph.agents.react import ReActResult, run_react
|
|
17
|
+
from chorusgraph.agents.reflection import ReflectionResult, ValidationVerdict, run_reflection
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Agent",
|
|
21
|
+
"AgentNode",
|
|
22
|
+
"AgentTraceStep",
|
|
23
|
+
"BeliefPolicy",
|
|
24
|
+
"BeliefPolicyNotCalibratedError",
|
|
25
|
+
"LoopOutcome",
|
|
26
|
+
"PlanPolicy",
|
|
27
|
+
"PlanSolveOpts",
|
|
28
|
+
"PlanSolveResult",
|
|
29
|
+
"PlanStep",
|
|
30
|
+
"ReActOpts",
|
|
31
|
+
"ReActResult",
|
|
32
|
+
"ReflectionOpts",
|
|
33
|
+
"ReflectionResult",
|
|
34
|
+
"TraceKind",
|
|
35
|
+
"ValidationVerdict",
|
|
36
|
+
"agent_result_to_state",
|
|
37
|
+
"plan_tasks",
|
|
38
|
+
"promote_to_agent",
|
|
39
|
+
"run_agent_loop",
|
|
40
|
+
"run_plan_solve",
|
|
41
|
+
"run_react",
|
|
42
|
+
"run_reflection",
|
|
43
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Unified configurable Agent type (DESIGN §7.8)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from chorusgraph.agents.policy import (
|
|
9
|
+
BeliefPolicy,
|
|
10
|
+
PATTERN_DEFAULTS,
|
|
11
|
+
PatternOpts,
|
|
12
|
+
PlanPolicy,
|
|
13
|
+
PlanSolveOpts,
|
|
14
|
+
ReActOpts,
|
|
15
|
+
ReflectionOpts,
|
|
16
|
+
)
|
|
17
|
+
from chorusgraph.agents.strategies import get_strategy
|
|
18
|
+
from chorusgraph.agents.strategies.base import AgentContext, AgentRunResult
|
|
19
|
+
from chorusgraph.nodes.roles import RoleTemplate
|
|
20
|
+
from chorusgraph.nodes.tool import ToolRegistry
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Agent:
|
|
25
|
+
"""
|
|
26
|
+
One agent type — pattern selected via pluggable strategy config.
|
|
27
|
+
|
|
28
|
+
>>> agent = Agent(pattern="react", tools=registry, model=gemini.generate_json)
|
|
29
|
+
>>> agent.run("Compare USD/EUR and USD/GBP")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
pattern: str
|
|
33
|
+
tools: ToolRegistry
|
|
34
|
+
model: Callable[[str, str], str]
|
|
35
|
+
role: Optional[RoleTemplate] = None
|
|
36
|
+
policy: PlanPolicy = field(default_factory=PlanPolicy)
|
|
37
|
+
pattern_opts: Optional[PatternOpts] = None
|
|
38
|
+
belief: BeliefPolicy = field(default_factory=BeliefPolicy)
|
|
39
|
+
llm_text: Optional[Callable[[str, str], str]] = None
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
if self.pattern_opts is None:
|
|
43
|
+
self.pattern_opts = PATTERN_DEFAULTS[self.pattern] # type: ignore[assignment]
|
|
44
|
+
|
|
45
|
+
def run(
|
|
46
|
+
self,
|
|
47
|
+
question: str,
|
|
48
|
+
/,
|
|
49
|
+
*,
|
|
50
|
+
scratchpad_prefix: str = "",
|
|
51
|
+
initial_draft: str = "",
|
|
52
|
+
validate: Optional[Callable[[str], Any]] = None,
|
|
53
|
+
revise: Optional[Callable[[str, Any], str]] = None,
|
|
54
|
+
) -> AgentRunResult:
|
|
55
|
+
strategy = get_strategy(self.pattern)
|
|
56
|
+
ctx = AgentContext(
|
|
57
|
+
question=question,
|
|
58
|
+
registry=self.tools,
|
|
59
|
+
llm_json=self.model,
|
|
60
|
+
llm_text=self.llm_text,
|
|
61
|
+
policy=self.policy,
|
|
62
|
+
pattern_opts=self.pattern_opts or PATTERN_DEFAULTS[self.pattern], # type: ignore[arg-type]
|
|
63
|
+
belief=self.belief,
|
|
64
|
+
scratchpad_prefix=scratchpad_prefix,
|
|
65
|
+
initial_draft=initial_draft,
|
|
66
|
+
validate=validate,
|
|
67
|
+
revise=revise,
|
|
68
|
+
)
|
|
69
|
+
return strategy.run(ctx)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""AgentNode — Agent IS-A Node (DESIGN §7.7)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from chorusgraph.agents.agent import Agent
|
|
8
|
+
from chorusgraph.agents.strategies.base import AgentRunResult
|
|
9
|
+
from chorusgraph.nodes.roles import Node, RoleTemplate, promote
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _trace_dicts(result: AgentRunResult) -> List[Dict[str, Any]]:
|
|
13
|
+
return [s.to_dict() for s in result.trace]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _rule_chain_from_result(result: AgentRunResult) -> List[str]:
|
|
17
|
+
return [s.to_rule() for s in result.trace]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def agent_result_to_state(result: AgentRunResult, *, pattern: str, prior: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
21
|
+
prior = prior or {}
|
|
22
|
+
tool_results = [obs for obs in result.observations if isinstance(obs, dict) and "rate" in obs]
|
|
23
|
+
if not tool_results and result.observations:
|
|
24
|
+
tool_results = [
|
|
25
|
+
(obs.get("data") if isinstance(obs, dict) else obs) for obs in result.observations
|
|
26
|
+
]
|
|
27
|
+
update: Dict[str, Any] = {
|
|
28
|
+
"tool_calls": list(prior.get("tool_calls") or []) + result.tool_calls,
|
|
29
|
+
"tool_results": tool_results,
|
|
30
|
+
"tool_result": tool_results[-1] if tool_results else None,
|
|
31
|
+
"needs_tool": False,
|
|
32
|
+
"agent_trace": _trace_dicts(result),
|
|
33
|
+
"rule_chain": _rule_chain_from_result(result),
|
|
34
|
+
}
|
|
35
|
+
if pattern == "react":
|
|
36
|
+
update["research_plan"] = "ReAct via Agent"
|
|
37
|
+
if pattern == "plan_solve":
|
|
38
|
+
update["plan_steps"] = [
|
|
39
|
+
{"id": s.id, "description": s.description, "tool": s.tool} for s in result.plan
|
|
40
|
+
]
|
|
41
|
+
if pattern == "reflection":
|
|
42
|
+
update["validation"] = result.validation
|
|
43
|
+
update["response"] = result.draft
|
|
44
|
+
update["draft_response"] = result.draft
|
|
45
|
+
update["reflection_pass"] = result.passes
|
|
46
|
+
return update
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def AgentNode(
|
|
50
|
+
agent: Agent,
|
|
51
|
+
node_id: str = "agent",
|
|
52
|
+
*,
|
|
53
|
+
state_mapper: Optional[Callable[[Dict[str, Any], AgentRunResult], Dict[str, Any]]] = None,
|
|
54
|
+
) -> Node:
|
|
55
|
+
"""Wrap an Agent as a LangGraph-compatible Node."""
|
|
56
|
+
|
|
57
|
+
def handler(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
58
|
+
if state.get("cache_hit") and state.get("tool_result") and agent.pattern == "react":
|
|
59
|
+
return {"agent_trace": [], "rule_chain": ["agent=skipped_cache_hit"]}
|
|
60
|
+
message = state.get("message") or ""
|
|
61
|
+
kwargs: Dict[str, Any] = {}
|
|
62
|
+
if agent.pattern == "reflection":
|
|
63
|
+
kwargs["initial_draft"] = state.get("draft_response") or ""
|
|
64
|
+
if state.get("_reflection_validate"):
|
|
65
|
+
kwargs["validate"] = state["_reflection_validate"]
|
|
66
|
+
if state.get("_reflection_revise"):
|
|
67
|
+
kwargs["revise"] = state["_reflection_revise"]
|
|
68
|
+
result = agent.run(message, **kwargs)
|
|
69
|
+
update = agent_result_to_state(result, pattern=agent.pattern, prior=state)
|
|
70
|
+
if state_mapper:
|
|
71
|
+
update = state_mapper(state, result) | update
|
|
72
|
+
return update
|
|
73
|
+
|
|
74
|
+
role = agent.role
|
|
75
|
+
node = Node(node_id=node_id, role=role, handler=handler)
|
|
76
|
+
return node
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def promote_to_agent(node: Node, agent: Agent) -> Node:
|
|
80
|
+
"""Promote a plain Node to an Agent-backed node."""
|
|
81
|
+
bound = AgentNode(agent, node_id=node.node_id)
|
|
82
|
+
return Node(node_id=node.node_id, role=agent.role or node.role, handler=bound.handler)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Generic agent-loop substrate — reason ↔ act ↔ route with observable trace."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol
|
|
8
|
+
|
|
9
|
+
from chorusgraph.agents.policy import PlanPolicy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TraceKind(str, Enum):
|
|
13
|
+
THOUGHT = "thought"
|
|
14
|
+
ACTION = "action"
|
|
15
|
+
OBSERVATION = "observation"
|
|
16
|
+
PLAN_STEP = "plan_step"
|
|
17
|
+
REVISION = "revision"
|
|
18
|
+
ROUTER = "router"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class AgentTraceStep:
|
|
23
|
+
kind: TraceKind
|
|
24
|
+
content: str
|
|
25
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
28
|
+
return {
|
|
29
|
+
"kind": self.kind.value,
|
|
30
|
+
"content": self.content,
|
|
31
|
+
"metadata": self.metadata,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def to_rule(self) -> str:
|
|
35
|
+
return f"{self.kind.value}={self.content[:240]}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LoopOutcome:
|
|
40
|
+
finished: bool
|
|
41
|
+
trace: List[AgentTraceStep] = field(default_factory=list)
|
|
42
|
+
observations: List[Any] = field(default_factory=list)
|
|
43
|
+
tokens_used: int = 0
|
|
44
|
+
steps_used: int = 0
|
|
45
|
+
finish_reason: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Reasoner(Protocol):
|
|
49
|
+
def __call__(self, *, scratchpad: str, step: int) -> Dict[str, Any]: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Actor(Protocol):
|
|
53
|
+
def __call__(self, action: Dict[str, Any]) -> Any: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Router(Protocol):
|
|
57
|
+
def __call__(self, *, reason_result: Dict[str, Any], observations: List[Any]) -> str: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_agent_loop(
|
|
61
|
+
*,
|
|
62
|
+
policy: PlanPolicy,
|
|
63
|
+
reason: Reasoner,
|
|
64
|
+
act: Optional[Actor],
|
|
65
|
+
route: Router,
|
|
66
|
+
initial_scratchpad: str = "",
|
|
67
|
+
) -> LoopOutcome:
|
|
68
|
+
"""
|
|
69
|
+
Generic cyclic machinery: llm (reason) ↔ tool (act) ↔ router (act-or-finish).
|
|
70
|
+
|
|
71
|
+
Each cycle appends Thought / Action / Observation steps to the trace.
|
|
72
|
+
"""
|
|
73
|
+
trace: List[AgentTraceStep] = []
|
|
74
|
+
observations: List[Any] = []
|
|
75
|
+
scratchpad = initial_scratchpad
|
|
76
|
+
tokens_used = 0
|
|
77
|
+
steps_used = 0
|
|
78
|
+
finished = False
|
|
79
|
+
finish_reason: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
while steps_used < policy.max_steps and policy.within_budget(tokens_used):
|
|
82
|
+
reason_result = reason(scratchpad=scratchpad, step=steps_used)
|
|
83
|
+
tokens_used += int(reason_result.get("tokens_used") or 0)
|
|
84
|
+
|
|
85
|
+
thought = (reason_result.get("thought") or "").strip()
|
|
86
|
+
if thought:
|
|
87
|
+
trace.append(AgentTraceStep(TraceKind.THOUGHT, thought))
|
|
88
|
+
|
|
89
|
+
decision = route(reason_result=reason_result, observations=observations)
|
|
90
|
+
trace.append(AgentTraceStep(TraceKind.ROUTER, decision, metadata={"step": steps_used}))
|
|
91
|
+
|
|
92
|
+
if decision == "finish":
|
|
93
|
+
finished = True
|
|
94
|
+
finish_reason = reason_result.get("finish_reason") or "router_finish"
|
|
95
|
+
steps_used += 1
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
if decision == "continue":
|
|
99
|
+
scratchpad += f"\nThought: {thought}\n(Policy: continuing — tool required before finish)\n"
|
|
100
|
+
steps_used += 1
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if decision == "act" and act is not None:
|
|
104
|
+
action = reason_result.get("action") or {}
|
|
105
|
+
tool_name = action.get("tool") or action.get("name") or "unknown"
|
|
106
|
+
trace.append(
|
|
107
|
+
AgentTraceStep(
|
|
108
|
+
TraceKind.ACTION,
|
|
109
|
+
f"{tool_name}({action.get('args') or action.get('arguments') or {}})",
|
|
110
|
+
metadata={"tool": tool_name, "args": action.get("args") or action.get("arguments") or {}},
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
observation = act(action)
|
|
114
|
+
observations.append(observation)
|
|
115
|
+
obs_text = observation if isinstance(observation, str) else str(observation)
|
|
116
|
+
trace.append(AgentTraceStep(TraceKind.OBSERVATION, obs_text[:2000], metadata={"tool": tool_name}))
|
|
117
|
+
scratchpad += f"\nThought: {thought}\nAction: {tool_name}\nObservation: {obs_text}\n"
|
|
118
|
+
else:
|
|
119
|
+
scratchpad += f"\nThought: {thought}\n"
|
|
120
|
+
if reason_result.get("error"):
|
|
121
|
+
trace.append(AgentTraceStep(TraceKind.OBSERVATION, str(reason_result["error"])))
|
|
122
|
+
|
|
123
|
+
steps_used += 1
|
|
124
|
+
|
|
125
|
+
if not finished and steps_used >= policy.max_steps:
|
|
126
|
+
finish_reason = "max_steps"
|
|
127
|
+
elif not finished and not policy.within_budget(tokens_used):
|
|
128
|
+
finish_reason = "token_budget"
|
|
129
|
+
|
|
130
|
+
return LoopOutcome(
|
|
131
|
+
finished=finished or bool(observations),
|
|
132
|
+
trace=trace,
|
|
133
|
+
observations=observations,
|
|
134
|
+
tokens_used=tokens_used,
|
|
135
|
+
steps_used=steps_used,
|
|
136
|
+
finish_reason=finish_reason,
|
|
137
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Plan-Solve Agent shim."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from chorusgraph.agents.plan_utils import PlanStep, plan_tasks, _try_compute_cross
|
|
9
|
+
from chorusgraph.agents.policy import PlanPolicy, PlanSolveOpts
|
|
10
|
+
from chorusgraph.nodes.tool import ToolRegistry
|
|
11
|
+
|
|
12
|
+
__all__ = ["PlanStep", "PlanSolveResult", "plan_tasks", "run_plan_solve", "_try_compute_cross"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PlanSolveResult:
|
|
17
|
+
plan: List[PlanStep] = field(default_factory=list)
|
|
18
|
+
trace: List[Any] = field(default_factory=list)
|
|
19
|
+
observations: List[Any] = field(default_factory=list)
|
|
20
|
+
tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
|
21
|
+
failed_step: Optional[int] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_plan_solve(
|
|
25
|
+
*,
|
|
26
|
+
question: str,
|
|
27
|
+
registry: ToolRegistry,
|
|
28
|
+
llm_json: Callable[[str, str], str],
|
|
29
|
+
policy: Optional[PlanPolicy] = None,
|
|
30
|
+
pattern_opts: Optional[PlanSolveOpts] = None,
|
|
31
|
+
) -> PlanSolveResult:
|
|
32
|
+
from chorusgraph.agents.agent import Agent
|
|
33
|
+
|
|
34
|
+
policy = policy or PlanPolicy(max_steps=10)
|
|
35
|
+
opts = pattern_opts or PlanSolveOpts(max_plan_steps=policy.max_steps, on_step_failure="abort")
|
|
36
|
+
agent = Agent(
|
|
37
|
+
pattern="plan_solve",
|
|
38
|
+
tools=registry,
|
|
39
|
+
model=llm_json,
|
|
40
|
+
policy=policy,
|
|
41
|
+
pattern_opts=opts,
|
|
42
|
+
)
|
|
43
|
+
result = agent.run(question)
|
|
44
|
+
return PlanSolveResult(
|
|
45
|
+
plan=result.plan,
|
|
46
|
+
trace=result.trace,
|
|
47
|
+
observations=result.observations,
|
|
48
|
+
tool_calls=result.tool_calls,
|
|
49
|
+
failed_step=result.failed_step,
|
|
50
|
+
)
|