pyagentkit 1.0.0__tar.gz → 1.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.
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/PKG-INFO +12 -9
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/README.md +11 -8
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/USAGE.md +71 -52
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/pyproject.toml +1 -1
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/src/pyagentkit/agent.py +44 -28
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/src/pyagentkit/async_agent.py +43 -25
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/src/pyagentkit/definitions.py +6 -3
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/.github/workflows/publish.yml +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/.github/workflows/python-publish.yml +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/.gitignore +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/.python-version +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/LICENSE +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/src/pyagentkit/__init__.py +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/src/pyagentkit/exceptions.py +0 -0
- {pyagentkit-1.0.0 → pyagentkit-1.1.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyagentkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Agent toolkit for Ollama
|
|
5
5
|
Project-URL: Homepage, https://github.com/TarikEren/pyagentkit
|
|
6
6
|
Project-URL: Issues, https://github.com/TarikEren/pyagentkit/issues
|
|
@@ -33,7 +33,8 @@ A Python library for building tool-calling agents on top of locally-hosted LLMs
|
|
|
33
33
|
- **Dependency injection** — pass runtime dependencies (database connections, config, etc.) through `AgentDependencies` into tools without polluting tool signatures
|
|
34
34
|
- **Retry logic** — configurable retry budgets separately for tool calls and for response validation failures
|
|
35
35
|
- **Approval gates** — optionally require human confirmation before any tool is executed
|
|
36
|
-
- **Lifecycle hooks** — callbacks for tool calls, retries, successes, and final responses
|
|
36
|
+
- **Lifecycle hooks** — sync and async callbacks for tool calls, retries, successes, and final responses
|
|
37
|
+
- **Validation hook** — supply `on_validate` to run custom response logic and trigger retries or abort
|
|
37
38
|
- **Agent composition** — expose any agent as a tool that another agent can call via `.as_tool()`
|
|
38
39
|
- **Token usage tracking** — cumulative `TokenUsage` object updated after every LLM call
|
|
39
40
|
- **Message history** — persistent within a session, with optional trimming, save, and load
|
|
@@ -76,19 +77,21 @@ src/pyagentkit/
|
|
|
76
77
|
Every agent responds in one of two JSON shapes, validated via Pydantic:
|
|
77
78
|
|
|
78
79
|
```json
|
|
79
|
-
// Tool call
|
|
80
80
|
{
|
|
81
81
|
"response": {
|
|
82
82
|
"type": "tool_call",
|
|
83
|
+
"message": "<why you're calling the tool>",
|
|
83
84
|
"tool_call": { "name": "my_tool", "params": [{ "name": "x", "value": "42" }] }
|
|
84
|
-
}
|
|
85
|
-
"message": "Calling my_tool to get the result"
|
|
85
|
+
}
|
|
86
86
|
}
|
|
87
|
+
```
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
```json
|
|
89
90
|
{
|
|
90
|
-
"response": {
|
|
91
|
-
|
|
91
|
+
"response": {
|
|
92
|
+
"type": "final",
|
|
93
|
+
"message": "<your answer or result of your operation(s)>"
|
|
94
|
+
}
|
|
92
95
|
}
|
|
93
96
|
```
|
|
94
97
|
|
|
@@ -112,4 +115,4 @@ ExceptionUnhandledError # Unhandled runtime exception (not a PyAgentKit
|
|
|
112
115
|
|
|
113
116
|
## License
|
|
114
117
|
|
|
115
|
-
|
|
118
|
+
Apache 2.0
|
|
@@ -13,7 +13,8 @@ A Python library for building tool-calling agents on top of locally-hosted LLMs
|
|
|
13
13
|
- **Dependency injection** — pass runtime dependencies (database connections, config, etc.) through `AgentDependencies` into tools without polluting tool signatures
|
|
14
14
|
- **Retry logic** — configurable retry budgets separately for tool calls and for response validation failures
|
|
15
15
|
- **Approval gates** — optionally require human confirmation before any tool is executed
|
|
16
|
-
- **Lifecycle hooks** — callbacks for tool calls, retries, successes, and final responses
|
|
16
|
+
- **Lifecycle hooks** — sync and async callbacks for tool calls, retries, successes, and final responses
|
|
17
|
+
- **Validation hook** — supply `on_validate` to run custom response logic and trigger retries or abort
|
|
17
18
|
- **Agent composition** — expose any agent as a tool that another agent can call via `.as_tool()`
|
|
18
19
|
- **Token usage tracking** — cumulative `TokenUsage` object updated after every LLM call
|
|
19
20
|
- **Message history** — persistent within a session, with optional trimming, save, and load
|
|
@@ -56,19 +57,21 @@ src/pyagentkit/
|
|
|
56
57
|
Every agent responds in one of two JSON shapes, validated via Pydantic:
|
|
57
58
|
|
|
58
59
|
```json
|
|
59
|
-
// Tool call
|
|
60
60
|
{
|
|
61
61
|
"response": {
|
|
62
62
|
"type": "tool_call",
|
|
63
|
+
"message": "<why you're calling the tool>",
|
|
63
64
|
"tool_call": { "name": "my_tool", "params": [{ "name": "x", "value": "42" }] }
|
|
64
|
-
}
|
|
65
|
-
"message": "Calling my_tool to get the result"
|
|
65
|
+
}
|
|
66
66
|
}
|
|
67
|
+
```
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
```json
|
|
69
70
|
{
|
|
70
|
-
"response": {
|
|
71
|
-
|
|
71
|
+
"response": {
|
|
72
|
+
"type": "final",
|
|
73
|
+
"message": "<your answer or result of your operation(s)>"
|
|
74
|
+
}
|
|
72
75
|
}
|
|
73
76
|
```
|
|
74
77
|
|
|
@@ -92,4 +95,4 @@ ExceptionUnhandledError # Unhandled runtime exception (not a PyAgentKit
|
|
|
92
95
|
|
|
93
96
|
## License
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
Apache 2.0
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
5. [Custom Response Models](#5-custom-response-models)
|
|
12
12
|
6. [Async Agent](#6-async-agent)
|
|
13
13
|
7. [Lifecycle Hooks](#7-lifecycle-hooks)
|
|
14
|
-
8. [
|
|
15
|
-
9. [
|
|
16
|
-
10. [
|
|
17
|
-
11. [
|
|
18
|
-
12. [
|
|
14
|
+
8. [Validation Hook](#8-validation-hook)
|
|
15
|
+
9. [Agent Composition](#9-agent-composition)
|
|
16
|
+
10. [Message History](#10-message-history)
|
|
17
|
+
11. [Token Usage](#11-token-usage)
|
|
18
|
+
12. [Error Handling](#12-error-handling)
|
|
19
|
+
13. [Configuration Reference](#13-configuration-reference)
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -25,13 +26,13 @@
|
|
|
25
26
|
from pyagentkit import Agent
|
|
26
27
|
|
|
27
28
|
agent = Agent(
|
|
28
|
-
llm_name="<llm_name>",
|
|
29
|
+
llm_name="<llm_name>",
|
|
29
30
|
system_prompt="You are a helpful assistant.",
|
|
30
31
|
agent_name="my-agent",
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
response = agent.handle_response("What is 2 + 2?")
|
|
34
|
-
print(response.message)
|
|
35
|
+
print(response.response.message) # "The answer is 4"
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
`handle_response` blocks until the agent produces a `final` response or exhausts its retries.
|
|
@@ -43,7 +44,6 @@ print(response.message) # "The answer is 4"
|
|
|
43
44
|
A tool is any plain Python function that returns a `ToolResult`. It **must** have a docstring — the docstring is what the agent reads to understand what the tool does.
|
|
44
45
|
|
|
45
46
|
```python
|
|
46
|
-
from pyagentkit import Agent
|
|
47
47
|
from pyagentkit.definitions import ToolResult, ToolReturnValue
|
|
48
48
|
|
|
49
49
|
|
|
@@ -73,11 +73,11 @@ def divide(a: float, b: float) -> ToolResult:
|
|
|
73
73
|
```python
|
|
74
74
|
agent = Agent(
|
|
75
75
|
llm_name="<llm_name>",
|
|
76
|
-
tools=[add_numbers, divide],
|
|
76
|
+
tools=[add_numbers, divide],
|
|
77
77
|
)
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Tools passed via the `tools=` constructor parameter use `requires_approval=True` by default, meaning the user will be prompted before each execution. To skip that, register them with `add_tool` instead:
|
|
81
81
|
|
|
82
82
|
```python
|
|
83
83
|
agent.add_tool(add_numbers, requires_approval=False)
|
|
@@ -138,13 +138,13 @@ class MyDeps(AgentDependencies):
|
|
|
138
138
|
|
|
139
139
|
def fetch_user(deps: MyDeps, user_id: str) -> ToolResult:
|
|
140
140
|
"""Fetch a user record from the database by user ID."""
|
|
141
|
-
# deps.db_url and deps.api_key are available here
|
|
142
|
-
# but `deps` is hidden from the LLM's tool signature
|
|
141
|
+
# deps.db_url and deps.api_key are available here,
|
|
142
|
+
# but `deps` is hidden from the LLM's view of the tool signature
|
|
143
143
|
result = f"User {user_id} from {deps.db_url}"
|
|
144
144
|
return ToolResult(return_value=ToolReturnValue.success, content=result)
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
deps = MyDeps(
|
|
147
|
+
deps = MyDeps(db_url="postgresql://...", api_key="sk-...")
|
|
148
148
|
|
|
149
149
|
agent = Agent(llm_name="<llm_name>", tools=[fetch_user])
|
|
150
150
|
response = agent.handle_response("Find user 42", deps=deps)
|
|
@@ -185,7 +185,7 @@ The schema examples injected into the system prompt are generated automatically
|
|
|
185
185
|
|
|
186
186
|
## 6. Async Agent
|
|
187
187
|
|
|
188
|
-
Use `AsyncAgent` in `asyncio` contexts. The API mirrors `Agent` except all relevant methods are coroutines and the client is `ollama.AsyncClient`.
|
|
188
|
+
Use `AsyncAgent` in `asyncio` contexts. The API mirrors `Agent` except all relevant methods are coroutines and the Ollama client is `ollama.AsyncClient`. Both sync and async callables are accepted for all hooks.
|
|
189
189
|
|
|
190
190
|
```python
|
|
191
191
|
import asyncio
|
|
@@ -195,7 +195,6 @@ from pyagentkit.definitions import ToolResult, ToolReturnValue
|
|
|
195
195
|
|
|
196
196
|
async def get_weather(city: str) -> ToolResult:
|
|
197
197
|
"""Return current weather for a given city name."""
|
|
198
|
-
# ... call a weather API ...
|
|
199
198
|
return ToolResult(return_value=ToolReturnValue.success, content=f"Sunny in {city}")
|
|
200
199
|
|
|
201
200
|
|
|
@@ -206,20 +205,20 @@ async def main():
|
|
|
206
205
|
agent_name="weather-agent",
|
|
207
206
|
)
|
|
208
207
|
response = await agent.handle_response("What is the weather in Istanbul?")
|
|
209
|
-
print(response.message)
|
|
208
|
+
print(response.response.message)
|
|
210
209
|
agent.dispose()
|
|
211
210
|
|
|
212
211
|
|
|
213
212
|
asyncio.run(main())
|
|
214
213
|
```
|
|
215
214
|
|
|
216
|
-
> **Important:** Use `await AsyncAgent.create(...)`
|
|
215
|
+
> **Important:** Use `await AsyncAgent.create(...)` rather than constructing `AsyncAgent(...)` directly. The constructor is synchronous and cannot run the async Ollama environment check — `create` does this for you. Constructing directly will emit a warning and skip validation.
|
|
217
216
|
|
|
218
217
|
---
|
|
219
218
|
|
|
220
219
|
## 7. Lifecycle Hooks
|
|
221
220
|
|
|
222
|
-
Hooks let you observe
|
|
221
|
+
Hooks let you observe what the agent is doing without modifying its core logic. All hooks accept both sync and async callables in `AsyncAgent`; `Agent` accepts sync callables only.
|
|
223
222
|
|
|
224
223
|
```python
|
|
225
224
|
from pyagentkit.definitions import AgentResponse
|
|
@@ -238,7 +237,7 @@ def on_tool_success(tool_name: str, params: dict) -> None:
|
|
|
238
237
|
|
|
239
238
|
|
|
240
239
|
def on_response(response: AgentResponse) -> None:
|
|
241
|
-
print(f"[FINAL] {response.message}")
|
|
240
|
+
print(f"[FINAL] {response.response.message}")
|
|
242
241
|
|
|
243
242
|
|
|
244
243
|
def on_response_retry(attempt: int, response_str: str, error: str) -> None:
|
|
@@ -257,23 +256,54 @@ agent = Agent(
|
|
|
257
256
|
|
|
258
257
|
---
|
|
259
258
|
|
|
260
|
-
## 8.
|
|
259
|
+
## 8. Validation Hook
|
|
260
|
+
|
|
261
|
+
`on_validate` runs after every response is parsed and validated by Pydantic, before the agent decides whether to call a tool or return a final answer. Raise `ExceptionAgentError` to send a correction back to the agent and consume a response retry. Raise `ExceptionAgentFatal` to abort immediately.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from pyagentkit.definitions import AgentResponse
|
|
265
|
+
from pyagentkit.exceptions import ExceptionAgentError
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def validate(response: AgentResponse) -> None:
|
|
269
|
+
if response.response.type == "final" and len(response.response.message) < 10:
|
|
270
|
+
raise ExceptionAgentError("Final response is too short. Please elaborate.")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
agent = Agent(
|
|
274
|
+
llm_name="<llm_name>",
|
|
275
|
+
on_validate=validate,
|
|
276
|
+
)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Async validators work the same way in `AsyncAgent`:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
async def validate(response: AgentResponse) -> None:
|
|
283
|
+
result = await some_async_check(response.response.message)
|
|
284
|
+
if not result:
|
|
285
|
+
raise ExceptionAgentError("Response failed async validation.")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
agent = await AsyncAgent.create(llm_name="<llm_name>", on_validate=validate)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 9. Agent Composition
|
|
261
294
|
|
|
262
295
|
Any agent can expose itself as a tool for another agent via `.as_tool()`.
|
|
263
296
|
|
|
264
297
|
```python
|
|
265
298
|
from pyagentkit import Agent
|
|
266
|
-
from pyagentkit.definitions import ToolResult, ToolReturnValue
|
|
267
299
|
|
|
268
300
|
|
|
269
|
-
# Inner specialist agent
|
|
270
301
|
researcher = Agent(
|
|
271
302
|
llm_name="<llm_name>",
|
|
272
303
|
agent_name="researcher",
|
|
273
304
|
system_prompt="You are a research specialist. Answer factual questions concisely.",
|
|
274
305
|
)
|
|
275
306
|
|
|
276
|
-
# Outer orchestrator agent
|
|
277
307
|
orchestrator = Agent(
|
|
278
308
|
llm_name="<llm_name>",
|
|
279
309
|
agent_name="orchestrator",
|
|
@@ -282,14 +312,14 @@ orchestrator = Agent(
|
|
|
282
312
|
)
|
|
283
313
|
|
|
284
314
|
response = orchestrator.handle_response("What is the capital of Japan?")
|
|
285
|
-
print(response.message)
|
|
315
|
+
print(response.response.message)
|
|
286
316
|
```
|
|
287
317
|
|
|
288
|
-
`.as_tool()` wraps `handle_response` in a `ToolResult`-returning function
|
|
318
|
+
`.as_tool()` wraps `handle_response` in a `ToolResult`-returning function named after the agent. If the inner agent raises a `PyAgentKitError`, it is caught and returned to the outer agent as a recoverable tool error rather than propagating.
|
|
289
319
|
|
|
290
320
|
---
|
|
291
321
|
|
|
292
|
-
##
|
|
322
|
+
## 10. Message History
|
|
293
323
|
|
|
294
324
|
History is maintained automatically within a session. Each call to `handle_response` appends to the same history, giving the agent memory of prior turns.
|
|
295
325
|
|
|
@@ -298,7 +328,7 @@ agent = Agent(llm_name="<llm_name>", agent_name="chat-agent")
|
|
|
298
328
|
|
|
299
329
|
r1 = agent.handle_response("My name is Alice.")
|
|
300
330
|
r2 = agent.handle_response("What is my name?")
|
|
301
|
-
print(r2.message) # "Your name is Alice."
|
|
331
|
+
print(r2.response.message) # "Your name is Alice."
|
|
302
332
|
```
|
|
303
333
|
|
|
304
334
|
### Trimming history
|
|
@@ -314,7 +344,7 @@ agent = Agent(llm_name="<llm_name>", max_history=20)
|
|
|
314
344
|
```python
|
|
315
345
|
agent.save_history("history.json")
|
|
316
346
|
|
|
317
|
-
#
|
|
347
|
+
# In a new session:
|
|
318
348
|
agent.load_history("history.json")
|
|
319
349
|
```
|
|
320
350
|
|
|
@@ -326,7 +356,7 @@ agent.clear_history()
|
|
|
326
356
|
|
|
327
357
|
---
|
|
328
358
|
|
|
329
|
-
##
|
|
359
|
+
## 11. Token Usage
|
|
330
360
|
|
|
331
361
|
`agent.token_usage` is a `TokenUsage` object that accumulates across all `handle_response` calls on an instance.
|
|
332
362
|
|
|
@@ -339,16 +369,16 @@ print(agent.token_usage.total_tokens)
|
|
|
339
369
|
|
|
340
370
|
---
|
|
341
371
|
|
|
342
|
-
##
|
|
372
|
+
## 12. Error Handling
|
|
343
373
|
|
|
344
374
|
```python
|
|
345
|
-
from pyagentkit
|
|
375
|
+
from pyagentkit import (
|
|
346
376
|
ExceptionToolRetriesExhausted,
|
|
347
377
|
ExceptionResponseRetriesExhausted,
|
|
348
378
|
ExceptionFatalError,
|
|
349
379
|
ExceptionEnvironmentError,
|
|
350
|
-
ExceptionUnhandledError,
|
|
351
380
|
)
|
|
381
|
+
from pyagentkit.exceptions import ExceptionUnhandledError
|
|
352
382
|
|
|
353
383
|
try:
|
|
354
384
|
response = agent.handle_response("Do something complex.")
|
|
@@ -364,7 +394,7 @@ except ExceptionUnhandledError as e:
|
|
|
364
394
|
print(f"Unexpected error: {e}")
|
|
365
395
|
```
|
|
366
396
|
|
|
367
|
-
Inside a tool, signal errors
|
|
397
|
+
Inside a tool, signal errors using return values rather than raising exceptions directly:
|
|
368
398
|
|
|
369
399
|
```python
|
|
370
400
|
def risky_tool(path: str) -> ToolResult:
|
|
@@ -382,21 +412,22 @@ def risky_tool(path: str) -> ToolResult:
|
|
|
382
412
|
|
|
383
413
|
---
|
|
384
414
|
|
|
385
|
-
##
|
|
415
|
+
## 13. Configuration Reference
|
|
386
416
|
|
|
387
417
|
| Parameter | Type | Default | Description |
|
|
388
418
|
|-----------|------|---------|-------------|
|
|
389
|
-
| `llm_name` | `str` | required | Ollama model name
|
|
390
|
-
| `agent_name` | `str \| None` | `llm_name` |
|
|
419
|
+
| `llm_name` | `str` | required | Ollama model name |
|
|
420
|
+
| `agent_name` | `str \| None` | `llm_name` | Name for this agent; auto-suffixed if already taken |
|
|
391
421
|
| `system_prompt` | `str \| None` | `""` | Base system prompt prepended to all requests |
|
|
392
422
|
| `instructions` | `str \| None` | `""` | Text appended to every user prompt |
|
|
393
423
|
| `response_model` | `Type[AgentResponse]` | `AgentResponse` | Pydantic model for response validation |
|
|
394
424
|
| `tool_retries` | `int` | `3` | Max tool call retries before raising |
|
|
395
425
|
| `response_retries` | `int` | `3` | Max response validation retries before raising |
|
|
396
426
|
| `num_ctx` | `int` | `8192` | Ollama context window size |
|
|
397
|
-
| `temperature` | `float
|
|
398
|
-
| `top_p` | `float
|
|
399
|
-
| `
|
|
427
|
+
| `temperature` | `float` | `0.0` | Sampling temperature |
|
|
428
|
+
| `top_p` | `float` | `0.0` | Nucleus sampling probability |
|
|
429
|
+
| `top_k` | `float` | `0.0` | Top-k sampling |
|
|
430
|
+
| `seed` | `int` | `42` | Random seed for reproducibility |
|
|
400
431
|
| `ollama_url` | `str \| None` | `None` | Custom Ollama host URL; uses default if `None` |
|
|
401
432
|
| `tools` | `list \| None` | `None` | Tool functions to register on init |
|
|
402
433
|
| `max_history` | `int \| None` | `None` | Max non-system messages to retain |
|
|
@@ -407,16 +438,4 @@ def risky_tool(path: str) -> ToolResult:
|
|
|
407
438
|
| `on_tool_success` | callable | `None` | Hook called on tool success |
|
|
408
439
|
| `on_response` | callable | `None` | Hook called on final response |
|
|
409
440
|
| `on_response_retry` | callable | `None` | Hook called on response validation failure |
|
|
410
|
-
|
|
411
|
-
### Agent lifecycle
|
|
412
|
-
|
|
413
|
-
```python
|
|
414
|
-
agent = Agent(llm_name="<llm_name>", agent_name="my-agent")
|
|
415
|
-
|
|
416
|
-
# Use the agent ...
|
|
417
|
-
|
|
418
|
-
# When done, unregister and clean up the logger:
|
|
419
|
-
agent.dispose()
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
Each agent name must be unique within the `Agent` or `AsyncAgent` registry. Calling `dispose()` frees the name so it can be reused.
|
|
441
|
+
| `on_validate` | callable | `None` | Hook called after parsing; raise to trigger retry or abort |
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import logging
|
|
6
6
|
import inspect
|
|
7
7
|
from typing import (
|
|
8
|
+
Any,
|
|
8
9
|
Callable,
|
|
9
10
|
ClassVar,
|
|
10
11
|
Generic,
|
|
@@ -184,19 +185,22 @@ class Agent(Generic[T]):
|
|
|
184
185
|
return result
|
|
185
186
|
|
|
186
187
|
# Canonical examples
|
|
187
|
-
tool_example = {
|
|
188
|
+
tool_example: dict[str, Any] = {
|
|
188
189
|
"response": {
|
|
189
190
|
"type": "tool_call",
|
|
191
|
+
"message": "<why you're calling the tool>",
|
|
190
192
|
"tool_call": {
|
|
191
193
|
"name": "<tool_name>",
|
|
192
194
|
"params": [{"name": "<param_name>", "value": "<param_value>"}],
|
|
193
195
|
},
|
|
194
196
|
},
|
|
195
|
-
"message": "<why you're calling the tool>",
|
|
196
197
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
198
|
+
|
|
199
|
+
final_example: dict[str, Any] = {
|
|
200
|
+
"response": {
|
|
201
|
+
"type": "final",
|
|
202
|
+
"message": "<your answer or result of your operation(s)>",
|
|
203
|
+
},
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
# Append any subclass-defined extra fields to both examples
|
|
@@ -287,9 +291,10 @@ class Agent(Generic[T]):
|
|
|
287
291
|
|
|
288
292
|
def run_agent(prompt: str) -> ToolResult:
|
|
289
293
|
try:
|
|
290
|
-
|
|
294
|
+
output = agent.handle_response(prompt=prompt, deps=deps)
|
|
291
295
|
return ToolResult(
|
|
292
|
-
return_value=ToolReturnValue.success,
|
|
296
|
+
return_value=ToolReturnValue.success,
|
|
297
|
+
content=output.response.message,
|
|
293
298
|
)
|
|
294
299
|
except PyAgentKitError as err:
|
|
295
300
|
return ToolResult(return_value=ToolReturnValue.error, content=str(err))
|
|
@@ -310,9 +315,10 @@ class Agent(Generic[T]):
|
|
|
310
315
|
agent_name: str | None = None,
|
|
311
316
|
response_model: Type[T] = AgentResponse,
|
|
312
317
|
num_ctx: int = 8192,
|
|
313
|
-
temperature: float
|
|
314
|
-
top_p: float
|
|
315
|
-
|
|
318
|
+
temperature: float = 0.0,
|
|
319
|
+
top_p: float = 0.0,
|
|
320
|
+
top_k: float = 0.0,
|
|
321
|
+
seed: int = 42,
|
|
316
322
|
ollama_url: str | None = None,
|
|
317
323
|
tools: list[TypeTool] | None = None,
|
|
318
324
|
max_history: int | None = None,
|
|
@@ -352,13 +358,13 @@ class Agent(Generic[T]):
|
|
|
352
358
|
self.logger.addHandler(handler)
|
|
353
359
|
|
|
354
360
|
# Create Ollama options dictionary
|
|
355
|
-
self.ollama_options: dict = {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
self.ollama_options: dict = {
|
|
362
|
+
"num_ctx": num_ctx,
|
|
363
|
+
"temperature": temperature,
|
|
364
|
+
"seed": seed,
|
|
365
|
+
"top_p": top_p,
|
|
366
|
+
"top_k": top_k,
|
|
367
|
+
}
|
|
362
368
|
|
|
363
369
|
# Create custom Ollama client
|
|
364
370
|
self.ollama_url = ollama_url
|
|
@@ -417,9 +423,18 @@ class Agent(Generic[T]):
|
|
|
417
423
|
"""
|
|
418
424
|
result = "Your response failed validation, handle the following issues and generate your answer with the same `type`\nEncountered errors:"
|
|
419
425
|
for error in errors:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
426
|
+
err_type = error.get("type")
|
|
427
|
+
err_fields = error.get("loc")
|
|
428
|
+
if err_type == "missing":
|
|
429
|
+
result += "\nYou are missing field(s):"
|
|
430
|
+
for field in err_fields:
|
|
431
|
+
result += f"\n- {field}"
|
|
432
|
+
elif err_type == "json_invalid":
|
|
433
|
+
result += "Your response wasn't valid json. Make sure your response is perfect JSON"
|
|
434
|
+
else:
|
|
435
|
+
err_msg = error.get("msg")
|
|
436
|
+
result += f"\nType: {err_type}\nField: {'.'.join(map(str, err_fields))}\nMessage: {err_msg}"
|
|
437
|
+
|
|
423
438
|
result += "\nCRITICAL: Fix the errors and respond ONLY with valid JSON. Make sure you close out your curly braces ('{'). Do NOT include any markdown formatting."
|
|
424
439
|
return result
|
|
425
440
|
|
|
@@ -569,6 +584,7 @@ class Agent(Generic[T]):
|
|
|
569
584
|
raise ExceptionToolError(invalid_param_message)
|
|
570
585
|
|
|
571
586
|
# Call tool and get return value
|
|
587
|
+
self.logger.debug("Calling tool %s with params %s", tool_name, str(kwargs))
|
|
572
588
|
self._on_tool_call(tool_name=tool_name, params=kwargs)
|
|
573
589
|
try:
|
|
574
590
|
tool_return = accepted_tool.function(**kwargs)
|
|
@@ -728,7 +744,7 @@ Complete the user task using the available tools and schemas.
|
|
|
728
744
|
},
|
|
729
745
|
)
|
|
730
746
|
while response_try < self.response_retries and tool_try < self.tool_retries:
|
|
731
|
-
self.logger.debug("Response try: %s", response_try)
|
|
747
|
+
self.logger.debug("Response try: %s, Tool try: %s", response_try, tool_try)
|
|
732
748
|
content = ""
|
|
733
749
|
try:
|
|
734
750
|
self._trim_history()
|
|
@@ -746,26 +762,25 @@ Complete the user task using the available tools and schemas.
|
|
|
746
762
|
total_tokens=_prompt_tokens + _response_tokens,
|
|
747
763
|
)
|
|
748
764
|
if self.think and response.message.thinking:
|
|
749
|
-
self.logger.info(response.message.thinking)
|
|
765
|
+
self.logger.info("Thought: %s", response.message.thinking)
|
|
750
766
|
content = response.message.content
|
|
751
767
|
if content is None:
|
|
752
768
|
raise RuntimeError(
|
|
753
769
|
f"Failed to get response from agent {self.agent_name}"
|
|
754
770
|
)
|
|
755
|
-
self.logger.debug(
|
|
756
|
-
"Content from agent %s:\n%s", self.agent_name, content
|
|
757
|
-
)
|
|
758
|
-
# Append the assistant message because it doesn't know that it
|
|
759
|
-
# actually did anything
|
|
771
|
+
self.logger.debug("Content:\n%s", content)
|
|
760
772
|
self.message_history.append({"role": "assistant", "content": content})
|
|
761
773
|
|
|
762
774
|
stripped_content = self._strip_markdown_formatting(content)
|
|
763
775
|
validated = self.response_model.model_validate_json(stripped_content)
|
|
764
776
|
validated = validated.model_copy(
|
|
765
777
|
update={
|
|
766
|
-
"message": self._strip_markdown_formatting(
|
|
778
|
+
"message": self._strip_markdown_formatting(
|
|
779
|
+
validated.response.message
|
|
780
|
+
)
|
|
767
781
|
}
|
|
768
782
|
)
|
|
783
|
+
self.logger.info("Message: %s", validated.response.message)
|
|
769
784
|
|
|
770
785
|
self._on_validate(response=validated)
|
|
771
786
|
if validated.response.type == "final":
|
|
@@ -782,6 +797,7 @@ Complete the user task using the available tools and schemas.
|
|
|
782
797
|
|
|
783
798
|
except ValidationError as exc:
|
|
784
799
|
error_message = self._print_validation_errors(exc.errors())
|
|
800
|
+
self.logger.debug("Validation errors:\n%s\n", exc.errors())
|
|
785
801
|
self.message_history.append({"role": "user", "content": error_message})
|
|
786
802
|
self._on_response_retry(
|
|
787
803
|
response_try=response_try,
|
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
import inspect
|
|
11
11
|
import asyncio
|
|
12
12
|
from typing import (
|
|
13
|
+
Any,
|
|
13
14
|
Callable,
|
|
14
15
|
ClassVar,
|
|
15
16
|
Generic,
|
|
@@ -191,19 +192,22 @@ class AsyncAgent(Generic[T]):
|
|
|
191
192
|
return result
|
|
192
193
|
|
|
193
194
|
# Canonical examples
|
|
194
|
-
tool_example = {
|
|
195
|
+
tool_example: dict[str, Any] = {
|
|
195
196
|
"response": {
|
|
196
197
|
"type": "tool_call",
|
|
198
|
+
"message": "<why you're calling the tool>",
|
|
197
199
|
"tool_call": {
|
|
198
200
|
"name": "<tool_name>",
|
|
199
201
|
"params": [{"name": "<param_name>", "value": "<param_value>"}],
|
|
200
202
|
},
|
|
201
203
|
},
|
|
202
|
-
"message": "<why you're calling the tool>",
|
|
203
204
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
205
|
+
|
|
206
|
+
final_example: dict[str, Any] = {
|
|
207
|
+
"response": {
|
|
208
|
+
"type": "final",
|
|
209
|
+
"message": "<your answer or result of your operation(s)>",
|
|
210
|
+
},
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
# Append any subclass-defined extra fields to both examples
|
|
@@ -298,9 +302,10 @@ class AsyncAgent(Generic[T]):
|
|
|
298
302
|
|
|
299
303
|
async def run_agent(prompt: str) -> ToolResult:
|
|
300
304
|
try:
|
|
301
|
-
|
|
305
|
+
output = await agent.handle_response(prompt=prompt, deps=deps)
|
|
302
306
|
return ToolResult(
|
|
303
|
-
return_value=ToolReturnValue.success,
|
|
307
|
+
return_value=ToolReturnValue.success,
|
|
308
|
+
content=output.response.message,
|
|
304
309
|
)
|
|
305
310
|
except PyAgentKitError as err:
|
|
306
311
|
return ToolResult(return_value=ToolReturnValue.error, content=str(err))
|
|
@@ -321,9 +326,10 @@ class AsyncAgent(Generic[T]):
|
|
|
321
326
|
agent_name: str | None = None,
|
|
322
327
|
response_model: Type[T] = AgentResponse,
|
|
323
328
|
num_ctx: int = 8192,
|
|
324
|
-
temperature: float
|
|
325
|
-
top_p: float
|
|
326
|
-
|
|
329
|
+
temperature: float = 0.0,
|
|
330
|
+
top_p: float = 0.0,
|
|
331
|
+
top_k: float = 0.0,
|
|
332
|
+
seed: int = 42,
|
|
327
333
|
ollama_url: str | None = None,
|
|
328
334
|
tools: list[TypeAsyncTool] | None = None,
|
|
329
335
|
max_history: int | None = None,
|
|
@@ -368,13 +374,13 @@ class AsyncAgent(Generic[T]):
|
|
|
368
374
|
)
|
|
369
375
|
|
|
370
376
|
# Create Ollama options dictionary
|
|
371
|
-
self.ollama_options: dict = {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
self.ollama_options: dict = {
|
|
378
|
+
"num_ctx": num_ctx,
|
|
379
|
+
"temperature": temperature,
|
|
380
|
+
"seed": seed,
|
|
381
|
+
"top_p": top_p,
|
|
382
|
+
"top_k": top_k,
|
|
383
|
+
}
|
|
378
384
|
|
|
379
385
|
# Create custom Ollama client
|
|
380
386
|
self.ollama_url = ollama_url
|
|
@@ -438,9 +444,17 @@ class AsyncAgent(Generic[T]):
|
|
|
438
444
|
"""
|
|
439
445
|
result = "Your response failed validation, handle the following issues and generate your answer with the same `type`\nEncountered errors:"
|
|
440
446
|
for error in errors:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
447
|
+
err_type = error.get("type")
|
|
448
|
+
err_fields = error.get("loc")
|
|
449
|
+
if err_type == "missing":
|
|
450
|
+
result += "\nYou are missing field(s):"
|
|
451
|
+
for field in err_fields:
|
|
452
|
+
result += f"\n- {field}"
|
|
453
|
+
elif err_type == "json_invalid":
|
|
454
|
+
result += "Your response wasn't valid json. Make sure your response is perfect JSON"
|
|
455
|
+
else:
|
|
456
|
+
err_msg = error.get("msg")
|
|
457
|
+
result += f"\nType: {err_type}\nField: {'.'.join(map(str, err_fields))}\nMessage: {err_msg}"
|
|
444
458
|
result += "\nCRITICAL: Fix the errors and respond ONLY with valid JSON. Make sure you close out your curly braces ('{'). Do NOT include any markdown formatting."
|
|
445
459
|
return result
|
|
446
460
|
|
|
@@ -606,6 +620,7 @@ class AsyncAgent(Generic[T]):
|
|
|
606
620
|
)
|
|
607
621
|
raise ExceptionToolError(full_error)
|
|
608
622
|
|
|
623
|
+
self.logger.debug("Calling tool %s with params %s", tool_name, str(kwargs))
|
|
609
624
|
await self._on_tool_call(tool_name=tool_name, params=kwargs)
|
|
610
625
|
try:
|
|
611
626
|
if inspect.iscoroutinefunction(accepted_tool.function):
|
|
@@ -773,7 +788,7 @@ Complete the user task using the available tools and schemas.
|
|
|
773
788
|
|
|
774
789
|
while response_try < self.response_retries and tool_try < self.tool_retries:
|
|
775
790
|
content = ""
|
|
776
|
-
self.logger.debug("Response try: %s", response_try)
|
|
791
|
+
self.logger.debug("Response try: %s, Tool try: %s", response_try, tool_try)
|
|
777
792
|
try:
|
|
778
793
|
self._trim_history()
|
|
779
794
|
response = await self.ollama_client.chat(
|
|
@@ -796,18 +811,19 @@ Complete the user task using the available tools and schemas.
|
|
|
796
811
|
raise RuntimeError(
|
|
797
812
|
f"Failed to get response from agent {self.agent_name}"
|
|
798
813
|
)
|
|
799
|
-
self.logger.debug(
|
|
800
|
-
"Content from agent %s:\n%s", self.agent_name, content
|
|
801
|
-
)
|
|
814
|
+
self.logger.debug("Content:\n%s", content)
|
|
802
815
|
self.message_history.append({"role": "assistant", "content": content})
|
|
803
816
|
|
|
804
817
|
stripped_content = self._strip_markdown_formatting(content)
|
|
805
818
|
validated = self.response_model.model_validate_json(stripped_content)
|
|
806
819
|
validated = validated.model_copy(
|
|
807
820
|
update={
|
|
808
|
-
"message": self._strip_markdown_formatting(
|
|
821
|
+
"message": self._strip_markdown_formatting(
|
|
822
|
+
validated.response.message
|
|
823
|
+
)
|
|
809
824
|
}
|
|
810
825
|
)
|
|
826
|
+
self.logger.info("Message: %s", validated.response.message)
|
|
811
827
|
|
|
812
828
|
await self._on_validate(response=validated)
|
|
813
829
|
if validated.response.type == "final":
|
|
@@ -823,6 +839,8 @@ Complete the user task using the available tools and schemas.
|
|
|
823
839
|
|
|
824
840
|
except ValidationError as exc:
|
|
825
841
|
error_message = self._print_validation_errors(exc.errors())
|
|
842
|
+
self.logger.debug("Validation errors:\n%s\n", exc.errors())
|
|
843
|
+
self.logger.debug("Validation error prompt: \n%s\n", error_message)
|
|
826
844
|
self.message_history.append({"role": "user", "content": error_message})
|
|
827
845
|
await self._on_response_retry(
|
|
828
846
|
response_try=response_try,
|
|
@@ -16,7 +16,11 @@ class ToolReturnValue(Enum):
|
|
|
16
16
|
fatal = "fatal"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class
|
|
19
|
+
class BaseResponse(BaseModel):
|
|
20
|
+
message: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FinalResponse(BaseResponse):
|
|
20
24
|
type: Literal["final"]
|
|
21
25
|
|
|
22
26
|
|
|
@@ -38,14 +42,13 @@ class tool_call_schema(BaseModel):
|
|
|
38
42
|
params: list[tool_params]
|
|
39
43
|
|
|
40
44
|
|
|
41
|
-
class ToolCallResponse(
|
|
45
|
+
class ToolCallResponse(BaseResponse):
|
|
42
46
|
type: Literal["tool_call"]
|
|
43
47
|
tool_call: tool_call_schema
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
class AgentResponse(BaseModel):
|
|
47
51
|
response: Union[FinalResponse, ToolCallResponse] = Field(discriminator="type")
|
|
48
|
-
message: str
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
# Tool types
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|