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.
Files changed (85) hide show
  1. aaa/__init__.py +10 -0
  2. aaa/algos/__init__.py +26 -0
  3. aaa/algos/base.py +78 -0
  4. aaa/algos/control.py +183 -0
  5. aaa/algos/ensemble.py +173 -0
  6. aaa/algos/loops.py +333 -0
  7. aaa/algos/plan.py +265 -0
  8. aaa/algos/refine.py +163 -0
  9. aaa/algos/route.py +93 -0
  10. aaa/context/__init__.py +30 -0
  11. aaa/context/policy.py +199 -0
  12. aaa/context/strategies.py +353 -0
  13. aaa/context/tokens.py +115 -0
  14. aaa/core/__init__.py +33 -0
  15. aaa/core/aio.py +50 -0
  16. aaa/core/errors.py +30 -0
  17. aaa/core/ids.py +32 -0
  18. aaa/core/outcome.py +30 -0
  19. aaa/core/retry.py +152 -0
  20. aaa/data/__init__.py +27 -0
  21. aaa/data/blob.py +87 -0
  22. aaa/data/capabilities.py +40 -0
  23. aaa/data/codec.py +102 -0
  24. aaa/data/index.py +115 -0
  25. aaa/data/item.py +41 -0
  26. aaa/events/__init__.py +33 -0
  27. aaa/events/bus.py +277 -0
  28. aaa/events/cloudevents.py +78 -0
  29. aaa/events/distributed.py +500 -0
  30. aaa/events/event.py +81 -0
  31. aaa/events/guard.py +67 -0
  32. aaa/events/postgres.py +428 -0
  33. aaa/events/projection.py +95 -0
  34. aaa/events/redis.py +361 -0
  35. aaa/events/schema.py +55 -0
  36. aaa/events/snapshot.py +24 -0
  37. aaa/events/sqlite.py +222 -0
  38. aaa/events/store.py +157 -0
  39. aaa/llm/__init__.py +45 -0
  40. aaa/llm/client.py +26 -0
  41. aaa/llm/config.py +39 -0
  42. aaa/llm/embedder.py +14 -0
  43. aaa/llm/errors.py +41 -0
  44. aaa/llm/factory.py +30 -0
  45. aaa/llm/fake.py +93 -0
  46. aaa/llm/gateways.py +75 -0
  47. aaa/llm/handler.py +257 -0
  48. aaa/llm/litellm_client.py +119 -0
  49. aaa/llm/middleware.py +113 -0
  50. aaa/llm/openai_compat.py +261 -0
  51. aaa/llm/streaming.py +36 -0
  52. aaa/llm/types.py +70 -0
  53. aaa/mcp/__init__.py +6 -0
  54. aaa/mcp/client.py +364 -0
  55. aaa/mcp/memory.py +125 -0
  56. aaa/memory/__init__.py +46 -0
  57. aaa/memory/base.py +96 -0
  58. aaa/memory/callable.py +152 -0
  59. aaa/memory/fake.py +116 -0
  60. aaa/memory/graphiti.py +180 -0
  61. aaa/memory/handler.py +226 -0
  62. aaa/memory/langgraph.py +179 -0
  63. aaa/memory/mem0.py +158 -0
  64. aaa/memory/native.py +177 -0
  65. aaa/memory/supermemory.py +264 -0
  66. aaa/memory/tools.py +76 -0
  67. aaa/memory/types.py +138 -0
  68. aaa/privacy.py +264 -0
  69. aaa/py.typed +0 -0
  70. aaa/runtime/__init__.py +18 -0
  71. aaa/runtime/budget.py +57 -0
  72. aaa/runtime/engine.py +834 -0
  73. aaa/runtime/recovery.py +136 -0
  74. aaa/runtime/retention.py +171 -0
  75. aaa/runtime/tenancy.py +106 -0
  76. aaa/tools/__init__.py +29 -0
  77. aaa/tools/guard.py +104 -0
  78. aaa/tools/handler.py +436 -0
  79. aaa/tools/rag.py +28 -0
  80. aaa/tools/tool.py +225 -0
  81. aaa/validate/__init__.py +79 -0
  82. autonomous_agent_algos-0.1.0.dist-info/METADATA +276 -0
  83. autonomous_agent_algos-0.1.0.dist-info/RECORD +85 -0
  84. autonomous_agent_algos-0.1.0.dist-info/WHEEL +4 -0
  85. 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
+ )