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/tool.py ADDED
@@ -0,0 +1,516 @@
1
+ """
2
+ Tool implementation for the AGNT5 SDK.
3
+
4
+ Tools are functions that agents can call to perform specific tasks.
5
+ They support automatic schema generation, validation, and error handling.
6
+ """
7
+
8
+ from typing import Callable, Any, Dict, Optional, List, Union, get_type_hints
9
+ import inspect
10
+ import asyncio
11
+ import functools
12
+ import json
13
+ from dataclasses import dataclass
14
+ import logging
15
+ from datetime import datetime
16
+
17
+ from .types import ToolConfig
18
+ from .context import get_context
19
+ from .durable import durable, DurableContext
20
+
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class Tool:
26
+ """
27
+ A tool that can be called by agents.
28
+
29
+ Example:
30
+ ```python
31
+ from agnt5 import Tool
32
+
33
+ # Create a tool from a function
34
+ def calculate_sum(a: int, b: int) -> int:
35
+ '''Calculate the sum of two numbers.'''
36
+ return a + b
37
+
38
+ tool = Tool(calculate_sum)
39
+
40
+ # Or use the decorator
41
+ @tool
42
+ def search_web(query: str) -> str:
43
+ '''Search the web for information.'''
44
+ return f"Results for: {query}"
45
+ ```
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ func: Callable,
51
+ *,
52
+ name: Optional[str] = None,
53
+ description: Optional[str] = None,
54
+ config: Optional[ToolConfig] = None,
55
+ ):
56
+ """Initialize a Tool."""
57
+ self.func = func
58
+ self.is_async = inspect.iscoroutinefunction(func)
59
+
60
+ # Extract metadata from function
61
+ self.name = name or func.__name__
62
+ self.description = description or inspect.getdoc(func) or f"Tool: {self.name}"
63
+
64
+ # Extract parameters schema
65
+ self.parameters = self._extract_parameters()
66
+ self.returns = self._extract_returns()
67
+
68
+ # Configuration
69
+ if config:
70
+ self.config = config
71
+ else:
72
+ self.config = ToolConfig(
73
+ name=self.name,
74
+ description=self.description,
75
+ parameters=self.parameters,
76
+ returns=self.returns,
77
+ )
78
+
79
+ def _extract_parameters(self) -> Dict[str, Any]:
80
+ """Extract parameter schema from function signature."""
81
+ sig = inspect.signature(self.func)
82
+ hints = get_type_hints(self.func)
83
+
84
+ properties = {}
85
+ required = []
86
+
87
+ for param_name, param in sig.parameters.items():
88
+ if param_name == "self":
89
+ continue
90
+
91
+ # Get type hint
92
+ param_type = hints.get(param_name, Any)
93
+
94
+ # Convert to JSON schema type
95
+ schema_type = self._python_type_to_json_schema(param_type)
96
+ properties[param_name] = schema_type
97
+
98
+ # Check if required
99
+ if param.default == param.empty:
100
+ required.append(param_name)
101
+
102
+ return {
103
+ "type": "object",
104
+ "properties": properties,
105
+ "required": required,
106
+ }
107
+
108
+ def _extract_returns(self) -> Optional[Dict[str, Any]]:
109
+ """Extract return type schema."""
110
+ hints = get_type_hints(self.func)
111
+ return_type = hints.get("return", Any)
112
+
113
+ if return_type is None or return_type is type(None):
114
+ return None
115
+
116
+ return self._python_type_to_json_schema(return_type)
117
+
118
+ def _python_type_to_json_schema(self, python_type: type) -> Dict[str, Any]:
119
+ """Convert Python type to JSON schema."""
120
+ # Handle basic types
121
+ type_mapping = {
122
+ str: {"type": "string"},
123
+ int: {"type": "integer"},
124
+ float: {"type": "number"},
125
+ bool: {"type": "boolean"},
126
+ list: {"type": "array"},
127
+ dict: {"type": "object"},
128
+ Any: {"type": "any"},
129
+ }
130
+
131
+ # Handle Optional types
132
+ origin = getattr(python_type, "__origin__", None)
133
+ if origin is Union:
134
+ args = python_type.__args__
135
+ # Check if it's Optional (Union with None)
136
+ if len(args) == 2 and type(None) in args:
137
+ non_none_type = args[0] if args[1] is type(None) else args[1]
138
+ schema = self._python_type_to_json_schema(non_none_type)
139
+ schema["nullable"] = True
140
+ return schema
141
+
142
+ # Handle List types
143
+ if origin is list:
144
+ args = getattr(python_type, "__args__", ())
145
+ if args:
146
+ item_type = args[0]
147
+ return {
148
+ "type": "array",
149
+ "items": self._python_type_to_json_schema(item_type),
150
+ }
151
+
152
+ # Handle Dict types
153
+ if origin is dict:
154
+ return {"type": "object"}
155
+
156
+ # Default mapping
157
+ return type_mapping.get(python_type, {"type": "any"})
158
+
159
+ async def invoke(self, **kwargs) -> Any:
160
+ """
161
+ Invoke the tool with the given arguments.
162
+
163
+ This method provides automatic durability, retries, and state management.
164
+
165
+ Args:
166
+ **kwargs: Tool arguments
167
+
168
+ Returns:
169
+ Tool output
170
+ """
171
+ # Check if this is a durable tool (has durability enabled)
172
+ if getattr(self.config, 'enable_durability', True):
173
+ return await self._invoke_durable(kwargs)
174
+ else:
175
+ return await self._invoke_legacy(kwargs)
176
+
177
+ async def invoke_durable(self, ctx: DurableContext, **kwargs) -> Any:
178
+ """
179
+ Invoke the tool with a durable context for enhanced state management.
180
+
181
+ Args:
182
+ ctx: Durable execution context
183
+ **kwargs: Tool arguments
184
+
185
+ Returns:
186
+ Tool output
187
+ """
188
+ # Store tool invocation in durable state
189
+ await ctx.state.set(f"tool_{self.name}_input", kwargs)
190
+ await ctx.state.set(f"tool_{self.name}_started_at", datetime.now().isoformat())
191
+
192
+ try:
193
+ result = await self._invoke_durable(kwargs, ctx)
194
+ await ctx.state.set(f"tool_{self.name}_result", result)
195
+ await ctx.state.set(f"tool_{self.name}_completed_at", datetime.now().isoformat())
196
+ return result
197
+ except Exception as e:
198
+ await ctx.state.set(f"tool_{self.name}_error", str(e))
199
+ await ctx.state.set(f"tool_{self.name}_failed_at", datetime.now().isoformat())
200
+ raise
201
+
202
+ async def _invoke_durable(self, kwargs: Dict[str, Any], ctx: Optional[DurableContext] = None) -> Any:
203
+ """
204
+ Execute tool with full durability using durable function.
205
+ """
206
+ @durable.function(
207
+ name=f"tool_{self.name}",
208
+ max_retries=self.config.max_retries,
209
+ timeout=self.config.timeout,
210
+ deterministic=True,
211
+ )
212
+ async def durable_tool_execution(durable_ctx: DurableContext, tool_kwargs: Dict[str, Any]) -> Any:
213
+ # Store execution metadata
214
+ await durable_ctx.state.set("tool_name", self.name)
215
+ await durable_ctx.state.set("tool_description", self.description)
216
+ await durable_ctx.state.set("tool_input", tool_kwargs)
217
+ await durable_ctx.state.set("execution_started", datetime.now().isoformat())
218
+
219
+ # Validate arguments within durable context
220
+ self._validate_arguments(tool_kwargs)
221
+
222
+ # Execute the tool function
223
+ try:
224
+ if self.is_async:
225
+ result = await self.func(**tool_kwargs)
226
+ else:
227
+ # Run sync function in thread pool
228
+ loop = asyncio.get_event_loop()
229
+ result = await loop.run_in_executor(None, lambda: self.func(**tool_kwargs))
230
+
231
+ # Store successful result
232
+ await durable_ctx.state.set("tool_result", result)
233
+ await durable_ctx.state.set("execution_completed", datetime.now().isoformat())
234
+ await durable_ctx.state.set("execution_status", "success")
235
+
236
+ logger.debug(f"Tool '{self.name}' completed successfully")
237
+ return result
238
+
239
+ except Exception as e:
240
+ # Store error information
241
+ await durable_ctx.state.set("tool_error", str(e))
242
+ await durable_ctx.state.set("execution_failed", datetime.now().isoformat())
243
+ await durable_ctx.state.set("execution_status", "failed")
244
+
245
+ logger.error(f"Tool '{self.name}' failed: {e}")
246
+ raise
247
+
248
+ # Execute the durable function
249
+ return await durable_tool_execution(kwargs)
250
+
251
+ async def _invoke_legacy(self, kwargs: Dict[str, Any]) -> Any:
252
+ """
253
+ Legacy invocation method for backward compatibility.
254
+ """
255
+ ctx = get_context()
256
+
257
+ # Validate arguments
258
+ self._validate_arguments(kwargs)
259
+
260
+ # Log invocation
261
+ logger.debug(f"Invoking tool '{self.name}' with args: {kwargs}")
262
+
263
+ try:
264
+ # Execute with timeout if configured
265
+ if self.config.timeout:
266
+ return await self._invoke_with_timeout(kwargs)
267
+ else:
268
+ return await self._invoke_direct(kwargs)
269
+ except Exception as e:
270
+ logger.error(f"Tool '{self.name}' failed: {e}")
271
+
272
+ # Retry if configured
273
+ if self.config.max_retries > 1:
274
+ return await self._invoke_with_retry(kwargs)
275
+ else:
276
+ raise
277
+
278
+ async def _invoke_direct(self, kwargs: Dict[str, Any]) -> Any:
279
+ """Direct invocation of the tool."""
280
+ if self.is_async:
281
+ return await self.func(**kwargs)
282
+ else:
283
+ # Run sync function in thread pool
284
+ loop = asyncio.get_event_loop()
285
+ return await loop.run_in_executor(None, self.func, **kwargs)
286
+
287
+ async def _invoke_with_timeout(self, kwargs: Dict[str, Any]) -> Any:
288
+ """Invoke with timeout."""
289
+ try:
290
+ return await asyncio.wait_for(
291
+ self._invoke_direct(kwargs),
292
+ timeout=self.config.timeout,
293
+ )
294
+ except asyncio.TimeoutError:
295
+ raise TimeoutError(f"Tool '{self.name}' timed out after {self.config.timeout}s")
296
+
297
+ async def _invoke_with_retry(self, kwargs: Dict[str, Any]) -> Any:
298
+ """Invoke with retry logic."""
299
+ last_error = None
300
+
301
+ for attempt in range(self.config.max_retries):
302
+ try:
303
+ return await self._invoke_with_timeout(kwargs)
304
+ except Exception as e:
305
+ last_error = e
306
+ if attempt < self.config.max_retries - 1:
307
+ await asyncio.sleep(self.config.retry_delay * (attempt + 1))
308
+ logger.warning(f"Tool '{self.name}' attempt {attempt + 1} failed, retrying...")
309
+
310
+ raise last_error
311
+
312
+ def _validate_arguments(self, kwargs: Dict[str, Any]) -> None:
313
+ """Validate tool arguments against schema."""
314
+ properties = self.parameters.get("properties", {})
315
+ required = self.parameters.get("required", [])
316
+
317
+ # Check required parameters
318
+ for param in required:
319
+ if param not in kwargs:
320
+ raise ValueError(f"Missing required parameter: {param}")
321
+
322
+ # Check for unknown parameters
323
+ for key in kwargs:
324
+ if key not in properties:
325
+ raise ValueError(f"Unknown parameter: {key}")
326
+
327
+ def get_schema(self) -> Dict[str, Any]:
328
+ """Get OpenAI function schema for this tool."""
329
+ return {
330
+ "type": "function",
331
+ "function": {
332
+ "name": self.name,
333
+ "description": self.description,
334
+ "parameters": self.parameters,
335
+ },
336
+ }
337
+
338
+ def __call__(self, **kwargs) -> Any:
339
+ """Allow synchronous calling of the tool."""
340
+ return asyncio.run(self.invoke(**kwargs))
341
+
342
+ def __repr__(self) -> str:
343
+ durability_status = "durable" if getattr(self.config, 'enable_durability', True) else "non-durable"
344
+ return f"Tool(name='{self.name}', {durability_status}, retries={self.config.max_retries})"
345
+
346
+
347
+ def tool(
348
+ func: Optional[Callable] = None,
349
+ *,
350
+ name: Optional[str] = None,
351
+ description: Optional[str] = None,
352
+ timeout: Optional[float] = None,
353
+ max_retries: int = 3,
354
+ retry_delay: float = 1.0,
355
+ enable_durability: bool = True,
356
+ ) -> Union[Tool, Callable[[Callable], Tool]]:
357
+ """
358
+ Decorator to create a durable tool from a function.
359
+
360
+ Tools created with this decorator automatically get:
361
+ - Durability and state persistence
362
+ - Automatic retries with exponential backoff
363
+ - Timeout handling
364
+ - Comprehensive logging and monitoring
365
+ - Integration with durable flows and contexts
366
+
367
+ Example:
368
+ ```python
369
+ @tool
370
+ def calculate_sum(a: int, b: int) -> int:
371
+ '''Calculate the sum of two numbers.'''
372
+ return a + b
373
+
374
+ @tool(name="web_search", timeout=30.0, max_retries=5)
375
+ async def search(query: str) -> str:
376
+ '''Search the web for information.'''
377
+ # This tool will automatically retry on failures
378
+ # and persist its state across interruptions
379
+ return f"Results for: {query}"
380
+
381
+ @tool(enable_durability=False)
382
+ def simple_tool(value: str) -> str:
383
+ '''A simple tool without durability (for backwards compatibility).'''
384
+ return f"Processed: {value}"
385
+ ```
386
+ """
387
+ def decorator(f: Callable) -> Tool:
388
+ config = ToolConfig(
389
+ name=name or f.__name__,
390
+ description=description or inspect.getdoc(f) or f"Tool: {f.__name__}",
391
+ timeout=timeout,
392
+ max_retries=max_retries,
393
+ retry_delay=retry_delay,
394
+ enable_durability=enable_durability,
395
+ )
396
+
397
+ tool_instance = Tool(f, config=config)
398
+
399
+ # If durability is enabled, register as a durable function
400
+ if enable_durability:
401
+ # Create a durable function wrapper for the tool
402
+ durable_func = durable.function(
403
+ name=f"tool_{config.name}",
404
+ max_retries=max_retries,
405
+ timeout=timeout,
406
+ deterministic=True,
407
+ )(f)
408
+
409
+ # Store reference to durable function in tool
410
+ tool_instance._durable_func = durable_func
411
+
412
+ return tool_instance
413
+
414
+ if func is None:
415
+ # Called with arguments: @tool(name="...", ...)
416
+ return decorator
417
+ else:
418
+ # Called without arguments: @tool
419
+ return decorator(func)
420
+
421
+
422
+ def durable_tool(
423
+ func: Optional[Callable] = None,
424
+ *,
425
+ name: Optional[str] = None,
426
+ description: Optional[str] = None,
427
+ timeout: Optional[float] = None,
428
+ max_retries: int = 3,
429
+ retry_delay: float = 1.0,
430
+ ) -> Union[Tool, Callable[[Callable], Tool]]:
431
+ """
432
+ Explicit decorator for creating durable tools.
433
+
434
+ This is an alias for @tool with enable_durability=True (the default).
435
+ Use this when you want to be explicit about durability.
436
+ """
437
+ return tool(
438
+ func,
439
+ name=name,
440
+ description=description,
441
+ timeout=timeout,
442
+ max_retries=max_retries,
443
+ retry_delay=retry_delay,
444
+ enable_durability=True,
445
+ )
446
+
447
+
448
+ class ToolKit:
449
+ """
450
+ A collection of related tools.
451
+
452
+ Example:
453
+ ```python
454
+ from agnt5 import ToolKit
455
+
456
+ class FileTools(ToolKit):
457
+ '''Tools for file operations.'''
458
+
459
+ @tool
460
+ def read_file(self, path: str) -> str:
461
+ '''Read contents of a file.'''
462
+ with open(path, 'r') as f:
463
+ return f.read()
464
+
465
+ @tool
466
+ def write_file(self, path: str, content: str) -> None:
467
+ '''Write content to a file.'''
468
+ with open(path, 'w') as f:
469
+ f.write(content)
470
+
471
+ # Use the toolkit
472
+ file_tools = FileTools()
473
+ tools = file_tools.get_tools()
474
+ ```
475
+ """
476
+
477
+ def __init__(self):
478
+ """Initialize the toolkit."""
479
+ self._tools: Dict[str, Tool] = {}
480
+ self._discover_tools()
481
+
482
+ def _discover_tools(self) -> None:
483
+ """Discover tools defined as methods."""
484
+ for name in dir(self):
485
+ if name.startswith("_"):
486
+ continue
487
+
488
+ attr = getattr(self, name)
489
+ if isinstance(attr, Tool):
490
+ self._tools[attr.name] = attr
491
+ elif hasattr(attr, "__tool__"):
492
+ # Method decorated with @tool
493
+ tool_instance = attr.__tool__
494
+ # Bind method to self
495
+ bound_func = functools.partial(tool_instance.func, self)
496
+ tool_instance.func = bound_func
497
+ self._tools[tool_instance.name] = tool_instance
498
+
499
+ def get_tools(self) -> List[Tool]:
500
+ """Get all tools in this toolkit."""
501
+ return list(self._tools.values())
502
+
503
+ def get_tool(self, name: str) -> Optional[Tool]:
504
+ """Get a specific tool by name."""
505
+ return self._tools.get(name)
506
+
507
+ def get_durable_tools(self) -> List[Tool]:
508
+ """Get all durable tools in this toolkit."""
509
+ return [
510
+ tool for tool in self._tools.values()
511
+ if getattr(tool.config, 'enable_durability', True)
512
+ ]
513
+
514
+ def get_tool_schemas(self) -> List[Dict[str, Any]]:
515
+ """Get OpenAI function schemas for all tools."""
516
+ return [tool.get_schema() for tool in self._tools.values()]