agnt5 0.2.8a2__cp310-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.

Potentially problematic release.


This version of agnt5 might be problematic. Click here for more details.

agnt5/tool.py ADDED
@@ -0,0 +1,418 @@
1
+ """
2
+ Tool component for Agent capabilities with automatic schema extraction.
3
+
4
+ Tools extend what agents can do by providing structured interfaces to functions,
5
+ with automatic schema generation from Python type hints and docstrings.
6
+ """
7
+
8
+ import asyncio
9
+ import functools
10
+ import inspect
11
+ import logging
12
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, get_args, get_origin
13
+
14
+ from docstring_parser import parse as parse_docstring
15
+
16
+ from .context import Context
17
+ from .exceptions import ConfigurationError
18
+ from ._telemetry import setup_module_logger
19
+
20
+ logger = setup_module_logger(__name__)
21
+
22
+ T = TypeVar("T")
23
+ ToolHandler = Callable[..., Awaitable[T]]
24
+
25
+
26
+ def _python_type_to_json_schema_type(py_type: Any) -> str:
27
+ """
28
+ Convert Python type to JSON Schema type.
29
+
30
+ Args:
31
+ py_type: Python type annotation
32
+
33
+ Returns:
34
+ JSON Schema type string
35
+ """
36
+ # Handle None/NoneType
37
+ if py_type is None or py_type is type(None):
38
+ return "null"
39
+
40
+ # Handle string types
41
+ origin = get_origin(py_type)
42
+
43
+ # Handle Optional[T] -> unwrap to T
44
+ if origin is type(None.__class__): # Union type
45
+ args = get_args(py_type)
46
+ # Filter out NoneType
47
+ non_none_args = [arg for arg in args if arg is not type(None)]
48
+ if len(non_none_args) == 1:
49
+ return _python_type_to_json_schema_type(non_none_args[0])
50
+ # Multiple non-None types -> just use first one
51
+ if non_none_args:
52
+ return _python_type_to_json_schema_type(non_none_args[0])
53
+ return "null"
54
+
55
+ # Handle basic types
56
+ type_map = {
57
+ str: "string",
58
+ int: "integer",
59
+ float: "number",
60
+ bool: "boolean",
61
+ list: "array",
62
+ List: "array",
63
+ dict: "object",
64
+ Dict: "object",
65
+ Any: "string", # Default to string for Any
66
+ }
67
+
68
+ # Check origin for generic types
69
+ if origin is not None:
70
+ return type_map.get(origin, "string")
71
+
72
+ # Direct type match
73
+ return type_map.get(py_type, "string")
74
+
75
+
76
+ def _extract_schema_from_function(func: Callable) -> Dict[str, Any]:
77
+ """
78
+ Extract JSON schema from function signature and docstring.
79
+
80
+ Args:
81
+ func: Function to extract schema from
82
+
83
+ Returns:
84
+ Dict containing input_schema and output_schema
85
+ """
86
+ # Parse function signature
87
+ sig = inspect.signature(func)
88
+ docstring = inspect.getdoc(func) or ""
89
+ parsed_doc = parse_docstring(docstring)
90
+
91
+ # Build parameter schemas
92
+ properties = {}
93
+ required = []
94
+
95
+ # Build mapping from param name to docstring description
96
+ param_descriptions = {}
97
+ if parsed_doc.params:
98
+ for param_doc in parsed_doc.params:
99
+ param_descriptions[param_doc.arg_name] = param_doc.description or ""
100
+
101
+ for param_name, param in sig.parameters.items():
102
+ # Skip 'ctx' parameter (Context is auto-injected)
103
+ if param_name == "ctx":
104
+ continue
105
+
106
+ # Get type annotation
107
+ param_type = param.annotation
108
+ if param_type == inspect.Parameter.empty:
109
+ param_type = Any
110
+
111
+ # Get description from docstring
112
+ description = param_descriptions.get(param_name, "")
113
+
114
+ # Build parameter schema
115
+ param_schema = {
116
+ "type": _python_type_to_json_schema_type(param_type),
117
+ "description": description
118
+ }
119
+
120
+ properties[param_name] = param_schema
121
+
122
+ # Check if required (no default value)
123
+ if param.default == inspect.Parameter.empty:
124
+ required.append(param_name)
125
+
126
+ # Build input schema
127
+ input_schema = {
128
+ "type": "object",
129
+ "properties": properties,
130
+ "required": required
131
+ }
132
+
133
+ # Extract return type for output schema (optional for basic tool functionality)
134
+ return_type = sig.return_annotation
135
+ output_schema = None
136
+ if return_type != inspect.Parameter.empty:
137
+ output_schema = {
138
+ "type": _python_type_to_json_schema_type(return_type)
139
+ }
140
+
141
+ return {
142
+ "input_schema": input_schema,
143
+ "output_schema": output_schema
144
+ }
145
+
146
+
147
+ class Tool:
148
+ """
149
+ Represents a tool that agents can use.
150
+
151
+ Tools wrap functions with automatic schema extraction and provide
152
+ a structured interface for agent invocation.
153
+ """
154
+
155
+ def __init__(
156
+ self,
157
+ name: str,
158
+ description: str,
159
+ handler: ToolHandler,
160
+ input_schema: Optional[Dict[str, Any]] = None,
161
+ confirmation: bool = False,
162
+ auto_schema: bool = False
163
+ ):
164
+ """
165
+ Initialize a Tool.
166
+
167
+ Args:
168
+ name: Tool name
169
+ description: Tool description for agents
170
+ handler: Function that implements the tool
171
+ input_schema: Manual JSON schema for input parameters
172
+ confirmation: Whether tool requires human confirmation before execution
173
+ auto_schema: Whether to automatically extract schema from handler
174
+ """
175
+ self.name = name
176
+ self.description = description
177
+ self.handler = handler
178
+ self.confirmation = confirmation
179
+
180
+ # Extract or use provided schema
181
+ if auto_schema:
182
+ schemas = _extract_schema_from_function(handler)
183
+ self.input_schema = schemas["input_schema"]
184
+ self.output_schema = schemas.get("output_schema")
185
+ else:
186
+ self.input_schema = input_schema or {"type": "object", "properties": {}}
187
+ self.output_schema = None
188
+
189
+ # Validate handler signature
190
+ self._validate_handler()
191
+
192
+ logger.debug(f"Created tool '{name}' with auto_schema={auto_schema}")
193
+
194
+ def _validate_handler(self) -> None:
195
+ """Validate that handler has correct signature."""
196
+ sig = inspect.signature(self.handler)
197
+ params = list(sig.parameters.values())
198
+
199
+ if not params:
200
+ raise ConfigurationError(
201
+ f"Tool handler '{self.name}' must have at least one parameter (ctx: Context)"
202
+ )
203
+
204
+ first_param = params[0]
205
+ if first_param.annotation != Context and first_param.annotation != inspect.Parameter.empty:
206
+ logger.warning(
207
+ f"Tool handler '{self.name}' first parameter should be 'ctx: Context', "
208
+ f"got '{first_param.annotation}'"
209
+ )
210
+
211
+ async def invoke(self, ctx: Context, **kwargs) -> Any:
212
+ """
213
+ Invoke the tool with given arguments.
214
+
215
+ Args:
216
+ ctx: Execution context
217
+ **kwargs: Tool arguments matching input_schema
218
+
219
+ Returns:
220
+ Tool execution result
221
+
222
+ Raises:
223
+ ConfigurationError: If tool requires confirmation (not yet implemented)
224
+ """
225
+ if self.confirmation:
226
+ # TODO: Implement actual confirmation workflow
227
+ # For now, just log a warning
228
+ logger.warning(
229
+ f"Tool '{self.name}' requires confirmation but confirmation is not yet implemented"
230
+ )
231
+
232
+ # Create span for tool execution with trace linking
233
+ from ._core import create_span
234
+
235
+ logger.debug(f"Invoking tool '{self.name}' with args: {list(kwargs.keys())}")
236
+
237
+ # Create span with runtime_context for parent-child span linking
238
+ with create_span(
239
+ self.name,
240
+ "tool",
241
+ ctx._runtime_context if hasattr(ctx, "_runtime_context") else None,
242
+ {
243
+ "tool.name": self.name,
244
+ "tool.args": ",".join(kwargs.keys()),
245
+ },
246
+ ) as span:
247
+ # Handler is already async (validated in tool() decorator)
248
+ result = await self.handler(ctx, **kwargs)
249
+
250
+ logger.debug(f"Tool '{self.name}' completed successfully")
251
+ return result
252
+
253
+ def get_schema(self) -> Dict[str, Any]:
254
+ """
255
+ Get complete tool schema for agent consumption.
256
+
257
+ Returns:
258
+ Dict with name, description, and input_schema
259
+ """
260
+ return {
261
+ "name": self.name,
262
+ "description": self.description,
263
+ "input_schema": self.input_schema,
264
+ "requires_confirmation": self.confirmation
265
+ }
266
+
267
+
268
+ class ToolRegistry:
269
+ """Global registry for tools."""
270
+
271
+ _tools: Dict[str, Tool] = {}
272
+
273
+ @classmethod
274
+ def register(cls, tool: Tool) -> None:
275
+ """Register a tool."""
276
+ if tool.name in cls._tools:
277
+ logger.warning(f"Overwriting existing tool '{tool.name}'")
278
+ cls._tools[tool.name] = tool
279
+ logger.debug(f"Registered tool '{tool.name}'")
280
+
281
+ @classmethod
282
+ def get(cls, name: str) -> Optional[Tool]:
283
+ """Get a tool by name."""
284
+ return cls._tools.get(name)
285
+
286
+ @classmethod
287
+ def all(cls) -> Dict[str, Tool]:
288
+ """Get all registered tools."""
289
+ return cls._tools.copy()
290
+
291
+ @classmethod
292
+ def clear(cls) -> None:
293
+ """Clear all registered tools (for testing)."""
294
+ cls._tools.clear()
295
+ logger.debug("Cleared tool registry")
296
+
297
+ @classmethod
298
+ def list_names(cls) -> List[str]:
299
+ """Get list of all tool names."""
300
+ return list(cls._tools.keys())
301
+
302
+
303
+ def tool(
304
+ _func: Optional[Callable] = None,
305
+ *,
306
+ name: Optional[str] = None,
307
+ description: Optional[str] = None,
308
+ auto_schema: bool = True,
309
+ confirmation: bool = False,
310
+ input_schema: Optional[Dict[str, Any]] = None
311
+ ) -> Callable:
312
+ """
313
+ Decorator to mark a function as a tool with automatic schema extraction.
314
+
315
+ Args:
316
+ name: Tool name (defaults to function name)
317
+ description: Tool description (defaults to first line of docstring)
318
+ auto_schema: Automatically extract schema from type hints and docstring
319
+ confirmation: Whether tool requires confirmation before execution
320
+ input_schema: Manual schema (only if auto_schema=False)
321
+
322
+ Returns:
323
+ Decorated function that can be invoked as a tool
324
+
325
+ Example:
326
+ ```python
327
+ @tool(auto_schema=True)
328
+ def search_web(ctx: Context, query: str, max_results: int = 10) -> List[Dict]:
329
+ \"\"\"Search the web for information.
330
+
331
+ Args:
332
+ query: The search query string
333
+ max_results: Maximum number of results to return
334
+
335
+ Returns:
336
+ List of search results
337
+ \"\"\"
338
+ # Implementation
339
+ return results
340
+ ```
341
+ """
342
+ def decorator(func: Callable) -> Callable:
343
+ # Determine tool name
344
+ tool_name = name or func.__name__
345
+
346
+ # Extract description from docstring if not provided
347
+ tool_description = description
348
+ if tool_description is None:
349
+ docstring = inspect.getdoc(func)
350
+ if docstring:
351
+ parsed_doc = parse_docstring(docstring)
352
+ tool_description = parsed_doc.short_description or parsed_doc.long_description or ""
353
+ else:
354
+ tool_description = ""
355
+
356
+ # Validate function signature
357
+ sig = inspect.signature(func)
358
+ params = list(sig.parameters.values())
359
+
360
+ if not params:
361
+ raise ConfigurationError(
362
+ f"Tool function '{func.__name__}' must have at least one parameter (ctx: Context)"
363
+ )
364
+
365
+ first_param = params[0]
366
+ if first_param.annotation != Context and first_param.annotation != inspect.Parameter.empty:
367
+ raise ConfigurationError(
368
+ f"Tool function '{func.__name__}' first parameter must be 'ctx: Context', "
369
+ f"got '{first_param.annotation}'"
370
+ )
371
+
372
+ # Convert sync to async if needed
373
+ if not asyncio.iscoroutinefunction(func):
374
+ original_func = func
375
+
376
+ @functools.wraps(original_func)
377
+ async def async_wrapper(*args, **kwargs):
378
+ # Run sync function in thread pool to prevent blocking event loop
379
+ loop = asyncio.get_running_loop()
380
+ return await loop.run_in_executor(None, lambda: original_func(*args, **kwargs))
381
+
382
+ handler_func = async_wrapper
383
+ else:
384
+ handler_func = func
385
+
386
+ # Create Tool instance
387
+ tool_instance = Tool(
388
+ name=tool_name,
389
+ description=tool_description,
390
+ handler=handler_func,
391
+ input_schema=input_schema,
392
+ confirmation=confirmation,
393
+ auto_schema=auto_schema
394
+ )
395
+
396
+ # Register tool
397
+ ToolRegistry.register(tool_instance)
398
+
399
+ # Return wrapper that invokes tool
400
+ @functools.wraps(func)
401
+ async def tool_wrapper(*args, **kwargs) -> Any:
402
+ """Wrapper that invokes tool with context."""
403
+ # If called with Context as first arg, use tool.invoke
404
+ if args and isinstance(args[0], Context):
405
+ ctx = args[0]
406
+ return await tool_instance.invoke(ctx, **kwargs)
407
+
408
+ # Otherwise, direct call (for testing)
409
+ return await handler_func(*args, **kwargs)
410
+
411
+ # Attach tool instance to wrapper for inspection
412
+ tool_wrapper._tool = tool_instance
413
+
414
+ return tool_wrapper
415
+
416
+ if _func is None:
417
+ return decorator
418
+ return decorator(_func)
agnt5/tracing.py ADDED
@@ -0,0 +1,196 @@
1
+ """
2
+ User-facing tracing API for AGNT5 SDK.
3
+
4
+ Provides decorators and context managers for instrumenting Python code with
5
+ OpenTelemetry spans. All spans are created via Rust FFI and exported through
6
+ the centralized Rust OpenTelemetry system.
7
+
8
+ Example:
9
+ ```python
10
+ from agnt5.tracing import span
11
+
12
+ @span("my_operation")
13
+ async def my_function(ctx, data):
14
+ # Your code here
15
+ return result
16
+
17
+ # Or use context manager
18
+ from agnt5.tracing import span_context
19
+
20
+ async def process():
21
+ with span_context("processing", user_id="123") as s:
22
+ data = await fetch_data()
23
+ s.set_attribute("records", str(len(data)))
24
+ return data
25
+ ```
26
+ """
27
+
28
+ import functools
29
+ import inspect
30
+ from contextlib import contextmanager
31
+ from typing import Any, Callable, Dict, Optional
32
+
33
+ from ._core import create_span as _create_span
34
+
35
+
36
+ def span(
37
+ name: Optional[str] = None,
38
+ component_type: str = "function",
39
+ runtime_context: Optional[Any] = None,
40
+ **attributes: str
41
+ ):
42
+ """
43
+ Decorator to automatically create spans for functions.
44
+
45
+ Args:
46
+ name: Span name (defaults to function name)
47
+ component_type: Component type (default: "function")
48
+ runtime_context: Optional RuntimeContext for trace linking
49
+ **attributes: Additional span attributes
50
+
51
+ Example:
52
+ ```python
53
+ @span("fetch_user_data", user_type="premium")
54
+ async def fetch_user(user_id: str):
55
+ return await db.get_user(user_id)
56
+ ```
57
+ """
58
+ def decorator(func: Callable) -> Callable:
59
+ span_name = name or func.__name__
60
+
61
+ if inspect.iscoroutinefunction(func):
62
+ @functools.wraps(func)
63
+ async def async_wrapper(*args, **kwargs):
64
+ # Try to extract runtime_context from first arg if it's a Context
65
+ ctx = runtime_context
66
+ if ctx is None and args:
67
+ from .context import Context
68
+ if isinstance(args[0], Context):
69
+ ctx = args[0]._runtime_context
70
+
71
+ with _create_span(span_name, component_type, ctx, attributes) as s:
72
+ try:
73
+ result = await func(*args, **kwargs)
74
+ # Span automatically marked as OK on success
75
+ return result
76
+ except Exception as e:
77
+ # Exception automatically recorded by PySpan.__exit__
78
+ raise
79
+ return async_wrapper
80
+ else:
81
+ @functools.wraps(func)
82
+ def sync_wrapper(*args, **kwargs):
83
+ # Try to extract runtime_context from first arg if it's a Context
84
+ ctx = runtime_context
85
+ if ctx is None and args:
86
+ from .context import Context
87
+ if isinstance(args[0], Context):
88
+ ctx = args[0]._runtime_context
89
+
90
+ with _create_span(span_name, component_type, ctx, attributes) as s:
91
+ try:
92
+ result = func(*args, **kwargs)
93
+ return result
94
+ except Exception as e:
95
+ raise
96
+ return sync_wrapper
97
+
98
+ return decorator
99
+
100
+
101
+ @contextmanager
102
+ def span_context(
103
+ name: str,
104
+ component_type: str = "operation",
105
+ runtime_context: Optional[Any] = None,
106
+ **attributes: str
107
+ ):
108
+ """
109
+ Context manager for creating spans around code blocks.
110
+
111
+ Args:
112
+ name: Span name
113
+ component_type: Component type (default: "operation")
114
+ runtime_context: Optional RuntimeContext for trace linking
115
+ **attributes: Span attributes
116
+
117
+ Yields:
118
+ PySpan object with set_attribute() and record_exception() methods
119
+
120
+ Example:
121
+ ```python
122
+ with span_context("db_query", runtime_context=ctx._runtime_context, table="users") as s:
123
+ results = query_database()
124
+ s.set_attribute("result_count", str(len(results)))
125
+ ```
126
+ """
127
+ s = _create_span(name, component_type, runtime_context, attributes)
128
+ try:
129
+ yield s
130
+ # Context manager automatically calls s.__exit__ which sets status
131
+ except Exception as e:
132
+ # Exception will be recorded by __exit__
133
+ raise
134
+ finally:
135
+ # PySpan's __exit__ is called automatically when context ends
136
+ pass
137
+
138
+
139
+ def create_task_span(name: str, runtime_context: Optional[Any] = None, **attributes: str):
140
+ """
141
+ Create a span for task execution.
142
+
143
+ Args:
144
+ name: Task name
145
+ runtime_context: Optional RuntimeContext for trace linking
146
+ **attributes: Task attributes
147
+
148
+ Returns:
149
+ PySpan object to use as context manager
150
+
151
+ Example:
152
+ ```python
153
+ with create_task_span("process_data", runtime_context=ctx._runtime_context, batch_size="100") as s:
154
+ result = await process()
155
+ ```
156
+ """
157
+ return _create_span(name, "task", runtime_context, attributes)
158
+
159
+
160
+ def create_workflow_span(name: str, runtime_context: Optional[Any] = None, **attributes: str):
161
+ """
162
+ Create a span for workflow execution.
163
+
164
+ Args:
165
+ name: Workflow name
166
+ runtime_context: Optional RuntimeContext for trace linking
167
+ **attributes: Workflow attributes
168
+
169
+ Returns:
170
+ PySpan object to use as context manager
171
+ """
172
+ return _create_span(name, "workflow", runtime_context, attributes)
173
+
174
+
175
+ def create_agent_span(name: str, runtime_context: Optional[Any] = None, **attributes: str):
176
+ """
177
+ Create a span for agent execution.
178
+
179
+ Args:
180
+ name: Agent name
181
+ runtime_context: Optional RuntimeContext for trace linking
182
+ **attributes: Agent attributes
183
+
184
+ Returns:
185
+ PySpan object to use as context manager
186
+ """
187
+ return _create_span(name, "agent", runtime_context, attributes)
188
+
189
+
190
+ __all__ = [
191
+ "span",
192
+ "span_context",
193
+ "create_task_span",
194
+ "create_workflow_span",
195
+ "create_agent_span",
196
+ ]