agentsdk-py 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.
agentsdk/__init__.py ADDED
@@ -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
+ ]
agentsdk/agent.py ADDED
@@ -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