agnt5 0.1.0__cp39-abi3-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agnt5/__init__.py +307 -0
- agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/__pycache__/agent.cpython-311.pyc +0 -0
- agnt5/__pycache__/context.cpython-311.pyc +0 -0
- agnt5/__pycache__/durable.cpython-311.pyc +0 -0
- agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
- agnt5/__pycache__/memory.cpython-311.pyc +0 -0
- agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
- agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
- agnt5/__pycache__/task.cpython-311.pyc +0 -0
- agnt5/__pycache__/tool.cpython-311.pyc +0 -0
- agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
- agnt5/__pycache__/types.cpython-311.pyc +0 -0
- agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/agent.py +1086 -0
- agnt5/context.py +406 -0
- agnt5/durable.py +1050 -0
- agnt5/extraction.py +410 -0
- agnt5/llm/__init__.py +179 -0
- agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
- agnt5/llm/anthropic.py +319 -0
- agnt5/llm/azure.py +348 -0
- agnt5/llm/base.py +315 -0
- agnt5/llm/google.py +373 -0
- agnt5/llm/mistral.py +330 -0
- agnt5/llm/model_registry.py +467 -0
- agnt5/llm/models.json +227 -0
- agnt5/llm/openai.py +334 -0
- agnt5/llm/together.py +377 -0
- agnt5/memory.py +746 -0
- agnt5/reflection.py +514 -0
- agnt5/runtime.py +699 -0
- agnt5/task.py +476 -0
- agnt5/testing.py +451 -0
- agnt5/tool.py +516 -0
- agnt5/tracing.py +624 -0
- agnt5/types.py +210 -0
- agnt5/workflow.py +897 -0
- agnt5-0.1.0.dist-info/METADATA +93 -0
- agnt5-0.1.0.dist-info/RECORD +49 -0
- agnt5-0.1.0.dist-info/WHEEL +4 -0
agnt5/testing.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Testing utilities for the AGNT5 SDK.
|
|
3
|
+
|
|
4
|
+
Provides test helpers, mocks, and fixtures for testing agents,
|
|
5
|
+
tools, and workflows in isolation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union, Callable, AsyncIterator
|
|
9
|
+
import asyncio
|
|
10
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
from .types import (
|
|
17
|
+
Message,
|
|
18
|
+
MessageRole,
|
|
19
|
+
ToolCall,
|
|
20
|
+
ToolResult,
|
|
21
|
+
ExecutionContext,
|
|
22
|
+
ExecutionState,
|
|
23
|
+
)
|
|
24
|
+
from .agent import Agent
|
|
25
|
+
from .tool import Tool
|
|
26
|
+
from .workflow import Workflow
|
|
27
|
+
from .context import Context, create_context
|
|
28
|
+
from .memory import Memory, InMemoryStore
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MockLLM:
|
|
35
|
+
"""
|
|
36
|
+
Mock LLM for testing agents without real API calls.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
```python
|
|
40
|
+
from agnt5.testing import MockLLM, TestAgent
|
|
41
|
+
|
|
42
|
+
# Create mock LLM with predefined responses
|
|
43
|
+
mock_llm = MockLLM(responses=[
|
|
44
|
+
"Hello! How can I help you?",
|
|
45
|
+
ToolCall(name="search", arguments={"query": "weather"}),
|
|
46
|
+
"The weather is sunny today.",
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
# Use in test
|
|
50
|
+
agent = TestAgent("my-agent", llm=mock_llm)
|
|
51
|
+
response = await agent.run("What's the weather?")
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
responses: Optional[List[Union[str, ToolCall, List[ToolCall]]]] = None,
|
|
58
|
+
default_response: str = "Mock response",
|
|
59
|
+
):
|
|
60
|
+
"""Initialize mock LLM."""
|
|
61
|
+
self.responses = responses or []
|
|
62
|
+
self.default_response = default_response
|
|
63
|
+
self.call_count = 0
|
|
64
|
+
self.call_history: List[Dict[str, Any]] = []
|
|
65
|
+
|
|
66
|
+
async def complete(
|
|
67
|
+
self,
|
|
68
|
+
messages: List[Dict[str, Any]],
|
|
69
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
70
|
+
**kwargs,
|
|
71
|
+
) -> Message:
|
|
72
|
+
"""Mock completion method."""
|
|
73
|
+
# Record call
|
|
74
|
+
self.call_history.append({
|
|
75
|
+
"messages": messages,
|
|
76
|
+
"tools": tools,
|
|
77
|
+
"kwargs": kwargs,
|
|
78
|
+
"timestamp": datetime.utcnow(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
# Get response
|
|
82
|
+
if self.call_count < len(self.responses):
|
|
83
|
+
response = self.responses[self.call_count]
|
|
84
|
+
else:
|
|
85
|
+
response = self.default_response
|
|
86
|
+
|
|
87
|
+
self.call_count += 1
|
|
88
|
+
|
|
89
|
+
# Convert response to Message
|
|
90
|
+
if isinstance(response, str):
|
|
91
|
+
return Message(
|
|
92
|
+
role=MessageRole.ASSISTANT,
|
|
93
|
+
content=response,
|
|
94
|
+
)
|
|
95
|
+
elif isinstance(response, ToolCall):
|
|
96
|
+
return Message(
|
|
97
|
+
role=MessageRole.ASSISTANT,
|
|
98
|
+
content="",
|
|
99
|
+
tool_calls=[response],
|
|
100
|
+
)
|
|
101
|
+
elif isinstance(response, list) and all(isinstance(tc, ToolCall) for tc in response):
|
|
102
|
+
return Message(
|
|
103
|
+
role=MessageRole.ASSISTANT,
|
|
104
|
+
content="",
|
|
105
|
+
tool_calls=response,
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
return Message(
|
|
109
|
+
role=MessageRole.ASSISTANT,
|
|
110
|
+
content=str(response),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def reset(self):
|
|
114
|
+
"""Reset the mock."""
|
|
115
|
+
self.call_count = 0
|
|
116
|
+
self.call_history.clear()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestAgent(Agent):
|
|
120
|
+
"""
|
|
121
|
+
Test-friendly agent with mock LLM support.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
```python
|
|
125
|
+
from agnt5.testing import TestAgent
|
|
126
|
+
|
|
127
|
+
# Create test agent
|
|
128
|
+
agent = TestAgent(
|
|
129
|
+
"test-agent",
|
|
130
|
+
responses=["Hello!", "How are you?"],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Test conversation
|
|
134
|
+
response1 = await agent.run("Hi")
|
|
135
|
+
assert response1.content == "Hello!"
|
|
136
|
+
|
|
137
|
+
response2 = await agent.run("What's up?")
|
|
138
|
+
assert response2.content == "How are you?"
|
|
139
|
+
```
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
name: str,
|
|
145
|
+
*,
|
|
146
|
+
llm: Optional[MockLLM] = None,
|
|
147
|
+
responses: Optional[List[Union[str, ToolCall]]] = None,
|
|
148
|
+
**kwargs,
|
|
149
|
+
):
|
|
150
|
+
"""Initialize test agent."""
|
|
151
|
+
super().__init__(name, **kwargs)
|
|
152
|
+
|
|
153
|
+
# Set up mock LLM
|
|
154
|
+
if llm:
|
|
155
|
+
self.llm = llm
|
|
156
|
+
else:
|
|
157
|
+
self.llm = MockLLM(responses=responses)
|
|
158
|
+
|
|
159
|
+
async def _call_llm(
|
|
160
|
+
self,
|
|
161
|
+
messages: List[Dict[str, Any]],
|
|
162
|
+
stream: bool,
|
|
163
|
+
) -> Message:
|
|
164
|
+
"""Override to use mock LLM."""
|
|
165
|
+
return await self.llm.complete(messages, tools=list(self._tools.values()))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class MockTool(Tool):
|
|
169
|
+
"""
|
|
170
|
+
Mock tool for testing.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
```python
|
|
174
|
+
from agnt5.testing import MockTool
|
|
175
|
+
|
|
176
|
+
# Create mock tool
|
|
177
|
+
tool = MockTool(
|
|
178
|
+
name="calculator",
|
|
179
|
+
return_value=42,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Use in tests
|
|
183
|
+
result = await tool.invoke(a=10, b=32)
|
|
184
|
+
assert result == 42
|
|
185
|
+
assert tool.call_count == 1
|
|
186
|
+
assert tool.last_args == {"a": 10, "b": 32}
|
|
187
|
+
```
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
name: str,
|
|
193
|
+
*,
|
|
194
|
+
return_value: Any = None,
|
|
195
|
+
side_effect: Optional[Callable] = None,
|
|
196
|
+
raises: Optional[Exception] = None,
|
|
197
|
+
):
|
|
198
|
+
"""Initialize mock tool."""
|
|
199
|
+
async def mock_func(**kwargs):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
super().__init__(mock_func, name=name)
|
|
203
|
+
|
|
204
|
+
self.return_value = return_value
|
|
205
|
+
self.side_effect = side_effect
|
|
206
|
+
self.raises = raises
|
|
207
|
+
self.call_count = 0
|
|
208
|
+
self.call_history: List[Dict[str, Any]] = []
|
|
209
|
+
self.last_args: Optional[Dict[str, Any]] = None
|
|
210
|
+
|
|
211
|
+
async def invoke(self, **kwargs) -> Any:
|
|
212
|
+
"""Mock invoke method."""
|
|
213
|
+
# Record call
|
|
214
|
+
self.call_count += 1
|
|
215
|
+
self.last_args = kwargs
|
|
216
|
+
self.call_history.append({
|
|
217
|
+
"args": kwargs,
|
|
218
|
+
"timestamp": datetime.utcnow(),
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
# Handle exceptions
|
|
222
|
+
if self.raises:
|
|
223
|
+
raise self.raises
|
|
224
|
+
|
|
225
|
+
# Handle side effect
|
|
226
|
+
if self.side_effect:
|
|
227
|
+
if asyncio.iscoroutinefunction(self.side_effect):
|
|
228
|
+
return await self.side_effect(**kwargs)
|
|
229
|
+
else:
|
|
230
|
+
return self.side_effect(**kwargs)
|
|
231
|
+
|
|
232
|
+
# Return configured value
|
|
233
|
+
return self.return_value
|
|
234
|
+
|
|
235
|
+
def reset(self):
|
|
236
|
+
"""Reset the mock."""
|
|
237
|
+
self.call_count = 0
|
|
238
|
+
self.call_history.clear()
|
|
239
|
+
self.last_args = None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TestWorkflow(Workflow):
|
|
243
|
+
"""
|
|
244
|
+
Test-friendly workflow with step tracking.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
```python
|
|
248
|
+
from agnt5.testing import TestWorkflow
|
|
249
|
+
|
|
250
|
+
class MyWorkflow(TestWorkflow):
|
|
251
|
+
async def run(self, data: str) -> str:
|
|
252
|
+
step1 = await self.step("process", lambda: data.upper())
|
|
253
|
+
step2 = await self.step("validate", lambda: len(step1) > 0)
|
|
254
|
+
return step1 if step2 else ""
|
|
255
|
+
|
|
256
|
+
# Test workflow
|
|
257
|
+
workflow = MyWorkflow("test-workflow")
|
|
258
|
+
result = await workflow.execute("hello")
|
|
259
|
+
|
|
260
|
+
assert result == "HELLO"
|
|
261
|
+
assert workflow.executed_steps == ["process", "validate"]
|
|
262
|
+
assert workflow.get_step_result("process") == "HELLO"
|
|
263
|
+
```
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
def __init__(self, name: str, **kwargs):
|
|
267
|
+
"""Initialize test workflow."""
|
|
268
|
+
super().__init__(name, **kwargs)
|
|
269
|
+
self.executed_steps: List[str] = []
|
|
270
|
+
self.step_errors: Dict[str, Exception] = {}
|
|
271
|
+
|
|
272
|
+
async def step(
|
|
273
|
+
self,
|
|
274
|
+
name: str,
|
|
275
|
+
func: Callable,
|
|
276
|
+
*args,
|
|
277
|
+
**kwargs,
|
|
278
|
+
) -> Any:
|
|
279
|
+
"""Track step execution."""
|
|
280
|
+
self.executed_steps.append(name)
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
return await super().step(name, func, *args, **kwargs)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
self.step_errors[name] = e
|
|
286
|
+
raise
|
|
287
|
+
|
|
288
|
+
def assert_steps_executed(self, expected_steps: List[str]):
|
|
289
|
+
"""Assert that specific steps were executed in order."""
|
|
290
|
+
assert self.executed_steps == expected_steps, \
|
|
291
|
+
f"Expected steps {expected_steps}, but got {self.executed_steps}"
|
|
292
|
+
|
|
293
|
+
def assert_step_succeeded(self, step_name: str):
|
|
294
|
+
"""Assert that a step succeeded."""
|
|
295
|
+
assert step_name in self.executed_steps, \
|
|
296
|
+
f"Step '{step_name}' was not executed"
|
|
297
|
+
assert step_name not in self.step_errors, \
|
|
298
|
+
f"Step '{step_name}' failed with error: {self.step_errors.get(step_name)}"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@asynccontextmanager
|
|
302
|
+
async def test_context(
|
|
303
|
+
name: str = "test",
|
|
304
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
305
|
+
):
|
|
306
|
+
"""
|
|
307
|
+
Context manager for test contexts.
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
```python
|
|
311
|
+
from agnt5.testing import test_context
|
|
312
|
+
|
|
313
|
+
async with test_context("my-test") as ctx:
|
|
314
|
+
ctx.set("user_id", "test-user")
|
|
315
|
+
|
|
316
|
+
agent = Agent("test-agent")
|
|
317
|
+
response = await agent.run("Hello")
|
|
318
|
+
```
|
|
319
|
+
"""
|
|
320
|
+
async with create_context(name, metadata) as ctx:
|
|
321
|
+
# Set up test context
|
|
322
|
+
ctx.metadata["test"] = True
|
|
323
|
+
ctx.metadata["test_started_at"] = datetime.utcnow()
|
|
324
|
+
|
|
325
|
+
yield ctx
|
|
326
|
+
|
|
327
|
+
# Clean up
|
|
328
|
+
ctx.metadata["test_completed_at"] = datetime.utcnow()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class MemoryFixture:
|
|
332
|
+
"""
|
|
333
|
+
Test fixture for memory operations.
|
|
334
|
+
|
|
335
|
+
Example:
|
|
336
|
+
```python
|
|
337
|
+
from agnt5.testing import MemoryFixture
|
|
338
|
+
|
|
339
|
+
# Create fixture with test data
|
|
340
|
+
memory_fixture = MemoryFixture()
|
|
341
|
+
memory = await memory_fixture.with_entries([
|
|
342
|
+
"User's name is Alice",
|
|
343
|
+
"User likes Python",
|
|
344
|
+
{"type": "preference", "value": "dark_mode"},
|
|
345
|
+
])
|
|
346
|
+
|
|
347
|
+
# Use in tests
|
|
348
|
+
results = await memory.search("name")
|
|
349
|
+
assert len(results) == 1
|
|
350
|
+
assert "Alice" in results[0].content
|
|
351
|
+
```
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def __init__(self):
|
|
355
|
+
"""Initialize memory fixture."""
|
|
356
|
+
self.memory = Memory(store=InMemoryStore())
|
|
357
|
+
|
|
358
|
+
async def with_entries(self, entries: List[Any]) -> Memory:
|
|
359
|
+
"""Create memory with predefined entries."""
|
|
360
|
+
for entry in entries:
|
|
361
|
+
await self.memory.add(entry)
|
|
362
|
+
return self.memory
|
|
363
|
+
|
|
364
|
+
async def with_messages(self, messages: List[Union[str, Message]]) -> Memory:
|
|
365
|
+
"""Create memory with message history."""
|
|
366
|
+
for msg in messages:
|
|
367
|
+
if isinstance(msg, str):
|
|
368
|
+
msg = Message(
|
|
369
|
+
role=MessageRole.USER,
|
|
370
|
+
content=msg,
|
|
371
|
+
)
|
|
372
|
+
await self.memory.add(msg)
|
|
373
|
+
return self.memory
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def assert_tool_called(tool: Union[Tool, MockTool], times: int = 1):
|
|
377
|
+
"""
|
|
378
|
+
Assert that a tool was called a specific number of times.
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
```python
|
|
382
|
+
from agnt5.testing import MockTool, assert_tool_called
|
|
383
|
+
|
|
384
|
+
tool = MockTool("search", return_value="results")
|
|
385
|
+
await tool.invoke(query="test")
|
|
386
|
+
|
|
387
|
+
assert_tool_called(tool, times=1)
|
|
388
|
+
```
|
|
389
|
+
"""
|
|
390
|
+
if isinstance(tool, MockTool):
|
|
391
|
+
assert tool.call_count == times, \
|
|
392
|
+
f"Expected tool '{tool.name}' to be called {times} times, but was called {tool.call_count} times"
|
|
393
|
+
else:
|
|
394
|
+
logger.warning("assert_tool_called only works with MockTool instances")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def assert_message_content(message: Message, expected_content: str):
|
|
398
|
+
"""
|
|
399
|
+
Assert that a message contains expected content.
|
|
400
|
+
|
|
401
|
+
Example:
|
|
402
|
+
```python
|
|
403
|
+
from agnt5.testing import assert_message_content
|
|
404
|
+
|
|
405
|
+
message = Message(role=MessageRole.ASSISTANT, content="Hello, world!")
|
|
406
|
+
assert_message_content(message, "Hello")
|
|
407
|
+
```
|
|
408
|
+
"""
|
|
409
|
+
assert expected_content in message.content, \
|
|
410
|
+
f"Expected message to contain '{expected_content}', but got '{message.content}'"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
async def run_until_complete(
|
|
414
|
+
agent: Agent,
|
|
415
|
+
max_turns: int = 10,
|
|
416
|
+
stop_on: Optional[Callable[[Message], bool]] = None,
|
|
417
|
+
) -> List[Message]:
|
|
418
|
+
"""
|
|
419
|
+
Run agent conversation until completion or max turns.
|
|
420
|
+
|
|
421
|
+
Example:
|
|
422
|
+
```python
|
|
423
|
+
from agnt5.testing import run_until_complete
|
|
424
|
+
|
|
425
|
+
agent = TestAgent("assistant")
|
|
426
|
+
|
|
427
|
+
# Run until agent says "goodbye"
|
|
428
|
+
messages = await run_until_complete(
|
|
429
|
+
agent,
|
|
430
|
+
stop_on=lambda msg: "goodbye" in msg.content.lower()
|
|
431
|
+
)
|
|
432
|
+
```
|
|
433
|
+
"""
|
|
434
|
+
messages = []
|
|
435
|
+
|
|
436
|
+
for i in range(max_turns):
|
|
437
|
+
# Create user message
|
|
438
|
+
user_msg = Message(
|
|
439
|
+
role=MessageRole.USER,
|
|
440
|
+
content=f"Message {i+1}",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Get agent response
|
|
444
|
+
response = await agent.run(user_msg)
|
|
445
|
+
messages.append(response)
|
|
446
|
+
|
|
447
|
+
# Check stop condition
|
|
448
|
+
if stop_on and stop_on(response):
|
|
449
|
+
break
|
|
450
|
+
|
|
451
|
+
return messages
|