pyagentkit 0.1.4__tar.gz → 0.1.6__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-0.1.6/PKG-INFO +115 -0
- pyagentkit-0.1.6/README.md +95 -0
- pyagentkit-0.1.6/USAGE.md +422 -0
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/pyproject.toml +1 -1
- pyagentkit-0.1.6/src/pyagentkit/__init__.py +40 -0
- pyagentkit-0.1.6/src/pyagentkit/agent.py +814 -0
- pyagentkit-0.1.6/src/pyagentkit/async_agent.py +855 -0
- pyagentkit-0.1.6/src/pyagentkit/definitions.py +114 -0
- pyagentkit-0.1.6/src/pyagentkit/exceptions.py +75 -0
- pyagentkit-0.1.4/PKG-INFO +0 -87
- pyagentkit-0.1.4/README.md +0 -67
- pyagentkit-0.1.4/USAGE.md +0 -218
- pyagentkit-0.1.4/src/pyagentkit/__init__.py +0 -33
- pyagentkit-0.1.4/src/pyagentkit/agent.py +0 -487
- pyagentkit-0.1.4/src/pyagentkit/definitions.py +0 -90
- pyagentkit-0.1.4/src/pyagentkit/exceptions.py +0 -30
- pyagentkit-0.1.4/src/pyagentkit/helpers.py +0 -16
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/.github/workflows/publish.yml +0 -0
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/.github/workflows/python-publish.yml +0 -0
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/.gitignore +0 -0
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/.python-version +0 -0
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/LICENSE +0 -0
- {pyagentkit-0.1.4 → pyagentkit-0.1.6}/uv.lock +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyagentkit
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: Agent toolkit for Ollama
|
|
5
|
+
Project-URL: Homepage, https://github.com/TarikEren/pyagentkit
|
|
6
|
+
Project-URL: Issues, https://github.com/TarikEren/pyagentkit/issues
|
|
7
|
+
Author-email: Tarık Eren Tosun <tarikerentosun@outlook.com>
|
|
8
|
+
Maintainer-email: Tarık Eren Tosun <tarikerentosun@outlook.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,agentic,ai,ollama
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: build>=1.4.2
|
|
16
|
+
Requires-Dist: ollama>=0.6.1
|
|
17
|
+
Requires-Dist: pydantic>=2.12.5
|
|
18
|
+
Requires-Dist: twine>=6.2.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# PyAgentKit
|
|
22
|
+
|
|
23
|
+
A Python library for building tool-calling agents on top of locally-hosted LLMs via [Ollama](https://ollama.com). PyAgentKit gives models that lack native function-calling support the ability to call tools through structured JSON output, retries, dependency injection, and lifecycle hooks.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Sync and async agents** — `Agent` for synchronous use, `AsyncAgent` for `asyncio`-based workflows
|
|
30
|
+
- **Tool registration** — attach tools at the class level (shared across all instances) or at the instance level (per-agent)
|
|
31
|
+
- **Structured JSON responses** — agents respond in a validated, discriminated-union schema (`final` or `tool_call`)
|
|
32
|
+
- **Pydantic response models** — extend `AgentResponse` to add your own typed fields to every response
|
|
33
|
+
- **Dependency injection** — pass runtime dependencies (database connections, config, etc.) through `AgentDependencies` into tools without polluting tool signatures
|
|
34
|
+
- **Retry logic** — configurable retry budgets separately for tool calls and for response validation failures
|
|
35
|
+
- **Approval gates** — optionally require human confirmation before any tool is executed
|
|
36
|
+
- **Lifecycle hooks** — callbacks for tool calls, retries, successes, and final responses
|
|
37
|
+
- **Agent composition** — expose any agent as a tool that another agent can call via `.as_tool()`
|
|
38
|
+
- **Token usage tracking** — cumulative `TokenUsage` object updated after every LLM call
|
|
39
|
+
- **Message history** — persistent within a session, with optional trimming, save, and load
|
|
40
|
+
- **Thinking support** — pass `think=True` to enable chain-of-thought reasoning on supported models
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.12+
|
|
47
|
+
- [Ollama](https://ollama.com) running locally (or at a reachable URL)
|
|
48
|
+
- A model pulled in Ollama (e.g. `ollama pull llama3.2`)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install pyagentkit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Project Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/pyagentkit/
|
|
64
|
+
├── agent.py # Synchronous Agent class
|
|
65
|
+
├── async_agent.py # Asynchronous AsyncAgent class
|
|
66
|
+
├── definitions.py # Pydantic models, type aliases, enums
|
|
67
|
+
└── exceptions.py # Exception hierarchy
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Core Concepts
|
|
73
|
+
|
|
74
|
+
### Response Schema
|
|
75
|
+
|
|
76
|
+
Every agent responds in one of two JSON shapes, validated via Pydantic:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
// Tool call
|
|
80
|
+
{
|
|
81
|
+
"response": {
|
|
82
|
+
"type": "tool_call",
|
|
83
|
+
"tool_call": { "name": "my_tool", "params": [{ "name": "x", "value": "42" }] }
|
|
84
|
+
},
|
|
85
|
+
"message": "Calling my_tool to get the result"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Final answer
|
|
89
|
+
{
|
|
90
|
+
"response": { "type": "final" },
|
|
91
|
+
"message": "The answer is 42"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Exception Hierarchy
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
PyAgentKitError
|
|
99
|
+
├── ExceptionAgentError # Recoverable response failure (triggers retry)
|
|
100
|
+
├── ExceptionAgentFatal # Irrecoverable response failure
|
|
101
|
+
├── ExceptionToolError # Recoverable tool failure (triggers retry)
|
|
102
|
+
├── ExceptionToolFatal # Irrecoverable tool failure
|
|
103
|
+
├── ExceptionToolRetriesExhausted
|
|
104
|
+
├── ExceptionResponseRetriesExhausted
|
|
105
|
+
├── ExceptionEnvironmentError # Ollama unreachable or model not found
|
|
106
|
+
├── ExceptionInvalidTool # Tool missing docstring or malformed
|
|
107
|
+
└── ExceptionFatalError # Wraps any fatal agent or tool exception
|
|
108
|
+
ExceptionUnhandledError # Unhandled runtime exception (not a PyAgentKitError)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
APACHE 2.0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# PyAgentKit
|
|
2
|
+
|
|
3
|
+
A Python library for building tool-calling agents on top of locally-hosted LLMs via [Ollama](https://ollama.com). PyAgentKit gives models that lack native function-calling support the ability to call tools through structured JSON output, retries, dependency injection, and lifecycle hooks.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Sync and async agents** — `Agent` for synchronous use, `AsyncAgent` for `asyncio`-based workflows
|
|
10
|
+
- **Tool registration** — attach tools at the class level (shared across all instances) or at the instance level (per-agent)
|
|
11
|
+
- **Structured JSON responses** — agents respond in a validated, discriminated-union schema (`final` or `tool_call`)
|
|
12
|
+
- **Pydantic response models** — extend `AgentResponse` to add your own typed fields to every response
|
|
13
|
+
- **Dependency injection** — pass runtime dependencies (database connections, config, etc.) through `AgentDependencies` into tools without polluting tool signatures
|
|
14
|
+
- **Retry logic** — configurable retry budgets separately for tool calls and for response validation failures
|
|
15
|
+
- **Approval gates** — optionally require human confirmation before any tool is executed
|
|
16
|
+
- **Lifecycle hooks** — callbacks for tool calls, retries, successes, and final responses
|
|
17
|
+
- **Agent composition** — expose any agent as a tool that another agent can call via `.as_tool()`
|
|
18
|
+
- **Token usage tracking** — cumulative `TokenUsage` object updated after every LLM call
|
|
19
|
+
- **Message history** — persistent within a session, with optional trimming, save, and load
|
|
20
|
+
- **Thinking support** — pass `think=True` to enable chain-of-thought reasoning on supported models
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Python 3.12+
|
|
27
|
+
- [Ollama](https://ollama.com) running locally (or at a reachable URL)
|
|
28
|
+
- A model pulled in Ollama (e.g. `ollama pull llama3.2`)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install pyagentkit
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Project Structure
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
src/pyagentkit/
|
|
44
|
+
├── agent.py # Synchronous Agent class
|
|
45
|
+
├── async_agent.py # Asynchronous AsyncAgent class
|
|
46
|
+
├── definitions.py # Pydantic models, type aliases, enums
|
|
47
|
+
└── exceptions.py # Exception hierarchy
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Core Concepts
|
|
53
|
+
|
|
54
|
+
### Response Schema
|
|
55
|
+
|
|
56
|
+
Every agent responds in one of two JSON shapes, validated via Pydantic:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
// Tool call
|
|
60
|
+
{
|
|
61
|
+
"response": {
|
|
62
|
+
"type": "tool_call",
|
|
63
|
+
"tool_call": { "name": "my_tool", "params": [{ "name": "x", "value": "42" }] }
|
|
64
|
+
},
|
|
65
|
+
"message": "Calling my_tool to get the result"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Final answer
|
|
69
|
+
{
|
|
70
|
+
"response": { "type": "final" },
|
|
71
|
+
"message": "The answer is 42"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Exception Hierarchy
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
PyAgentKitError
|
|
79
|
+
├── ExceptionAgentError # Recoverable response failure (triggers retry)
|
|
80
|
+
├── ExceptionAgentFatal # Irrecoverable response failure
|
|
81
|
+
├── ExceptionToolError # Recoverable tool failure (triggers retry)
|
|
82
|
+
├── ExceptionToolFatal # Irrecoverable tool failure
|
|
83
|
+
├── ExceptionToolRetriesExhausted
|
|
84
|
+
├── ExceptionResponseRetriesExhausted
|
|
85
|
+
├── ExceptionEnvironmentError # Ollama unreachable or model not found
|
|
86
|
+
├── ExceptionInvalidTool # Tool missing docstring or malformed
|
|
87
|
+
└── ExceptionFatalError # Wraps any fatal agent or tool exception
|
|
88
|
+
ExceptionUnhandledError # Unhandled runtime exception (not a PyAgentKitError)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
APACHE 2.0
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# PyAgentKit — Usage Guide
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Basic Agent](#1-basic-agent)
|
|
8
|
+
2. [Writing Tools](#2-writing-tools)
|
|
9
|
+
3. [Instance vs Class Tools](#3-instance-vs-class-tools)
|
|
10
|
+
4. [Dependencies](#4-dependencies)
|
|
11
|
+
5. [Custom Response Models](#5-custom-response-models)
|
|
12
|
+
6. [Async Agent](#6-async-agent)
|
|
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)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Basic Agent
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pyagentkit import Agent
|
|
26
|
+
|
|
27
|
+
agent = Agent(
|
|
28
|
+
llm_name="llama3.2", # Must be pulled in Ollama
|
|
29
|
+
system_prompt="You are a helpful assistant.",
|
|
30
|
+
agent_name="my-agent",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
response = agent.handle_response("What is 2 + 2?")
|
|
34
|
+
print(response.message) # "The answer is 4"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`handle_response` blocks until the agent produces a `final` response or exhausts its retries.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2. Writing Tools
|
|
42
|
+
|
|
43
|
+
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
|
+
```python
|
|
46
|
+
from pyagentkit import Agent
|
|
47
|
+
from pyagentkit.definitions import ToolResult, ToolReturnValue
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def add_numbers(a: int, b: int) -> ToolResult:
|
|
51
|
+
"""Add two integers together and return their sum."""
|
|
52
|
+
total = a + b
|
|
53
|
+
return ToolResult(return_value=ToolReturnValue.success, content=str(total))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def divide(a: float, b: float) -> ToolResult:
|
|
57
|
+
"""Divide a by b. Returns an error if b is zero."""
|
|
58
|
+
if b == 0:
|
|
59
|
+
return ToolResult(return_value=ToolReturnValue.error, content="Cannot divide by zero")
|
|
60
|
+
return ToolResult(return_value=ToolReturnValue.success, content=str(a / b))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### ToolReturnValue options
|
|
64
|
+
|
|
65
|
+
| Value | Effect |
|
|
66
|
+
|-------|--------|
|
|
67
|
+
| `success` | Result appended to history; agent continues |
|
|
68
|
+
| `error` | Error message sent back to agent; counts as a tool retry |
|
|
69
|
+
| `fatal` | Immediately raises `ExceptionFatalError`; no retry |
|
|
70
|
+
|
|
71
|
+
### Passing tools to an agent
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
agent = Agent(
|
|
75
|
+
llm_name="llama3.2",
|
|
76
|
+
tools=[add_numbers, divide], # registered without approval requirement
|
|
77
|
+
)
|
|
78
|
+
```
|
|
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`:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
agent.add_tool(add_numbers, requires_approval=False)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 3. Instance vs Class Tools
|
|
89
|
+
|
|
90
|
+
### Instance tools
|
|
91
|
+
|
|
92
|
+
Registered on a single agent instance. Use `add_tool` or pass `tools=` to the constructor.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
agent = Agent(llm_name="llama3.2")
|
|
96
|
+
agent.add_tool(my_tool, requires_approval=False)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Class tools
|
|
100
|
+
|
|
101
|
+
Registered on the class and shared across **all instances** of that subclass.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
class MyAgent(Agent):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@MyAgent.register_tool(requires_approval=False)
|
|
109
|
+
def get_time() -> ToolResult:
|
|
110
|
+
"""Return the current UTC time as an ISO string."""
|
|
111
|
+
from datetime import datetime, timezone
|
|
112
|
+
return ToolResult(
|
|
113
|
+
return_value=ToolReturnValue.success,
|
|
114
|
+
content=datetime.now(timezone.utc).isoformat(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
agent_a = MyAgent(llm_name="llama3.2")
|
|
119
|
+
agent_b = MyAgent(llm_name="llama3.2", agent_name="second")
|
|
120
|
+
# Both agents can call get_time
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 4. Dependencies
|
|
126
|
+
|
|
127
|
+
Use `AgentDependencies` to inject runtime state (database handles, API clients, config) into tools without exposing them in the tool's public signature shown to the LLM.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pydantic import Field
|
|
131
|
+
from pyagentkit.definitions import AgentDependencies, ToolResult, ToolReturnValue
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MyDeps(AgentDependencies):
|
|
135
|
+
db_url: str = Field(description="Database connection URL")
|
|
136
|
+
api_key: str = Field(description="External API key")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def fetch_user(deps: MyDeps, user_id: str) -> ToolResult:
|
|
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
|
|
143
|
+
result = f"User {user_id} from {deps.db_url}"
|
|
144
|
+
return ToolResult(return_value=ToolReturnValue.success, content=result)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
deps = MyDeps(prompt="Find user 42", db_url="postgresql://...", api_key="sk-...")
|
|
148
|
+
|
|
149
|
+
agent = Agent(llm_name="llama3.2", tools=[fetch_user])
|
|
150
|
+
response = agent.handle_response("Find user 42", deps=deps)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The `deps` first parameter is detected automatically by inspecting whether the first parameter is a subclass of `AgentDependencies`. It is stripped from the tool signature shown to the LLM.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 5. Custom Response Models
|
|
158
|
+
|
|
159
|
+
Extend `AgentResponse` to add typed fields that the agent must populate on every response.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from pydantic import Field
|
|
163
|
+
from pyagentkit.definitions import AgentResponse
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SentimentResponse(AgentResponse):
|
|
167
|
+
sentiment: str = Field(description="positive, negative, or neutral")
|
|
168
|
+
confidence: float = Field(description="Confidence score between 0 and 1")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
agent = Agent(
|
|
172
|
+
llm_name="llama3.2",
|
|
173
|
+
response_model=SentimentResponse,
|
|
174
|
+
system_prompt="You are a sentiment analysis assistant.",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
response = agent.handle_response("I love this product!")
|
|
178
|
+
print(response.sentiment) # "positive"
|
|
179
|
+
print(response.confidence) # 0.95
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The schema examples injected into the system prompt are generated automatically from the model's field annotations, so the LLM always sees the correct shape.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 6. Async Agent
|
|
187
|
+
|
|
188
|
+
Use `AsyncAgent` in `asyncio` contexts. The API mirrors `Agent` except all relevant methods are coroutines and the client is `ollama.AsyncClient`.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import asyncio
|
|
192
|
+
from pyagentkit import AsyncAgent
|
|
193
|
+
from pyagentkit.definitions import ToolResult, ToolReturnValue
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def get_weather(city: str) -> ToolResult:
|
|
197
|
+
"""Return current weather for a given city name."""
|
|
198
|
+
# ... call a weather API ...
|
|
199
|
+
return ToolResult(return_value=ToolReturnValue.success, content=f"Sunny in {city}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def main():
|
|
203
|
+
agent = await AsyncAgent.create(
|
|
204
|
+
llm_name="llama3.2",
|
|
205
|
+
tools=[get_weather],
|
|
206
|
+
agent_name="weather-agent",
|
|
207
|
+
)
|
|
208
|
+
response = await agent.handle_response("What is the weather in Istanbul?")
|
|
209
|
+
print(response.message)
|
|
210
|
+
agent.dispose()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
asyncio.run(main())
|
|
214
|
+
```
|
|
215
|
+
|
|
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`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 7. Lifecycle Hooks
|
|
221
|
+
|
|
222
|
+
Hooks let you observe and log what the agent is doing without modifying its core logic.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from pyagentkit.definitions import AgentResponse
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def on_tool_call(tool_name: str, params: dict) -> None:
|
|
229
|
+
print(f"[CALL] {tool_name} | params={params}")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def on_tool_retry(tool_name: str, params: dict, error: str) -> None:
|
|
233
|
+
print(f"[RETRY] {tool_name} | error={error}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def on_tool_success(tool_name: str, params: dict) -> None:
|
|
237
|
+
print(f"[SUCCESS] {tool_name}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def on_response(response: AgentResponse) -> None:
|
|
241
|
+
print(f"[FINAL] {response.message}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def on_response_retry(attempt: int, response_str: str, error: str) -> None:
|
|
245
|
+
print(f"[RESP RETRY] attempt={attempt} | error={error}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
agent = Agent(
|
|
249
|
+
llm_name="llama3.2",
|
|
250
|
+
on_tool_call=on_tool_call,
|
|
251
|
+
on_tool_retry=on_tool_retry,
|
|
252
|
+
on_tool_success=on_tool_success,
|
|
253
|
+
on_response=on_response,
|
|
254
|
+
on_response_retry=on_response_retry,
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 8. Agent Composition
|
|
261
|
+
|
|
262
|
+
Any agent can expose itself as a tool for another agent via `.as_tool()`.
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from pyagentkit import Agent
|
|
266
|
+
from pyagentkit.definitions import ToolResult, ToolReturnValue
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# Inner specialist agent
|
|
270
|
+
researcher = Agent(
|
|
271
|
+
llm_name="llama3.2",
|
|
272
|
+
agent_name="researcher",
|
|
273
|
+
system_prompt="You are a research specialist. Answer factual questions concisely.",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Outer orchestrator agent
|
|
277
|
+
orchestrator = Agent(
|
|
278
|
+
llm_name="llama3.2",
|
|
279
|
+
agent_name="orchestrator",
|
|
280
|
+
system_prompt="You are an orchestrator. Delegate research tasks to the researcher agent.",
|
|
281
|
+
tools=[researcher.as_tool(description="Ask the researcher agent a factual question.")],
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
response = orchestrator.handle_response("What is the capital of Japan?")
|
|
285
|
+
print(response.message)
|
|
286
|
+
```
|
|
287
|
+
|
|
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.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 9. Message History
|
|
293
|
+
|
|
294
|
+
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
|
+
|
|
296
|
+
```python
|
|
297
|
+
agent = Agent(llm_name="llama3.2", agent_name="chat-agent")
|
|
298
|
+
|
|
299
|
+
r1 = agent.handle_response("My name is Alice.")
|
|
300
|
+
r2 = agent.handle_response("What is my name?")
|
|
301
|
+
print(r2.message) # "Your name is Alice."
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Trimming history
|
|
305
|
+
|
|
306
|
+
Set `max_history` to limit how many non-system messages the agent sees. Older messages are dropped automatically. Orphaned assistant messages (those without a preceding user message after trimming) are also removed.
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
agent = Agent(llm_name="llama3.2", max_history=20)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Saving and loading history
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
agent.save_history("history.json")
|
|
316
|
+
|
|
317
|
+
# Later, in a new session:
|
|
318
|
+
agent.load_history("history.json")
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Clearing history
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
agent.clear_history()
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 10. Token Usage
|
|
330
|
+
|
|
331
|
+
`agent.token_usage` is a `TokenUsage` object that accumulates across all `handle_response` calls on an instance.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
response = agent.handle_response("Summarize the water cycle.")
|
|
335
|
+
print(agent.token_usage.prompt_tokens)
|
|
336
|
+
print(agent.token_usage.response_tokens)
|
|
337
|
+
print(agent.token_usage.total_tokens)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 11. Error Handling
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from pyagentkit.exceptions import (
|
|
346
|
+
ExceptionToolRetriesExhausted,
|
|
347
|
+
ExceptionResponseRetriesExhausted,
|
|
348
|
+
ExceptionFatalError,
|
|
349
|
+
ExceptionEnvironmentError,
|
|
350
|
+
ExceptionUnhandledError,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
response = agent.handle_response("Do something complex.")
|
|
355
|
+
except ExceptionEnvironmentError as e:
|
|
356
|
+
print(f"Ollama not reachable or model missing: {e.message}")
|
|
357
|
+
except ExceptionToolRetriesExhausted as e:
|
|
358
|
+
print(f"Tool retries exhausted after {e.retries} attempts")
|
|
359
|
+
except ExceptionResponseRetriesExhausted as e:
|
|
360
|
+
print(f"Response retries exhausted after {e.retries} attempts")
|
|
361
|
+
except ExceptionFatalError as e:
|
|
362
|
+
print(f"Fatal error, cannot recover: {e.message}")
|
|
363
|
+
except ExceptionUnhandledError as e:
|
|
364
|
+
print(f"Unexpected error: {e}")
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Inside a tool, signal errors to the agent using return values rather than raising exceptions directly:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
def risky_tool(path: str) -> ToolResult:
|
|
371
|
+
"""Read a file from the given path."""
|
|
372
|
+
try:
|
|
373
|
+
with open(path) as f:
|
|
374
|
+
return ToolResult(return_value=ToolReturnValue.success, content=f.read())
|
|
375
|
+
except FileNotFoundError:
|
|
376
|
+
# Recoverable — agent will retry with corrected params
|
|
377
|
+
return ToolResult(return_value=ToolReturnValue.error, content=f"File not found: {path}")
|
|
378
|
+
except PermissionError:
|
|
379
|
+
# Irrecoverable — raises ExceptionFatalError immediately
|
|
380
|
+
return ToolResult(return_value=ToolReturnValue.fatal, content=f"Permission denied: {path}")
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 12. Configuration Reference
|
|
386
|
+
|
|
387
|
+
| Parameter | Type | Default | Description |
|
|
388
|
+
|-----------|------|---------|-------------|
|
|
389
|
+
| `llm_name` | `str` | required | Ollama model name (e.g. `"llama3.2"`) |
|
|
390
|
+
| `agent_name` | `str \| None` | `llm_name` | Unique name for this agent instance |
|
|
391
|
+
| `system_prompt` | `str \| None` | `""` | Base system prompt prepended to all requests |
|
|
392
|
+
| `instructions` | `str \| None` | `""` | Text appended to every user prompt |
|
|
393
|
+
| `response_model` | `Type[AgentResponse]` | `AgentResponse` | Pydantic model for response validation |
|
|
394
|
+
| `tool_retries` | `int` | `3` | Max tool call retries before raising |
|
|
395
|
+
| `response_retries` | `int` | `3` | Max response validation retries before raising |
|
|
396
|
+
| `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 |
|
|
400
|
+
| `ollama_url` | `str \| None` | `None` | Custom Ollama host URL; uses default if `None` |
|
|
401
|
+
| `tools` | `list \| None` | `None` | Tool functions to register on init |
|
|
402
|
+
| `max_history` | `int \| None` | `None` | Max non-system messages to retain |
|
|
403
|
+
| `log_level` | `int` | `logging.INFO` | Python logging level |
|
|
404
|
+
| `think` | `bool` | `False` | Enable chain-of-thought on supported models |
|
|
405
|
+
| `on_tool_call` | callable | `None` | Hook called before each tool execution |
|
|
406
|
+
| `on_tool_retry` | callable | `None` | Hook called on tool error/retry |
|
|
407
|
+
| `on_tool_success` | callable | `None` | Hook called on tool success |
|
|
408
|
+
| `on_response` | callable | `None` | Hook called on final response |
|
|
409
|
+
| `on_response_retry` | callable | `None` | Hook called on response validation failure |
|
|
410
|
+
|
|
411
|
+
### Agent lifecycle
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
agent = Agent(llm_name="llama3.2", 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.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .agent import Agent
|
|
2
|
+
from .async_agent import AsyncAgent
|
|
3
|
+
from .definitions import (
|
|
4
|
+
ToolResult,
|
|
5
|
+
AgentResponse,
|
|
6
|
+
AgentDependencies,
|
|
7
|
+
ToolReturnValue,
|
|
8
|
+
TokenUsage,
|
|
9
|
+
)
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
ExceptionAgentError,
|
|
12
|
+
ExceptionAgentFatal,
|
|
13
|
+
ExceptionToolError,
|
|
14
|
+
ExceptionToolFatal,
|
|
15
|
+
ExceptionFatalError,
|
|
16
|
+
ExceptionEnvironmentError,
|
|
17
|
+
ExceptionToolRetriesExhausted,
|
|
18
|
+
ExceptionResponseRetriesExhausted,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Agents
|
|
23
|
+
"Agent",
|
|
24
|
+
"AsyncAgent",
|
|
25
|
+
# Definitions
|
|
26
|
+
"ToolResult",
|
|
27
|
+
"AgentResponse",
|
|
28
|
+
"AgentDependencies",
|
|
29
|
+
"ToolReturnValue",
|
|
30
|
+
"TokenUsage",
|
|
31
|
+
# Exceptions
|
|
32
|
+
"ExceptionAgentError",
|
|
33
|
+
"ExceptionAgentFatal",
|
|
34
|
+
"ExceptionToolError",
|
|
35
|
+
"ExceptionToolFatal",
|
|
36
|
+
"ExceptionFatalError",
|
|
37
|
+
"ExceptionEnvironmentError",
|
|
38
|
+
"ExceptionToolRetriesExhausted",
|
|
39
|
+
"ExceptionResponseRetriesExhausted",
|
|
40
|
+
]
|