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.
Files changed (49) hide show
  1. agnt5/__init__.py +307 -0
  2. agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
  3. agnt5/__pycache__/agent.cpython-311.pyc +0 -0
  4. agnt5/__pycache__/context.cpython-311.pyc +0 -0
  5. agnt5/__pycache__/durable.cpython-311.pyc +0 -0
  6. agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
  7. agnt5/__pycache__/memory.cpython-311.pyc +0 -0
  8. agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
  9. agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
  10. agnt5/__pycache__/task.cpython-311.pyc +0 -0
  11. agnt5/__pycache__/tool.cpython-311.pyc +0 -0
  12. agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
  13. agnt5/__pycache__/types.cpython-311.pyc +0 -0
  14. agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
  15. agnt5/_core.abi3.so +0 -0
  16. agnt5/agent.py +1086 -0
  17. agnt5/context.py +406 -0
  18. agnt5/durable.py +1050 -0
  19. agnt5/extraction.py +410 -0
  20. agnt5/llm/__init__.py +179 -0
  21. agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
  22. agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
  23. agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
  24. agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
  25. agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
  26. agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
  27. agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
  28. agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
  29. agnt5/llm/anthropic.py +319 -0
  30. agnt5/llm/azure.py +348 -0
  31. agnt5/llm/base.py +315 -0
  32. agnt5/llm/google.py +373 -0
  33. agnt5/llm/mistral.py +330 -0
  34. agnt5/llm/model_registry.py +467 -0
  35. agnt5/llm/models.json +227 -0
  36. agnt5/llm/openai.py +334 -0
  37. agnt5/llm/together.py +377 -0
  38. agnt5/memory.py +746 -0
  39. agnt5/reflection.py +514 -0
  40. agnt5/runtime.py +699 -0
  41. agnt5/task.py +476 -0
  42. agnt5/testing.py +451 -0
  43. agnt5/tool.py +516 -0
  44. agnt5/tracing.py +624 -0
  45. agnt5/types.py +210 -0
  46. agnt5/workflow.py +897 -0
  47. agnt5-0.1.0.dist-info/METADATA +93 -0
  48. agnt5-0.1.0.dist-info/RECORD +49 -0
  49. 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