agnt5 0.2.8a2__cp310-abi3-macosx_10_12_x86_64.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/__init__.py +87 -0
- agnt5/_compat.py +16 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/_retry_utils.py +169 -0
- agnt5/_schema_utils.py +312 -0
- agnt5/_telemetry.py +167 -0
- agnt5/agent.py +956 -0
- agnt5/client.py +724 -0
- agnt5/context.py +84 -0
- agnt5/entity.py +697 -0
- agnt5/exceptions.py +46 -0
- agnt5/function.py +314 -0
- agnt5/lm.py +705 -0
- agnt5/tool.py +418 -0
- agnt5/tracing.py +196 -0
- agnt5/types.py +110 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +1151 -0
- agnt5/workflow.py +596 -0
- agnt5-0.2.8a2.dist-info/METADATA +25 -0
- agnt5-0.2.8a2.dist-info/RECORD +22 -0
- agnt5-0.2.8a2.dist-info/WHEEL +4 -0
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
|
+
]
|