agentsdk-py 0.1.0__tar.gz
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.
- agentsdk_py-0.1.0/PKG-INFO +146 -0
- agentsdk_py-0.1.0/README.md +116 -0
- agentsdk_py-0.1.0/agentsdk/__init__.py +41 -0
- agentsdk_py-0.1.0/agentsdk/agent.py +321 -0
- agentsdk_py-0.1.0/agentsdk/cli.py +427 -0
- agentsdk_py-0.1.0/agentsdk/exceptions.py +54 -0
- agentsdk_py-0.1.0/agentsdk/graph/__init__.py +31 -0
- agentsdk_py-0.1.0/agentsdk/graph/bus.py +407 -0
- agentsdk_py-0.1.0/agentsdk/graph/graph.py +141 -0
- agentsdk_py-0.1.0/agentsdk/graph/node.py +121 -0
- agentsdk_py-0.1.0/agentsdk/graph/runner.py +140 -0
- agentsdk_py-0.1.0/agentsdk/llm.py +230 -0
- agentsdk_py-0.1.0/agentsdk/messages.py +289 -0
- agentsdk_py-0.1.0/agentsdk/observability/__init__.py +28 -0
- agentsdk_py-0.1.0/agentsdk/observability/middleware.py +295 -0
- agentsdk_py-0.1.0/agentsdk/observability/tracer.py +169 -0
- agentsdk_py-0.1.0/agentsdk/persistence/__init__.py +26 -0
- agentsdk_py-0.1.0/agentsdk/persistence/checkpoint.py +115 -0
- agentsdk_py-0.1.0/agentsdk/persistence/file_store.py +86 -0
- agentsdk_py-0.1.0/agentsdk/persistence/session.py +168 -0
- agentsdk_py-0.1.0/agentsdk/tools/__init__.py +20 -0
- agentsdk_py-0.1.0/agentsdk/tools/base.py +209 -0
- agentsdk_py-0.1.0/agentsdk/tools/builtin.py +158 -0
- agentsdk_py-0.1.0/agentsdk/tools/registry.py +85 -0
- agentsdk_py-0.1.0/agentsdk_py.egg-info/PKG-INFO +146 -0
- agentsdk_py-0.1.0/agentsdk_py.egg-info/SOURCES.txt +31 -0
- agentsdk_py-0.1.0/agentsdk_py.egg-info/dependency_links.txt +1 -0
- agentsdk_py-0.1.0/agentsdk_py.egg-info/entry_points.txt +2 -0
- agentsdk_py-0.1.0/agentsdk_py.egg-info/requires.txt +15 -0
- agentsdk_py-0.1.0/agentsdk_py.egg-info/top_level.txt +1 -0
- agentsdk_py-0.1.0/pyproject.toml +54 -0
- agentsdk_py-0.1.0/setup.cfg +4 -0
- agentsdk_py-0.1.0/tests/test_smoke.py +221 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentsdk-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python SDK for building AI agents with tools, memory, and multi-agent pipelines — powered by Groq
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/vishwa0198/agentsdk
|
|
7
|
+
Project-URL: Documentation, https://vishwa0198.github.io/agentsdk
|
|
8
|
+
Project-URL: Repository, https://github.com/vishwa0198/agentsdk
|
|
9
|
+
Project-URL: Changelog, https://github.com/vishwa0198/agentsdk/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: ai,agents,llm,groq,sdk,multi-agent,react-agent,tool-use
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: pydantic>=2.0
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: aiofiles>=23.0
|
|
20
|
+
Requires-Dist: groq>=0.9
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Provides-Extra: otel
|
|
24
|
+
Requires-Dist: opentelemetry-api>=1.24; extra == "otel"
|
|
25
|
+
Requires-Dist: opentelemetry-sdk>=1.24; extra == "otel"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# agentsdk
|
|
32
|
+
|
|
33
|
+
A lightweight Python SDK for building AI agents with tool use, multi-agent graphs, persistence, and tracing.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install agentsdk # core
|
|
39
|
+
pip install agentsdk[otel] # + OpenTelemetry tracing
|
|
40
|
+
pip install agentsdk[dev] # + pytest / dotenv
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio, os
|
|
47
|
+
from agentsdk import Agent, AgentConfig, GroqProvider
|
|
48
|
+
|
|
49
|
+
agent = Agent(
|
|
50
|
+
config=AgentConfig(
|
|
51
|
+
name="MyAgent",
|
|
52
|
+
system_prompt="You are a helpful assistant.",
|
|
53
|
+
),
|
|
54
|
+
llm=GroqProvider(api_key=os.environ["GROQ_API_KEY"]),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
async def main():
|
|
58
|
+
result = await agent.run("What is the capital of France?")
|
|
59
|
+
print(result.output)
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Tools
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from agentsdk import tool, ToolRegistry, Agent, AgentConfig, GroqProvider
|
|
68
|
+
|
|
69
|
+
@tool
|
|
70
|
+
async def add(a: int, b: int) -> str:
|
|
71
|
+
"""Add two integers."""
|
|
72
|
+
return str(a + b)
|
|
73
|
+
|
|
74
|
+
registry = ToolRegistry()
|
|
75
|
+
registry.register(add)
|
|
76
|
+
|
|
77
|
+
agent = Agent(config=AgentConfig(name="Calc", system_prompt="Use tools."),
|
|
78
|
+
llm=GroqProvider(...), registry=registry)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Multi-agent Graph
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from agentsdk import AgentNode, Edge, AgentGraph, GraphRunner
|
|
85
|
+
|
|
86
|
+
graph = AgentGraph()
|
|
87
|
+
graph.add_node(AgentNode("researcher", researcher_agent))
|
|
88
|
+
graph.add_node(AgentNode("writer", writer_agent))
|
|
89
|
+
graph.add_edge(Edge("researcher", "writer", data_map={"output": "input"}))
|
|
90
|
+
graph.set_entry("researcher"); graph.set_exit("writer")
|
|
91
|
+
result = await GraphRunner(graph).run({"input": "Explain black holes"})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Persistence
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from agentsdk import FileCheckpointStore, SessionManager, Agent
|
|
98
|
+
|
|
99
|
+
store = FileCheckpointStore(base_dir=".agentsdk/checkpoints")
|
|
100
|
+
session_mgr = SessionManager(store=store, agent_name="MyAgent")
|
|
101
|
+
agent = Agent(config=..., llm=..., session_manager=session_mgr)
|
|
102
|
+
|
|
103
|
+
# History is saved and reloaded automatically across runs:
|
|
104
|
+
await agent.run("My favourite language is Python.", session_id="user-001")
|
|
105
|
+
await agent.run("What language did I mention?", session_id="user-001")
|
|
106
|
+
|
|
107
|
+
# Fork a session to branch an agent run:
|
|
108
|
+
forked = await session_mgr.fork("user-001", "user-001-branch")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Tracing (requires `agentsdk[otel]`)
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from agentsdk.observability import SDKTracer, TracedLLMProvider, TracedAgent, print_trace
|
|
115
|
+
|
|
116
|
+
tracer = SDKTracer(service_name="myapp")
|
|
117
|
+
traced_llm = TracedLLMProvider(provider=GroqProvider(...), tracer=tracer)
|
|
118
|
+
agent = TracedAgent(config=..., llm=traced_llm, tracer=tracer)
|
|
119
|
+
|
|
120
|
+
result, ctx = await agent.run("Summarise the last quarter earnings.")
|
|
121
|
+
print_trace(ctx)
|
|
122
|
+
# ╔══ Trace: MyAgent ══════════════════════
|
|
123
|
+
# ║ Session : (none)
|
|
124
|
+
# ║ Trace ID : 6744d6eca33853c5bba0...
|
|
125
|
+
# ║ Duration : 3680ms
|
|
126
|
+
# ║ LLM calls : 2
|
|
127
|
+
# ║ Tool calls : 1
|
|
128
|
+
# ║ Tokens : 1292 in / 36 out
|
|
129
|
+
# ╚═════════════════════════════════════════
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## CLI
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Scaffold a new agent project
|
|
136
|
+
scaffold-agent new myproject
|
|
137
|
+
|
|
138
|
+
# Interactive REPL against any agent file
|
|
139
|
+
scaffold-agent run myproject/agents/main.py
|
|
140
|
+
|
|
141
|
+
# Inspect a saved checkpoint
|
|
142
|
+
scaffold-agent trace .agentsdk/checkpoints/MyAgent/user-001.json
|
|
143
|
+
|
|
144
|
+
# List all sessions for an agent
|
|
145
|
+
scaffold-agent list-sessions MyAgent
|
|
146
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# agentsdk
|
|
2
|
+
|
|
3
|
+
A lightweight Python SDK for building AI agents with tool use, multi-agent graphs, persistence, and tracing.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install agentsdk # core
|
|
9
|
+
pip install agentsdk[otel] # + OpenTelemetry tracing
|
|
10
|
+
pip install agentsdk[dev] # + pytest / dotenv
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio, os
|
|
17
|
+
from agentsdk import Agent, AgentConfig, GroqProvider
|
|
18
|
+
|
|
19
|
+
agent = Agent(
|
|
20
|
+
config=AgentConfig(
|
|
21
|
+
name="MyAgent",
|
|
22
|
+
system_prompt="You are a helpful assistant.",
|
|
23
|
+
),
|
|
24
|
+
llm=GroqProvider(api_key=os.environ["GROQ_API_KEY"]),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
result = await agent.run("What is the capital of France?")
|
|
29
|
+
print(result.output)
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from agentsdk import tool, ToolRegistry, Agent, AgentConfig, GroqProvider
|
|
38
|
+
|
|
39
|
+
@tool
|
|
40
|
+
async def add(a: int, b: int) -> str:
|
|
41
|
+
"""Add two integers."""
|
|
42
|
+
return str(a + b)
|
|
43
|
+
|
|
44
|
+
registry = ToolRegistry()
|
|
45
|
+
registry.register(add)
|
|
46
|
+
|
|
47
|
+
agent = Agent(config=AgentConfig(name="Calc", system_prompt="Use tools."),
|
|
48
|
+
llm=GroqProvider(...), registry=registry)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Multi-agent Graph
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from agentsdk import AgentNode, Edge, AgentGraph, GraphRunner
|
|
55
|
+
|
|
56
|
+
graph = AgentGraph()
|
|
57
|
+
graph.add_node(AgentNode("researcher", researcher_agent))
|
|
58
|
+
graph.add_node(AgentNode("writer", writer_agent))
|
|
59
|
+
graph.add_edge(Edge("researcher", "writer", data_map={"output": "input"}))
|
|
60
|
+
graph.set_entry("researcher"); graph.set_exit("writer")
|
|
61
|
+
result = await GraphRunner(graph).run({"input": "Explain black holes"})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Persistence
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from agentsdk import FileCheckpointStore, SessionManager, Agent
|
|
68
|
+
|
|
69
|
+
store = FileCheckpointStore(base_dir=".agentsdk/checkpoints")
|
|
70
|
+
session_mgr = SessionManager(store=store, agent_name="MyAgent")
|
|
71
|
+
agent = Agent(config=..., llm=..., session_manager=session_mgr)
|
|
72
|
+
|
|
73
|
+
# History is saved and reloaded automatically across runs:
|
|
74
|
+
await agent.run("My favourite language is Python.", session_id="user-001")
|
|
75
|
+
await agent.run("What language did I mention?", session_id="user-001")
|
|
76
|
+
|
|
77
|
+
# Fork a session to branch an agent run:
|
|
78
|
+
forked = await session_mgr.fork("user-001", "user-001-branch")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tracing (requires `agentsdk[otel]`)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from agentsdk.observability import SDKTracer, TracedLLMProvider, TracedAgent, print_trace
|
|
85
|
+
|
|
86
|
+
tracer = SDKTracer(service_name="myapp")
|
|
87
|
+
traced_llm = TracedLLMProvider(provider=GroqProvider(...), tracer=tracer)
|
|
88
|
+
agent = TracedAgent(config=..., llm=traced_llm, tracer=tracer)
|
|
89
|
+
|
|
90
|
+
result, ctx = await agent.run("Summarise the last quarter earnings.")
|
|
91
|
+
print_trace(ctx)
|
|
92
|
+
# ╔══ Trace: MyAgent ══════════════════════
|
|
93
|
+
# ║ Session : (none)
|
|
94
|
+
# ║ Trace ID : 6744d6eca33853c5bba0...
|
|
95
|
+
# ║ Duration : 3680ms
|
|
96
|
+
# ║ LLM calls : 2
|
|
97
|
+
# ║ Tool calls : 1
|
|
98
|
+
# ║ Tokens : 1292 in / 36 out
|
|
99
|
+
# ╚═════════════════════════════════════════
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## CLI
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Scaffold a new agent project
|
|
106
|
+
scaffold-agent new myproject
|
|
107
|
+
|
|
108
|
+
# Interactive REPL against any agent file
|
|
109
|
+
scaffold-agent run myproject/agents/main.py
|
|
110
|
+
|
|
111
|
+
# Inspect a saved checkpoint
|
|
112
|
+
scaffold-agent trace .agentsdk/checkpoints/MyAgent/user-001.json
|
|
113
|
+
|
|
114
|
+
# List all sessions for an agent
|
|
115
|
+
scaffold-agent list-sessions MyAgent
|
|
116
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""agentsdk — A lightweight Python SDK for building AI agents."""
|
|
2
|
+
|
|
3
|
+
from agentsdk.agent import Agent, AgentConfig, AgentResult
|
|
4
|
+
from agentsdk.messages import (
|
|
5
|
+
MessageHistory,
|
|
6
|
+
HumanMessage,
|
|
7
|
+
AIMessage,
|
|
8
|
+
SystemMessage,
|
|
9
|
+
ToolResultMessage,
|
|
10
|
+
)
|
|
11
|
+
from agentsdk.llm import GroqProvider, LLMResponse
|
|
12
|
+
from agentsdk.tools.base import tool, BaseTool
|
|
13
|
+
from agentsdk.tools.registry import ToolRegistry
|
|
14
|
+
from agentsdk.tools.builtin import DEFAULT_TOOLS
|
|
15
|
+
from agentsdk.graph.node import AgentNode, Edge
|
|
16
|
+
from agentsdk.graph.graph import AgentGraph
|
|
17
|
+
from agentsdk.graph.runner import GraphRunner
|
|
18
|
+
from agentsdk.graph.bus import MessageBus, BusAwareAgent, BusRunner
|
|
19
|
+
from agentsdk.persistence.file_store import FileCheckpointStore
|
|
20
|
+
from agentsdk.persistence.session import SessionManager
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Core agent
|
|
26
|
+
"Agent", "AgentConfig", "AgentResult",
|
|
27
|
+
# Messages
|
|
28
|
+
"MessageHistory", "HumanMessage", "AIMessage", "SystemMessage", "ToolResultMessage",
|
|
29
|
+
# LLM
|
|
30
|
+
"GroqProvider", "LLMResponse",
|
|
31
|
+
# Tools
|
|
32
|
+
"tool", "BaseTool", "ToolRegistry", "DEFAULT_TOOLS",
|
|
33
|
+
# Graph
|
|
34
|
+
"AgentNode", "Edge", "AgentGraph", "GraphRunner",
|
|
35
|
+
# Bus
|
|
36
|
+
"MessageBus", "BusAwareAgent", "BusRunner",
|
|
37
|
+
# Persistence
|
|
38
|
+
"FileCheckpointStore", "SessionManager",
|
|
39
|
+
# Meta
|
|
40
|
+
"__version__",
|
|
41
|
+
]
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
from agentsdk.llm import LLMProvider, ToolSchema
|
|
5
|
+
from agentsdk.messages import AIMessage, Memory, MessageHistory, ToolCall
|
|
6
|
+
from agentsdk.tools.base import BaseTool
|
|
7
|
+
from agentsdk.tools.registry import ToolRegistry
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from agentsdk.persistence.session import SessionManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# AgentConfig
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentConfig(BaseModel):
|
|
19
|
+
"""Immutable configuration for an Agent instance.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: Human-readable agent name used in verbose output and tracing.
|
|
23
|
+
system_prompt: Injected as the first SystemMessage for every new session.
|
|
24
|
+
max_iterations: Hard stop preventing infinite tool-call loops. Default 10.
|
|
25
|
+
max_tokens: Forwarded to LLMProvider.complete() on every call. Default 1024.
|
|
26
|
+
tools_enabled: When False, tool schemas are withheld even if registered.
|
|
27
|
+
verbose: Print each iteration's thought and tool calls to stdout.
|
|
28
|
+
|
|
29
|
+
Example::
|
|
30
|
+
|
|
31
|
+
config = AgentConfig(
|
|
32
|
+
name="MyAgent",
|
|
33
|
+
system_prompt="You are a helpful assistant.",
|
|
34
|
+
max_iterations=5,
|
|
35
|
+
verbose=True,
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(frozen=True)
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
"""Human-readable agent name (used in verbose output and tracing)."""
|
|
43
|
+
|
|
44
|
+
system_prompt: str
|
|
45
|
+
"""Injected as the first SystemMessage for every new session."""
|
|
46
|
+
|
|
47
|
+
max_iterations: int = 10
|
|
48
|
+
"""Hard stop — prevents infinite tool-call loops."""
|
|
49
|
+
|
|
50
|
+
max_tokens: int = 1024
|
|
51
|
+
"""Forwarded to LLMProvider.complete() on every call."""
|
|
52
|
+
|
|
53
|
+
tools_enabled: bool = True
|
|
54
|
+
"""When False, tool schemas are withheld from the LLM even if tools are registered."""
|
|
55
|
+
|
|
56
|
+
verbose: bool = False
|
|
57
|
+
"""Print each iteration's thought and tool calls to stdout (dev convenience)."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# StepResult
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StepResult(BaseModel):
|
|
66
|
+
"""Snapshot of one ReAct iteration."""
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(frozen=True)
|
|
69
|
+
|
|
70
|
+
iteration: int
|
|
71
|
+
thought: str
|
|
72
|
+
"""Raw content of the AIMessage produced this step."""
|
|
73
|
+
|
|
74
|
+
tool_calls: list[ToolCall]
|
|
75
|
+
"""Tool calls the model requested this step (empty if none)."""
|
|
76
|
+
|
|
77
|
+
stop_reason: str
|
|
78
|
+
"""Forwarded from LLMResponse.stop_reason."""
|
|
79
|
+
|
|
80
|
+
is_final: bool
|
|
81
|
+
"""True when this step caused the loop to terminate cleanly."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# AgentResult
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AgentResult(BaseModel):
|
|
90
|
+
"""Full output of a single Agent.run() invocation."""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(frozen=True)
|
|
93
|
+
|
|
94
|
+
output: str
|
|
95
|
+
"""Final assistant message content."""
|
|
96
|
+
|
|
97
|
+
steps: list[StepResult]
|
|
98
|
+
"""Ordered trace of every ReAct iteration."""
|
|
99
|
+
|
|
100
|
+
total_input_tokens: int
|
|
101
|
+
total_output_tokens: int
|
|
102
|
+
|
|
103
|
+
stopped_by: str
|
|
104
|
+
"""Why the loop ended: ``end_turn`` | ``max_iterations`` | ``error``."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Agent
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Agent:
|
|
113
|
+
"""Runs a ReAct agent loop using an LLM provider and optional tools.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
config: AgentConfig with name, system prompt, and settings.
|
|
117
|
+
llm: Any LLMProvider implementation — use GroqProvider.
|
|
118
|
+
tools: Optional flat list of BaseTool instances.
|
|
119
|
+
memory: Optional Memory backend (legacy; prefer session_manager).
|
|
120
|
+
registry: Optional ToolRegistry — merged with tools if both provided.
|
|
121
|
+
session_manager: Optional SessionManager for persistent sessions.
|
|
122
|
+
|
|
123
|
+
Example::
|
|
124
|
+
|
|
125
|
+
llm = GroqProvider(api_key="...")
|
|
126
|
+
config = AgentConfig(name="MyAgent", system_prompt="You are helpful.")
|
|
127
|
+
agent = Agent(config=config, llm=llm)
|
|
128
|
+
result = await agent.run("What is 2 + 2?")
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
config: AgentConfig,
|
|
134
|
+
llm: LLMProvider,
|
|
135
|
+
tools: list[BaseTool] | None = None,
|
|
136
|
+
memory: Memory | None = None,
|
|
137
|
+
registry: ToolRegistry | None = None,
|
|
138
|
+
session_manager: SessionManager | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
self.config = config
|
|
141
|
+
self._llm = llm
|
|
142
|
+
self._memory = memory
|
|
143
|
+
self._session_manager = session_manager
|
|
144
|
+
self._tools: list[BaseTool] = list(tools or [])
|
|
145
|
+
if registry is not None:
|
|
146
|
+
self._tools.extend(registry.all())
|
|
147
|
+
# O(1) lookup by tool name during dispatch
|
|
148
|
+
self._tool_map: dict[str, BaseTool] = {t.schema.name: t for t in self._tools}
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Core run loop
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
async def run(
|
|
155
|
+
self,
|
|
156
|
+
user_input: str,
|
|
157
|
+
session_id: str | None = None,
|
|
158
|
+
) -> AgentResult:
|
|
159
|
+
"""Execute the ReAct loop for *user_input* and return a full trace.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
user_input:
|
|
164
|
+
The human message that starts this turn.
|
|
165
|
+
session_id:
|
|
166
|
+
When provided alongside a ``Memory`` backend, the conversation
|
|
167
|
+
is loaded before running and persisted afterwards.
|
|
168
|
+
"""
|
|
169
|
+
# ── 1. Load or create history ──────────────────────────────────────
|
|
170
|
+
if self._session_manager and session_id:
|
|
171
|
+
history = await self._session_manager.load_history(session_id)
|
|
172
|
+
elif self._memory and session_id:
|
|
173
|
+
history = await self._memory.load(session_id)
|
|
174
|
+
else:
|
|
175
|
+
history = MessageHistory()
|
|
176
|
+
|
|
177
|
+
# Inject system prompt only for brand-new sessions.
|
|
178
|
+
if len(history) == 0:
|
|
179
|
+
history.add_system(self.config.system_prompt)
|
|
180
|
+
|
|
181
|
+
# Extension point: subclasses (e.g. BusAwareAgent) inject context here.
|
|
182
|
+
await self._pre_run_hook(history)
|
|
183
|
+
|
|
184
|
+
history.add_human(user_input)
|
|
185
|
+
|
|
186
|
+
# ── 2. Prepare tool schemas ────────────────────────────────────────
|
|
187
|
+
tool_schemas: list[ToolSchema] | None = None
|
|
188
|
+
if self.config.tools_enabled and self._tools:
|
|
189
|
+
tool_schemas = [t.schema for t in self._tools]
|
|
190
|
+
|
|
191
|
+
# ── 3. ReAct loop ──────────────────────────────────────────────────
|
|
192
|
+
steps: list[StepResult] = []
|
|
193
|
+
total_input = 0
|
|
194
|
+
total_output = 0
|
|
195
|
+
stopped_by = "max_iterations"
|
|
196
|
+
output = ""
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
for iteration in range(1, self.config.max_iterations + 1):
|
|
200
|
+
response = await self._llm.complete(
|
|
201
|
+
history,
|
|
202
|
+
tools=tool_schemas,
|
|
203
|
+
max_tokens=self.config.max_tokens,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
total_input += response.input_tokens
|
|
207
|
+
total_output += response.output_tokens
|
|
208
|
+
|
|
209
|
+
ai_message: AIMessage = response.message
|
|
210
|
+
history.add(ai_message)
|
|
211
|
+
|
|
212
|
+
has_tool_calls = bool(ai_message.tool_calls)
|
|
213
|
+
is_final = response.stop_reason == "end_turn" and not has_tool_calls
|
|
214
|
+
|
|
215
|
+
step = StepResult(
|
|
216
|
+
iteration=iteration,
|
|
217
|
+
thought=ai_message.content,
|
|
218
|
+
tool_calls=list(ai_message.tool_calls),
|
|
219
|
+
stop_reason=response.stop_reason,
|
|
220
|
+
is_final=is_final,
|
|
221
|
+
)
|
|
222
|
+
steps.append(step)
|
|
223
|
+
|
|
224
|
+
if self.config.verbose:
|
|
225
|
+
snippet = ai_message.content[:120].replace("\n", " ")
|
|
226
|
+
print(
|
|
227
|
+
f"[{self.config.name}] iter={iteration}"
|
|
228
|
+
f" stop={response.stop_reason}"
|
|
229
|
+
f" thought={snippet!r}"
|
|
230
|
+
)
|
|
231
|
+
for tc in ai_message.tool_calls:
|
|
232
|
+
print(f" → tool_call: {tc.name}({tc.arguments})")
|
|
233
|
+
|
|
234
|
+
# ── clean exit ─────────────────────────────────────────────
|
|
235
|
+
if is_final:
|
|
236
|
+
output = ai_message.content
|
|
237
|
+
stopped_by = "end_turn"
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# ── tool dispatch ──────────────────────────────────────────
|
|
241
|
+
if has_tool_calls:
|
|
242
|
+
for tc in ai_message.tool_calls:
|
|
243
|
+
result_content, is_error = await self._dispatch_tool(tc)
|
|
244
|
+
|
|
245
|
+
if self.config.verbose:
|
|
246
|
+
status = "ERROR" if is_error else "OK"
|
|
247
|
+
print(f" ← tool_result [{status}]: {result_content[:120]!r}")
|
|
248
|
+
|
|
249
|
+
history.add_tool_result(
|
|
250
|
+
tool_call_id=tc.id,
|
|
251
|
+
content=result_content,
|
|
252
|
+
is_error=is_error,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# If stop_reason is "max_tokens" or another non-final reason
|
|
256
|
+
# with no tool calls, the loop continues to let the model
|
|
257
|
+
# recover in the next iteration.
|
|
258
|
+
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
stopped_by = "error"
|
|
261
|
+
output = str(exc)
|
|
262
|
+
if self.config.verbose:
|
|
263
|
+
print(f"[{self.config.name}] ERROR: {exc}")
|
|
264
|
+
|
|
265
|
+
# Grab the last thought as output when the loop was exhausted.
|
|
266
|
+
if stopped_by == "max_iterations" and steps:
|
|
267
|
+
output = steps[-1].thought
|
|
268
|
+
|
|
269
|
+
# ── 4. Persist history ─────────────────────────────────────────────
|
|
270
|
+
if self._session_manager and session_id:
|
|
271
|
+
await self._session_manager.save_history(
|
|
272
|
+
session_id,
|
|
273
|
+
history,
|
|
274
|
+
iteration=len(steps),
|
|
275
|
+
metadata={"stopped_by": stopped_by},
|
|
276
|
+
)
|
|
277
|
+
elif self._memory and session_id:
|
|
278
|
+
await self._memory.save(session_id, history)
|
|
279
|
+
|
|
280
|
+
return AgentResult(
|
|
281
|
+
output=output,
|
|
282
|
+
steps=steps,
|
|
283
|
+
total_input_tokens=total_input,
|
|
284
|
+
total_output_tokens=total_output,
|
|
285
|
+
stopped_by=stopped_by,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# ------------------------------------------------------------------
|
|
289
|
+
# Convenience wrapper
|
|
290
|
+
# ------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
async def chat(self, user_input: str, session_id: str = "default") -> str:
|
|
293
|
+
"""Single-call interface — returns just the final response string."""
|
|
294
|
+
result = await self.run(user_input, session_id)
|
|
295
|
+
return result.output
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
# Extension hooks
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
async def _pre_run_hook(self, history: MessageHistory) -> None:
|
|
302
|
+
"""Called after system-prompt injection, before the user message is added.
|
|
303
|
+
|
|
304
|
+
Override in subclasses to inject additional context into *history*
|
|
305
|
+
before the ReAct loop starts. The base implementation is a no-op.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
# ------------------------------------------------------------------
|
|
309
|
+
# Internal helpers
|
|
310
|
+
# ------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
async def _dispatch_tool(self, tc: ToolCall) -> tuple[str, bool]:
|
|
313
|
+
"""Execute one tool call and return (result_content, is_error)."""
|
|
314
|
+
tool = self._tool_map.get(tc.name)
|
|
315
|
+
if tool is None:
|
|
316
|
+
return f"Tool not found: {tc.name}", True
|
|
317
|
+
try:
|
|
318
|
+
content = await tool.execute(**tc.arguments)
|
|
319
|
+
return content, False
|
|
320
|
+
except Exception as exc: # noqa: BLE001
|
|
321
|
+
return str(exc), True
|