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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentkit
3
- Version: 1.0.0
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
- // Final answer
89
+ ```json
89
90
  {
90
- "response": { "type": "final" },
91
- "message": "The answer is 42"
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
- APACHE 2.0
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
- // Final answer
69
+ ```json
69
70
  {
70
- "response": { "type": "final" },
71
- "message": "The answer is 42"
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
- APACHE 2.0
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. [Agent Composition](#8-agent-composition)
15
- 9. [Message History](#9-message-history)
16
- 10. [Token Usage](#10-token-usage)
17
- 11. [Error Handling](#11-error-handling)
18
- 12. [Configuration Reference](#12-configuration-reference)
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>", # Must be pulled in Ollama
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) # "The answer is 4"
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], # registered without approval requirement
76
+ tools=[add_numbers, divide],
77
77
  )
78
78
  ```
79
79
 
80
- By default, tools added via the `tools=` constructor parameter require approval (`requires_approval=True`). To skip the prompt, use `add_tool` with `requires_approval=False`:
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(prompt="Find user 42", db_url="postgresql://...", api_key="sk-...")
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(...)` instead of `AsyncAgent(...)` directly. The constructor is synchronous but environment verification (`_verify_ollama_environment`) is async and must be awaited via `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 and log what the agent is doing without modifying its core logic.
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. Agent Composition
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 and names it after the agent, so the outer agent can discover and call it like any other tool.
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
- ## 9. Message History
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
- # Later, in a new session:
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
- ## 10. Token Usage
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
- ## 11. Error Handling
372
+ ## 12. Error Handling
343
373
 
344
374
  ```python
345
- from pyagentkit.exceptions import (
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 to the agent using return values rather than raising exceptions directly:
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
- ## 12. Configuration Reference
415
+ ## 13. Configuration Reference
386
416
 
387
417
  | Parameter | Type | Default | Description |
388
418
  |-----------|------|---------|-------------|
389
- | `llm_name` | `str` | required | Ollama model name (e.g. `"devstral:22b"`) |
390
- | `agent_name` | `str \| None` | `llm_name` | Unique name for this agent instance |
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 \| None` | `None` | Sampling temperature |
398
- | `top_p` | `float \| None` | `None` | Nucleus sampling probability |
399
- | `seed` | `int \| None` | `None` | Random seed for reproducibility |
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 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyagentkit"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "Agent toolkit for Ollama"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
- final_example = {
198
- "response": {"type": "final"},
199
- "message": "<your answer or result of your operation(s)>",
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
- response = agent.handle_response(prompt=prompt, deps=deps)
294
+ output = agent.handle_response(prompt=prompt, deps=deps)
291
295
  return ToolResult(
292
- return_value=ToolReturnValue.success, content=response.message
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 | None = None,
314
- top_p: float | None = None,
315
- seed: int | None = None,
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 = {"num_ctx": num_ctx}
356
- if temperature:
357
- self.ollama_options["temperature"] = temperature
358
- if top_p:
359
- self.ollama_options["top_p"] = top_p
360
- if seed:
361
- self.ollama_options["seed"] = seed
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
- result += f"\nType: {error['type']}"
421
- result += f"\nField: {'.'.join(map(str, error['loc']))}"
422
- result += f"\nError message: {error['msg']}"
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(validated.message)
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
- final_example = {
205
- "response": {"type": "final"},
206
- "message": "<your answer or result of your operation(s)>",
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
- response = await agent.handle_response(prompt=prompt, deps=deps)
305
+ output = await agent.handle_response(prompt=prompt, deps=deps)
302
306
  return ToolResult(
303
- return_value=ToolReturnValue.success, content=response.message
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 | None = None,
325
- top_p: float | None = None,
326
- seed: int | None = None,
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 = {"num_ctx": num_ctx}
372
- if temperature:
373
- self.ollama_options["temperature"] = temperature
374
- if top_p:
375
- self.ollama_options["top_p"] = top_p
376
- if seed:
377
- self.ollama_options["seed"] = seed
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
- result += f"\nType: {error['type']}"
442
- result += f"\nField: {'.'.join(map(str, error['loc']))}"
443
- result += f"\nError message: {error['msg']}"
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(validated.message)
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 FinalResponse(BaseModel):
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(BaseModel):
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