autonomous-agent-algos 0.1.0__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.
- aaa/__init__.py +10 -0
- aaa/algos/__init__.py +26 -0
- aaa/algos/base.py +78 -0
- aaa/algos/control.py +183 -0
- aaa/algos/ensemble.py +173 -0
- aaa/algos/loops.py +333 -0
- aaa/algos/plan.py +265 -0
- aaa/algos/refine.py +163 -0
- aaa/algos/route.py +93 -0
- aaa/context/__init__.py +30 -0
- aaa/context/policy.py +199 -0
- aaa/context/strategies.py +353 -0
- aaa/context/tokens.py +115 -0
- aaa/core/__init__.py +33 -0
- aaa/core/aio.py +50 -0
- aaa/core/errors.py +30 -0
- aaa/core/ids.py +32 -0
- aaa/core/outcome.py +30 -0
- aaa/core/retry.py +152 -0
- aaa/data/__init__.py +27 -0
- aaa/data/blob.py +87 -0
- aaa/data/capabilities.py +40 -0
- aaa/data/codec.py +102 -0
- aaa/data/index.py +115 -0
- aaa/data/item.py +41 -0
- aaa/events/__init__.py +33 -0
- aaa/events/bus.py +277 -0
- aaa/events/cloudevents.py +78 -0
- aaa/events/distributed.py +500 -0
- aaa/events/event.py +81 -0
- aaa/events/guard.py +67 -0
- aaa/events/postgres.py +428 -0
- aaa/events/projection.py +95 -0
- aaa/events/redis.py +361 -0
- aaa/events/schema.py +55 -0
- aaa/events/snapshot.py +24 -0
- aaa/events/sqlite.py +222 -0
- aaa/events/store.py +157 -0
- aaa/llm/__init__.py +45 -0
- aaa/llm/client.py +26 -0
- aaa/llm/config.py +39 -0
- aaa/llm/embedder.py +14 -0
- aaa/llm/errors.py +41 -0
- aaa/llm/factory.py +30 -0
- aaa/llm/fake.py +93 -0
- aaa/llm/gateways.py +75 -0
- aaa/llm/handler.py +257 -0
- aaa/llm/litellm_client.py +119 -0
- aaa/llm/middleware.py +113 -0
- aaa/llm/openai_compat.py +261 -0
- aaa/llm/streaming.py +36 -0
- aaa/llm/types.py +70 -0
- aaa/mcp/__init__.py +6 -0
- aaa/mcp/client.py +364 -0
- aaa/mcp/memory.py +125 -0
- aaa/memory/__init__.py +46 -0
- aaa/memory/base.py +96 -0
- aaa/memory/callable.py +152 -0
- aaa/memory/fake.py +116 -0
- aaa/memory/graphiti.py +180 -0
- aaa/memory/handler.py +226 -0
- aaa/memory/langgraph.py +179 -0
- aaa/memory/mem0.py +158 -0
- aaa/memory/native.py +177 -0
- aaa/memory/supermemory.py +264 -0
- aaa/memory/tools.py +76 -0
- aaa/memory/types.py +138 -0
- aaa/privacy.py +264 -0
- aaa/py.typed +0 -0
- aaa/runtime/__init__.py +18 -0
- aaa/runtime/budget.py +57 -0
- aaa/runtime/engine.py +834 -0
- aaa/runtime/recovery.py +136 -0
- aaa/runtime/retention.py +171 -0
- aaa/runtime/tenancy.py +106 -0
- aaa/tools/__init__.py +29 -0
- aaa/tools/guard.py +104 -0
- aaa/tools/handler.py +436 -0
- aaa/tools/rag.py +28 -0
- aaa/tools/tool.py +225 -0
- aaa/validate/__init__.py +79 -0
- autonomous_agent_algos-0.1.0.dist-info/METADATA +276 -0
- autonomous_agent_algos-0.1.0.dist-info/RECORD +85 -0
- autonomous_agent_algos-0.1.0.dist-info/WHEEL +4 -0
- autonomous_agent_algos-0.1.0.dist-info/licenses/LICENSE +21 -0
aaa/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""AutonomousAgentAlgos (AAA): event-driven agent algorithms over arbitrary data."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("autonomous-agent-algos")
|
|
7
|
+
except PackageNotFoundError: # running from a source tree that was never installed
|
|
8
|
+
__version__ = "0+unknown"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|
aaa/algos/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from aaa.algos.control import FSM, Node, WorkflowGraph
|
|
2
|
+
from aaa.algos.ensemble import RAGAgent, SelfConsistency, Supervisor
|
|
3
|
+
from aaa.algos.loops import LLMLoop, ReAct, RecallConfig, ToolLoop
|
|
4
|
+
from aaa.algos.plan import Plan, PlanAndExecute, Step
|
|
5
|
+
from aaa.algos.refine import GenerateTestRepair, GeneratorCritic
|
|
6
|
+
from aaa.algos.route import Route, Router
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FSM",
|
|
10
|
+
"GenerateTestRepair",
|
|
11
|
+
"GeneratorCritic",
|
|
12
|
+
"LLMLoop",
|
|
13
|
+
"Node",
|
|
14
|
+
"Plan",
|
|
15
|
+
"PlanAndExecute",
|
|
16
|
+
"RAGAgent",
|
|
17
|
+
"ReAct",
|
|
18
|
+
"RecallConfig",
|
|
19
|
+
"Route",
|
|
20
|
+
"Router",
|
|
21
|
+
"SelfConsistency",
|
|
22
|
+
"Step",
|
|
23
|
+
"Supervisor",
|
|
24
|
+
"ToolLoop",
|
|
25
|
+
"WorkflowGraph",
|
|
26
|
+
]
|
aaa/algos/base.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aaa.core.outcome import OutcomeKind
|
|
8
|
+
from aaa.events import Event
|
|
9
|
+
from aaa.llm.types import LLMRequest, Message
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def llm_request(
|
|
13
|
+
parent: Event, req: LLMRequest, tag: str = "main", *, prev: str | None = None
|
|
14
|
+
) -> Event:
|
|
15
|
+
"""Build an `aaa.llm.requested` event.
|
|
16
|
+
|
|
17
|
+
With `prev` (the id of the previous request event of the SAME conversation), `req.messages`
|
|
18
|
+
must hold only the messages appended since that request's reply; the event then stores just
|
|
19
|
+
that delta plus a pointer. The LLM handler reconstructs the full transcript the provider
|
|
20
|
+
needs, so the log (and algorithm state) grows linearly with turns, not quadratically."""
|
|
21
|
+
body = req.model_dump(mode="json")
|
|
22
|
+
data: dict[str, Any] = {"request": body, "tag": tag}
|
|
23
|
+
if prev is not None:
|
|
24
|
+
data["conv"] = {"prev": prev, "delta": body["messages"]}
|
|
25
|
+
body["messages"] = []
|
|
26
|
+
return parent.caused("aaa.llm.requested", data)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def finish(parent: Event, kind: OutcomeKind, value: Any = None, reason: str | None = None) -> Event:
|
|
30
|
+
return parent.caused(
|
|
31
|
+
"aaa.run.completed", {"kind": kind.value, "value": value, "reason": reason}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def tool_request(parent: Event, call_id: str, name: str, arguments: dict[str, Any]) -> Event:
|
|
36
|
+
return parent.caused(
|
|
37
|
+
"aaa.tool.requested", {"call_id": call_id, "name": name, "arguments": arguments}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_FENCE = re.compile(r"^```[a-zA-Z0-9_-]*\s*\n(.*?)\n```\s*$", re.DOTALL)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def strip_fence(text: str) -> str:
|
|
45
|
+
t = text.strip()
|
|
46
|
+
m = _FENCE.match(t)
|
|
47
|
+
return m.group(1) if m else t
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_json_obj(text: str | None) -> dict[str, Any] | None:
|
|
51
|
+
"""Best-effort: a JSON object from raw model text (fenced or surrounded by prose)."""
|
|
52
|
+
if not text:
|
|
53
|
+
return None
|
|
54
|
+
t = strip_fence(text)
|
|
55
|
+
for cand in (t, t[t.find("{") : t.rfind("}") + 1] if "{" in t else ""):
|
|
56
|
+
try:
|
|
57
|
+
v = json.loads(cand)
|
|
58
|
+
except ValueError:
|
|
59
|
+
continue
|
|
60
|
+
if isinstance(v, dict):
|
|
61
|
+
return v
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def render_result(payload: Any) -> str:
|
|
66
|
+
return payload if isinstance(payload, str) else json.dumps(payload, default=str)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def first_started(hist: list[Event]) -> Event:
|
|
70
|
+
return next(e for e in hist if e.type == "aaa.run.started")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def task_messages(inp: dict[str, Any], system: str) -> list[Message]:
|
|
74
|
+
if "messages" in inp:
|
|
75
|
+
return [Message.model_validate(m) for m in inp["messages"]]
|
|
76
|
+
msgs = [Message(role="system", content=system)] if system else []
|
|
77
|
+
msgs.append(Message(role="user", content=str(inp.get("task", ""))))
|
|
78
|
+
return msgs
|
aaa/algos/control.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from aaa.algos.base import finish, tool_request
|
|
10
|
+
from aaa.core.outcome import OutcomeKind
|
|
11
|
+
from aaa.events import Event, Projection
|
|
12
|
+
from aaa.runtime import Ctx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Node:
|
|
17
|
+
"""One workflow step = one tool call. `args(results, input)` builds its arguments from
|
|
18
|
+
upstream outputs; `when(results, input)` can skip it (skipped nodes yield None)."""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
tool: str
|
|
22
|
+
after: list[str] = field(default_factory=list)
|
|
23
|
+
args: Callable[[dict[str, Any], dict[str, Any]], dict[str, Any]] | dict[str, Any] | None = None
|
|
24
|
+
when: Callable[[dict[str, Any], dict[str, Any]], bool] | None = None
|
|
25
|
+
on_error: str = "fail" # "fail" | "continue"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _WF(BaseModel):
|
|
29
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
30
|
+
results: dict[str, Any] = Field(default_factory=dict)
|
|
31
|
+
errors: dict[str, str] = Field(
|
|
32
|
+
default_factory=dict
|
|
33
|
+
) # raw failures; on_error policy applied at read time
|
|
34
|
+
requested: list[str] = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _apply_wf(st: _WF, e: Event) -> _WF:
|
|
38
|
+
d = e.data
|
|
39
|
+
if e.type == "aaa.run.started":
|
|
40
|
+
st.input = d["input"]
|
|
41
|
+
elif e.type == "aaa.tool.requested":
|
|
42
|
+
st.requested.append(d["call_id"])
|
|
43
|
+
elif e.type == "aaa.node.skipped":
|
|
44
|
+
st.requested.append(d["node_id"])
|
|
45
|
+
st.results[d["node_id"]] = None
|
|
46
|
+
elif e.type == "aaa.tool.completed":
|
|
47
|
+
st.results[d["call_id"]] = d["result"]
|
|
48
|
+
elif e.type == "aaa.tool.failed":
|
|
49
|
+
st.errors[d["call_id"]] = f"{d['error']['type']}: {d['error']['message']}"
|
|
50
|
+
return st
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
WF = Projection(_WF, _apply_wf, name="aaa.workflow", version=1, state_type=_WF)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WorkflowGraph:
|
|
57
|
+
"""Deterministic DAG execution: no LLM decides control flow. Ready nodes run in parallel;
|
|
58
|
+
tools bring approval gates, timeouts, idempotency and budgets. Cyclic/stateful control
|
|
59
|
+
flow belongs to FSM. Output = `output` node id, a callable(results), or all results."""
|
|
60
|
+
|
|
61
|
+
name = "workflow"
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self, nodes: list[Node], *, output: str | Callable[[dict[str, Any]], Any] | None = None
|
|
65
|
+
) -> None:
|
|
66
|
+
ids = [n.id for n in nodes]
|
|
67
|
+
if len(set(ids)) != len(ids):
|
|
68
|
+
raise ValueError("duplicate node ids")
|
|
69
|
+
for n in nodes:
|
|
70
|
+
if missing := [a for a in n.after if a not in ids]:
|
|
71
|
+
raise ValueError(f"node {n.id} depends on unknown {missing}")
|
|
72
|
+
self.nodes = {n.id: n for n in nodes}
|
|
73
|
+
self._check_acyclic()
|
|
74
|
+
self.output = output
|
|
75
|
+
|
|
76
|
+
def _check_acyclic(self) -> None:
|
|
77
|
+
seen: set[str] = set()
|
|
78
|
+
todo = set(self.nodes)
|
|
79
|
+
while todo:
|
|
80
|
+
ready = {i for i in todo if all(a in seen for a in self.nodes[i].after)}
|
|
81
|
+
if not ready:
|
|
82
|
+
raise ValueError("workflow has a dependency cycle (use FSM for loops)")
|
|
83
|
+
seen |= ready
|
|
84
|
+
todo -= ready
|
|
85
|
+
|
|
86
|
+
def subscriptions(self) -> list[str]:
|
|
87
|
+
return ["aaa.run.started", "aaa.tool.completed", "aaa.tool.failed"]
|
|
88
|
+
|
|
89
|
+
async def on_event(self, e: Event, ctx: Ctx) -> list[Event]:
|
|
90
|
+
st = await ctx.project(WF)
|
|
91
|
+
inp, requested = st.input, set(st.requested)
|
|
92
|
+
results: dict[str, Any] = dict(st.results)
|
|
93
|
+
failed: dict[str, str] = {}
|
|
94
|
+
for k, msg in st.errors.items():
|
|
95
|
+
if self.nodes[k].on_error == "continue":
|
|
96
|
+
results[k] = None
|
|
97
|
+
else:
|
|
98
|
+
failed[k] = msg
|
|
99
|
+
if failed:
|
|
100
|
+
k, msg = next(iter(failed.items()))
|
|
101
|
+
return [finish(e, OutcomeKind.FAILED, reason=f"node {k} failed: {msg}")]
|
|
102
|
+
out: list[Event] = []
|
|
103
|
+
progress = True
|
|
104
|
+
while progress: # fixpoint: skipping a node can make its dependents ready
|
|
105
|
+
progress = False
|
|
106
|
+
for n in self.nodes.values():
|
|
107
|
+
if n.id in requested or not all(a in results for a in n.after):
|
|
108
|
+
continue
|
|
109
|
+
requested.add(n.id)
|
|
110
|
+
progress = True
|
|
111
|
+
if n.when and not n.when(results, inp):
|
|
112
|
+
results[n.id] = None
|
|
113
|
+
out.append(e.caused("aaa.node.skipped", {"node_id": n.id}))
|
|
114
|
+
continue
|
|
115
|
+
args = n.args(results, inp) if callable(n.args) else dict(n.args or {})
|
|
116
|
+
out.append(tool_request(e, n.id, n.tool, args))
|
|
117
|
+
if all(i in results for i in self.nodes):
|
|
118
|
+
val = (
|
|
119
|
+
results[self.output]
|
|
120
|
+
if isinstance(self.output, str)
|
|
121
|
+
else (self.output(results) if self.output else results)
|
|
122
|
+
)
|
|
123
|
+
return [*out, finish(e, OutcomeKind.SUCCESS, val)]
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _FSM(BaseModel):
|
|
128
|
+
input: dict[str, Any] = Field(default_factory=dict)
|
|
129
|
+
path: list[str] = Field(default_factory=list) # states entered after `initial`
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _apply_fsm(st: _FSM, e: Event) -> _FSM:
|
|
133
|
+
if e.type == "aaa.run.started":
|
|
134
|
+
st.input = e.data["input"]
|
|
135
|
+
elif e.type == "aaa.fsm.transitioned":
|
|
136
|
+
st.path.append(e.data["to"])
|
|
137
|
+
return st
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
FSM_STATE = Projection(_FSM, _apply_fsm, name="aaa.fsm", version=1, state_type=_FSM)
|
|
141
|
+
|
|
142
|
+
Transition = str | Callable[[Event, dict[str, Any]], str | None]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class FSM:
|
|
146
|
+
"""Explicit finite-state machine driven by external/internal events. `transitions[state]
|
|
147
|
+
[event_type]` is the next state or a function(event, input) -> next state | None (guard).
|
|
148
|
+
`on_enter[state](event, input)` may return events (request a tool, an LLM call, ...).
|
|
149
|
+
Reaching a terminal state completes the run with the path taken. The current state is a
|
|
150
|
+
fold of `aaa.fsm.transitioned` events, so it survives restarts."""
|
|
151
|
+
|
|
152
|
+
name = "fsm"
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
initial: str,
|
|
157
|
+
transitions: dict[str, dict[str, Transition]],
|
|
158
|
+
terminal: set[str],
|
|
159
|
+
on_enter: dict[str, Callable[[Event, dict[str, Any]], list[Event]]] | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
self.initial, self.transitions, self.terminal = initial, transitions, terminal
|
|
162
|
+
self.on_enter = on_enter or {}
|
|
163
|
+
self._types = sorted({t for m in transitions.values() for t in m})
|
|
164
|
+
|
|
165
|
+
def subscriptions(self) -> list[str]:
|
|
166
|
+
return ["aaa.run.started", *self._types]
|
|
167
|
+
|
|
168
|
+
async def on_event(self, e: Event, ctx: Ctx) -> list[Event]:
|
|
169
|
+
st = await ctx.project(FSM_STATE)
|
|
170
|
+
inp = st.input
|
|
171
|
+
path = [self.initial, *st.path]
|
|
172
|
+
cur = path[-1]
|
|
173
|
+
if e.type == "aaa.run.started":
|
|
174
|
+
return [*self.on_enter.get(cur, lambda *_: [])(e, inp)]
|
|
175
|
+
rule = self.transitions.get(cur, {}).get(e.type)
|
|
176
|
+
nxt = rule(e, inp) if callable(rule) else rule
|
|
177
|
+
if nxt is None:
|
|
178
|
+
return [e.caused("aaa.fsm.ignored", {"state": cur, "event": e.type})]
|
|
179
|
+
out = [e.caused("aaa.fsm.transitioned", {"from": cur, "to": nxt, "on": e.type})]
|
|
180
|
+
out += self.on_enter.get(nxt, lambda *_: [])(e, inp)
|
|
181
|
+
if nxt in self.terminal:
|
|
182
|
+
out.append(finish(e, OutcomeKind.SUCCESS, {"state": nxt, "path": [*path, nxt]}))
|
|
183
|
+
return out
|
aaa/algos/ensemble.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from aaa.algos.base import finish, llm_request
|
|
11
|
+
from aaa.algos.loops import ToolLoop
|
|
12
|
+
from aaa.core.outcome import OutcomeKind
|
|
13
|
+
from aaa.data.index import Index
|
|
14
|
+
from aaa.events import Event, Projection
|
|
15
|
+
from aaa.llm.types import LLMRequest, LLMResponse, Message
|
|
16
|
+
from aaa.runtime import Algorithm, Ctx
|
|
17
|
+
from aaa.tools import Tool, ToolContext, ToolError, ToolRegistry, search_tool
|
|
18
|
+
|
|
19
|
+
_ANS = re.compile(r"Answer:\s*(.+)", re.IGNORECASE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_extract(text: str) -> str:
|
|
23
|
+
m = _ANS.findall(text)
|
|
24
|
+
return (m[-1] if m else (text.strip().splitlines() or [""])[-1]).strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _norm(s: str) -> str:
|
|
28
|
+
return re.sub(r"[^\w\s.-]", "", s.lower()).strip().rstrip(".")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Samples(BaseModel):
|
|
32
|
+
texts: list[str] = Field(
|
|
33
|
+
default_factory=list
|
|
34
|
+
) # raw model text; `extract` is applied at read time
|
|
35
|
+
failures: int = 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _apply_sample(st: _Samples, e: Event) -> _Samples:
|
|
39
|
+
if e.type == "aaa.llm.completed":
|
|
40
|
+
st.texts.append(LLMResponse.model_validate(e.data["response"]).message.content or "")
|
|
41
|
+
elif e.type == "aaa.llm.failed":
|
|
42
|
+
st.failures += 1
|
|
43
|
+
return st
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
SAMPLES = Projection(_Samples, _apply_sample, name="aaa.samples", version=1, state_type=_Samples)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SelfConsistency:
|
|
50
|
+
"""Sample N independent answers in parallel and return the majority (ties -> earliest).
|
|
51
|
+
Only worthwhile when answers can be compared exactly (math, classification, extraction)."""
|
|
52
|
+
|
|
53
|
+
name = "self_consistency"
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
n: int = 5,
|
|
58
|
+
*,
|
|
59
|
+
temperature: float = 0.8,
|
|
60
|
+
model: str | None = None,
|
|
61
|
+
system: str = "Think step by step, then end with a line 'Answer: <final answer>'.",
|
|
62
|
+
extract: Callable[[str], str] = default_extract,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.n, self.temperature, self.model, self.system, self.extract = (
|
|
65
|
+
n,
|
|
66
|
+
temperature,
|
|
67
|
+
model,
|
|
68
|
+
system,
|
|
69
|
+
extract,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def subscriptions(self) -> list[str]:
|
|
73
|
+
return ["aaa.run.started", "aaa.llm.completed", "aaa.llm.failed"]
|
|
74
|
+
|
|
75
|
+
async def on_event(self, e: Event, ctx: Ctx) -> list[Event]:
|
|
76
|
+
if e.type == "aaa.run.started":
|
|
77
|
+
task = e.data["input"].get("task", "")
|
|
78
|
+
req = LLMRequest(
|
|
79
|
+
model=self.model,
|
|
80
|
+
params={"temperature": self.temperature},
|
|
81
|
+
messages=[
|
|
82
|
+
Message(role="system", content=self.system),
|
|
83
|
+
Message(role="user", content=str(task)),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
return [llm_request(e, req, f"s{i}") for i in range(self.n)]
|
|
87
|
+
st = await ctx.project(SAMPLES)
|
|
88
|
+
samples = [self.extract(t) for t in st.texts]
|
|
89
|
+
failures = st.failures
|
|
90
|
+
if len(samples) + failures < self.n:
|
|
91
|
+
return []
|
|
92
|
+
if not samples:
|
|
93
|
+
return [finish(e, OutcomeKind.FAILED, reason="all samples failed")]
|
|
94
|
+
votes = Counter(_norm(s) for s in samples)
|
|
95
|
+
best, count = max(
|
|
96
|
+
votes.items(), key=lambda kv: (kv[1], -[_norm(s) for s in samples].index(kv[0]))
|
|
97
|
+
)
|
|
98
|
+
answer = next(s for s in samples if _norm(s) == best)
|
|
99
|
+
return [
|
|
100
|
+
finish(
|
|
101
|
+
e,
|
|
102
|
+
OutcomeKind.SUCCESS,
|
|
103
|
+
{
|
|
104
|
+
"answer": answer,
|
|
105
|
+
"votes": count,
|
|
106
|
+
"samples": len(samples),
|
|
107
|
+
"agreement": round(count / len(samples), 3),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RAGAgent(ToolLoop):
|
|
114
|
+
"""Agentic RAG: a ToolLoop whose tool is retrieval over any `Index`. The model decides
|
|
115
|
+
when/what to retrieve and is told to ground and cite. Attach `ToolHandler(rag.tools)`."""
|
|
116
|
+
|
|
117
|
+
name = "rag_agent"
|
|
118
|
+
_PROMPT = (
|
|
119
|
+
"Answer using only evidence retrieved with the search tool. Cite passage ids like [d1]. "
|
|
120
|
+
"If the evidence is insufficient, say so instead of guessing."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def __init__(self, index: Index, *, k: int = 5, **kw: Any) -> None:
|
|
124
|
+
kw.setdefault("system", self._PROMPT)
|
|
125
|
+
super().__init__(ToolRegistry([search_tool(index, k=k)]), **kw)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Supervisor(ToolLoop):
|
|
129
|
+
"""Centralized coordinator: an LLM delegates sub-tasks to named worker algorithms (each a
|
|
130
|
+
child run: own budget accounting, cancelled with the parent). Parallelism is bounded by the
|
|
131
|
+
'delegate' pool. Attach `ToolHandler(supervisor.tools)`."""
|
|
132
|
+
|
|
133
|
+
name = "supervisor"
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
workers: dict[str, tuple[Algorithm, str]],
|
|
138
|
+
*,
|
|
139
|
+
extra_tools: list[Tool] | None = None,
|
|
140
|
+
**kw: Any,
|
|
141
|
+
) -> None:
|
|
142
|
+
reg = ToolRegistry(extra_tools or [])
|
|
143
|
+
for wname, (algo, desc) in workers.items():
|
|
144
|
+
reg.add(self._delegate_tool(wname, algo, desc))
|
|
145
|
+
roster = "\n".join(f"- delegate_{k}: {d}" for k, (_, d) in workers.items())
|
|
146
|
+
kw.setdefault(
|
|
147
|
+
"system",
|
|
148
|
+
f"You coordinate specialist workers. Delegate sub-tasks (possibly in parallel), then combine their results.\n{roster}",
|
|
149
|
+
)
|
|
150
|
+
super().__init__(reg, **kw)
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _delegate_tool(wname: str, algo: Algorithm, desc: str) -> Tool:
|
|
154
|
+
async def delegate(task: str, ctx: ToolContext) -> Any:
|
|
155
|
+
handle = await ctx.engine.start(
|
|
156
|
+
algo, {"task": task}, parent=ctx.run_id, tag=ctx.call_id
|
|
157
|
+
)
|
|
158
|
+
out = await handle.result()
|
|
159
|
+
if not out.ok:
|
|
160
|
+
raise ToolError(f"worker {wname} ended {out.kind.value}: {out.reason}")
|
|
161
|
+
return (
|
|
162
|
+
out.value["text"]
|
|
163
|
+
if isinstance(out.value, dict) and "text" in out.value
|
|
164
|
+
else out.value
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return Tool(
|
|
168
|
+
delegate,
|
|
169
|
+
name=f"delegate_{wname}",
|
|
170
|
+
description=f"Delegate a self-contained sub-task. {desc}",
|
|
171
|
+
pool="delegate",
|
|
172
|
+
timeout_s=600,
|
|
173
|
+
)
|