agnt5 0.2.8a10__cp310-abi3-manylinux_2_34_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 +91 -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 +182 -0
- agnt5/agent.py +1685 -0
- agnt5/client.py +741 -0
- agnt5/context.py +178 -0
- agnt5/entity.py +795 -0
- agnt5/exceptions.py +102 -0
- agnt5/function.py +321 -0
- agnt5/lm.py +813 -0
- agnt5/tool.py +648 -0
- agnt5/tracing.py +196 -0
- agnt5/types.py +110 -0
- agnt5/version.py +19 -0
- agnt5/worker.py +1619 -0
- agnt5/workflow.py +1048 -0
- agnt5-0.2.8a10.dist-info/METADATA +25 -0
- agnt5-0.2.8a10.dist-info/RECORD +22 -0
- agnt5-0.2.8a10.dist-info/WHEEL +4 -0
agnt5/tool.py
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
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, set_current_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
|
+
# Emit checkpoint if called within a workflow context
|
|
233
|
+
from .context import get_workflow_context
|
|
234
|
+
|
|
235
|
+
workflow_ctx = get_workflow_context()
|
|
236
|
+
if workflow_ctx:
|
|
237
|
+
workflow_ctx._send_checkpoint("workflow.tool.started", {
|
|
238
|
+
"tool.name": self.name,
|
|
239
|
+
"tool.args": list(kwargs.keys()),
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
# Set context in task-local storage for automatic propagation to nested calls
|
|
243
|
+
token = set_current_context(ctx)
|
|
244
|
+
try:
|
|
245
|
+
try:
|
|
246
|
+
# Create span for tool execution with trace linking
|
|
247
|
+
from ._core import create_span
|
|
248
|
+
|
|
249
|
+
logger.debug(f"Invoking tool '{self.name}' with args: {list(kwargs.keys())}")
|
|
250
|
+
|
|
251
|
+
# Create span with runtime_context for parent-child span linking
|
|
252
|
+
with create_span(
|
|
253
|
+
self.name,
|
|
254
|
+
"tool",
|
|
255
|
+
ctx._runtime_context if hasattr(ctx, "_runtime_context") else None,
|
|
256
|
+
{
|
|
257
|
+
"tool.name": self.name,
|
|
258
|
+
"tool.args": ",".join(kwargs.keys()),
|
|
259
|
+
},
|
|
260
|
+
) as span:
|
|
261
|
+
# Handler is already async (validated in tool() decorator)
|
|
262
|
+
result = await self.handler(ctx, **kwargs)
|
|
263
|
+
|
|
264
|
+
logger.debug(f"Tool '{self.name}' completed successfully")
|
|
265
|
+
|
|
266
|
+
# Emit completion checkpoint
|
|
267
|
+
if workflow_ctx:
|
|
268
|
+
workflow_ctx._send_checkpoint("workflow.tool.completed", {
|
|
269
|
+
"tool.name": self.name,
|
|
270
|
+
"tool.success": True,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
except Exception as e:
|
|
275
|
+
# Emit error checkpoint for observability
|
|
276
|
+
if workflow_ctx:
|
|
277
|
+
workflow_ctx._send_checkpoint("workflow.tool.error", {
|
|
278
|
+
"tool.name": self.name,
|
|
279
|
+
"error": str(e),
|
|
280
|
+
"error_type": type(e).__name__,
|
|
281
|
+
})
|
|
282
|
+
raise
|
|
283
|
+
finally:
|
|
284
|
+
# Always reset context to prevent leakage
|
|
285
|
+
from .context import _current_context
|
|
286
|
+
_current_context.reset(token)
|
|
287
|
+
|
|
288
|
+
def get_schema(self) -> Dict[str, Any]:
|
|
289
|
+
"""
|
|
290
|
+
Get complete tool schema for agent consumption.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dict with name, description, and input_schema
|
|
294
|
+
"""
|
|
295
|
+
return {
|
|
296
|
+
"name": self.name,
|
|
297
|
+
"description": self.description,
|
|
298
|
+
"input_schema": self.input_schema,
|
|
299
|
+
"requires_confirmation": self.confirmation
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ToolRegistry:
|
|
304
|
+
"""Global registry for tools."""
|
|
305
|
+
|
|
306
|
+
_tools: Dict[str, Tool] = {}
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def register(cls, tool: Tool) -> None:
|
|
310
|
+
"""Register a tool."""
|
|
311
|
+
if tool.name in cls._tools:
|
|
312
|
+
logger.warning(f"Overwriting existing tool '{tool.name}'")
|
|
313
|
+
cls._tools[tool.name] = tool
|
|
314
|
+
logger.debug(f"Registered tool '{tool.name}'")
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def get(cls, name: str) -> Optional[Tool]:
|
|
318
|
+
"""Get a tool by name."""
|
|
319
|
+
return cls._tools.get(name)
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def all(cls) -> Dict[str, Tool]:
|
|
323
|
+
"""Get all registered tools."""
|
|
324
|
+
return cls._tools.copy()
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
def clear(cls) -> None:
|
|
328
|
+
"""Clear all registered tools (for testing)."""
|
|
329
|
+
cls._tools.clear()
|
|
330
|
+
logger.debug("Cleared tool registry")
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def list_names(cls) -> List[str]:
|
|
334
|
+
"""Get list of all tool names."""
|
|
335
|
+
return list(cls._tools.keys())
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def tool(
|
|
339
|
+
_func: Optional[Callable] = None,
|
|
340
|
+
*,
|
|
341
|
+
name: Optional[str] = None,
|
|
342
|
+
description: Optional[str] = None,
|
|
343
|
+
auto_schema: bool = True,
|
|
344
|
+
confirmation: bool = False,
|
|
345
|
+
input_schema: Optional[Dict[str, Any]] = None
|
|
346
|
+
) -> Callable:
|
|
347
|
+
"""
|
|
348
|
+
Decorator to mark a function as a tool with automatic schema extraction.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
name: Tool name (defaults to function name)
|
|
352
|
+
description: Tool description (defaults to first line of docstring)
|
|
353
|
+
auto_schema: Automatically extract schema from type hints and docstring
|
|
354
|
+
confirmation: Whether tool requires confirmation before execution
|
|
355
|
+
input_schema: Manual schema (only if auto_schema=False)
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Decorated function that can be invoked as a tool
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
```python
|
|
362
|
+
@tool(auto_schema=True)
|
|
363
|
+
def search_web(ctx: Context, query: str, max_results: int = 10) -> List[Dict]:
|
|
364
|
+
\"\"\"Search the web for information.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
query: The search query string
|
|
368
|
+
max_results: Maximum number of results to return
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
List of search results
|
|
372
|
+
\"\"\"
|
|
373
|
+
# Implementation
|
|
374
|
+
return results
|
|
375
|
+
```
|
|
376
|
+
"""
|
|
377
|
+
def decorator(func: Callable) -> Callable:
|
|
378
|
+
# Determine tool name
|
|
379
|
+
tool_name = name or func.__name__
|
|
380
|
+
|
|
381
|
+
# Extract description from docstring if not provided
|
|
382
|
+
tool_description = description
|
|
383
|
+
if tool_description is None:
|
|
384
|
+
docstring = inspect.getdoc(func)
|
|
385
|
+
if docstring:
|
|
386
|
+
parsed_doc = parse_docstring(docstring)
|
|
387
|
+
tool_description = parsed_doc.short_description or parsed_doc.long_description or ""
|
|
388
|
+
else:
|
|
389
|
+
tool_description = ""
|
|
390
|
+
|
|
391
|
+
# Validate function signature
|
|
392
|
+
sig = inspect.signature(func)
|
|
393
|
+
params = list(sig.parameters.values())
|
|
394
|
+
|
|
395
|
+
if not params:
|
|
396
|
+
raise ConfigurationError(
|
|
397
|
+
f"Tool function '{func.__name__}' must have at least one parameter (ctx: Context)"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
first_param = params[0]
|
|
401
|
+
if first_param.annotation != Context and first_param.annotation != inspect.Parameter.empty:
|
|
402
|
+
raise ConfigurationError(
|
|
403
|
+
f"Tool function '{func.__name__}' first parameter must be 'ctx: Context', "
|
|
404
|
+
f"got '{first_param.annotation}'"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Convert sync to async if needed
|
|
408
|
+
if not asyncio.iscoroutinefunction(func):
|
|
409
|
+
original_func = func
|
|
410
|
+
|
|
411
|
+
@functools.wraps(original_func)
|
|
412
|
+
async def async_wrapper(*args, **kwargs):
|
|
413
|
+
# Run sync function in thread pool to prevent blocking event loop
|
|
414
|
+
loop = asyncio.get_running_loop()
|
|
415
|
+
return await loop.run_in_executor(None, lambda: original_func(*args, **kwargs))
|
|
416
|
+
|
|
417
|
+
handler_func = async_wrapper
|
|
418
|
+
else:
|
|
419
|
+
handler_func = func
|
|
420
|
+
|
|
421
|
+
# Create Tool instance
|
|
422
|
+
tool_instance = Tool(
|
|
423
|
+
name=tool_name,
|
|
424
|
+
description=tool_description,
|
|
425
|
+
handler=handler_func,
|
|
426
|
+
input_schema=input_schema,
|
|
427
|
+
confirmation=confirmation,
|
|
428
|
+
auto_schema=auto_schema
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Register tool
|
|
432
|
+
ToolRegistry.register(tool_instance)
|
|
433
|
+
|
|
434
|
+
# Return wrapper that invokes tool
|
|
435
|
+
@functools.wraps(func)
|
|
436
|
+
async def tool_wrapper(*args, **kwargs) -> Any:
|
|
437
|
+
"""Wrapper that invokes tool with context."""
|
|
438
|
+
# If called with Context as first arg, use tool.invoke
|
|
439
|
+
if args and isinstance(args[0], Context):
|
|
440
|
+
ctx = args[0]
|
|
441
|
+
return await tool_instance.invoke(ctx, **kwargs)
|
|
442
|
+
|
|
443
|
+
# Otherwise, direct call (for testing)
|
|
444
|
+
return await handler_func(*args, **kwargs)
|
|
445
|
+
|
|
446
|
+
# Attach tool instance to wrapper for inspection
|
|
447
|
+
tool_wrapper._tool = tool_instance
|
|
448
|
+
|
|
449
|
+
return tool_wrapper
|
|
450
|
+
|
|
451
|
+
if _func is None:
|
|
452
|
+
return decorator
|
|
453
|
+
return decorator(_func)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ============================================================================
|
|
457
|
+
# Built-in Human-in-the-Loop Tools
|
|
458
|
+
# ============================================================================
|
|
459
|
+
|
|
460
|
+
class AskUserTool(Tool):
|
|
461
|
+
"""
|
|
462
|
+
Built-in tool that agents can use to request text input from users.
|
|
463
|
+
|
|
464
|
+
This tool pauses the workflow execution and waits for the user to provide
|
|
465
|
+
a text response. The workflow resumes when the user submits their input.
|
|
466
|
+
|
|
467
|
+
Example:
|
|
468
|
+
```python
|
|
469
|
+
from agnt5 import Agent, workflow, WorkflowContext
|
|
470
|
+
from agnt5.tool import AskUserTool
|
|
471
|
+
|
|
472
|
+
@workflow(chat=True)
|
|
473
|
+
async def agent_with_hitl(ctx: WorkflowContext, query: str) -> dict:
|
|
474
|
+
agent = Agent(
|
|
475
|
+
name="research_agent",
|
|
476
|
+
model="openai/gpt-4o-mini",
|
|
477
|
+
instructions="You are a research assistant.",
|
|
478
|
+
tools=[AskUserTool(ctx)]
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
result = await agent.run(query, context=ctx)
|
|
482
|
+
return {"response": result.output}
|
|
483
|
+
```
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
def __init__(self, context: Optional["WorkflowContext"] = None): # type: ignore
|
|
487
|
+
"""
|
|
488
|
+
Initialize AskUserTool.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
context: Optional workflow context with wait_for_user capability.
|
|
492
|
+
If not provided, will attempt to get from task-local contextvar.
|
|
493
|
+
"""
|
|
494
|
+
# Import here to avoid circular dependency
|
|
495
|
+
from .workflow import WorkflowContext
|
|
496
|
+
|
|
497
|
+
if context is not None and not isinstance(context, WorkflowContext):
|
|
498
|
+
raise ConfigurationError(
|
|
499
|
+
"AskUserTool requires a WorkflowContext. "
|
|
500
|
+
"This tool can only be used within workflows."
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
super().__init__(
|
|
504
|
+
name="ask_user",
|
|
505
|
+
description="Ask the user a question and wait for their text response",
|
|
506
|
+
handler=self._handler,
|
|
507
|
+
auto_schema=True
|
|
508
|
+
)
|
|
509
|
+
self.context = context
|
|
510
|
+
|
|
511
|
+
async def _handler(self, ctx: Context, question: str) -> str:
|
|
512
|
+
"""
|
|
513
|
+
Ask user a question and wait for their response.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
ctx: Execution context (may contain WorkflowContext via contextvar)
|
|
517
|
+
question: Question to ask the user
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
User's text response
|
|
521
|
+
"""
|
|
522
|
+
# Import here to avoid circular dependency
|
|
523
|
+
from .workflow import WorkflowContext
|
|
524
|
+
from .context import get_current_context
|
|
525
|
+
|
|
526
|
+
# Use explicit context if provided during __init__
|
|
527
|
+
workflow_ctx = self.context
|
|
528
|
+
|
|
529
|
+
# If not provided, try to get from task-local contextvar
|
|
530
|
+
if workflow_ctx is None:
|
|
531
|
+
current = get_current_context()
|
|
532
|
+
if isinstance(current, WorkflowContext):
|
|
533
|
+
workflow_ctx = current
|
|
534
|
+
elif hasattr(current, '_workflow_entity'):
|
|
535
|
+
# Current context has workflow entity (is WorkflowContext)
|
|
536
|
+
workflow_ctx = current # type: ignore
|
|
537
|
+
|
|
538
|
+
if workflow_ctx is None:
|
|
539
|
+
raise ConfigurationError(
|
|
540
|
+
"AskUserTool requires WorkflowContext. "
|
|
541
|
+
"Either pass context to __init__ or ensure tool is used within a workflow."
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
return await workflow_ctx.wait_for_user(question, input_type="text")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class RequestApprovalTool(Tool):
|
|
548
|
+
"""
|
|
549
|
+
Built-in tool that agents can use to request approval from users.
|
|
550
|
+
|
|
551
|
+
This tool pauses the workflow execution and presents an approval request
|
|
552
|
+
to the user with approve/reject options. The workflow resumes when the
|
|
553
|
+
user makes a decision.
|
|
554
|
+
|
|
555
|
+
Example:
|
|
556
|
+
```python
|
|
557
|
+
from agnt5 import Agent, workflow, WorkflowContext
|
|
558
|
+
from agnt5.tool import RequestApprovalTool
|
|
559
|
+
|
|
560
|
+
@workflow(chat=True)
|
|
561
|
+
async def deployment_agent(ctx: WorkflowContext, changes: dict) -> dict:
|
|
562
|
+
agent = Agent(
|
|
563
|
+
name="deploy_agent",
|
|
564
|
+
model="openai/gpt-4o-mini",
|
|
565
|
+
instructions="You help deploy code changes safely.",
|
|
566
|
+
tools=[RequestApprovalTool(ctx)]
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
result = await agent.run(
|
|
570
|
+
f"Review and deploy these changes: {changes}",
|
|
571
|
+
context=ctx
|
|
572
|
+
)
|
|
573
|
+
return {"response": result.output}
|
|
574
|
+
```
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
def __init__(self, context: Optional["WorkflowContext"] = None): # type: ignore
|
|
578
|
+
"""
|
|
579
|
+
Initialize RequestApprovalTool.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
context: Optional workflow context with wait_for_user capability.
|
|
583
|
+
If not provided, will attempt to get from task-local contextvar.
|
|
584
|
+
"""
|
|
585
|
+
# Import here to avoid circular dependency
|
|
586
|
+
from .workflow import WorkflowContext
|
|
587
|
+
|
|
588
|
+
if context is not None and not isinstance(context, WorkflowContext):
|
|
589
|
+
raise ConfigurationError(
|
|
590
|
+
"RequestApprovalTool requires a WorkflowContext. "
|
|
591
|
+
"This tool can only be used within workflows."
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
super().__init__(
|
|
595
|
+
name="request_approval",
|
|
596
|
+
description="Request user approval for an action before proceeding",
|
|
597
|
+
handler=self._handler,
|
|
598
|
+
auto_schema=True
|
|
599
|
+
)
|
|
600
|
+
self.context = context
|
|
601
|
+
|
|
602
|
+
async def _handler(self, ctx: Context, action: str, details: str = "") -> str:
|
|
603
|
+
"""
|
|
604
|
+
Request approval from user for an action.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
ctx: Execution context (may contain WorkflowContext via contextvar)
|
|
608
|
+
action: The action requiring approval
|
|
609
|
+
details: Additional details about the action
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
"approve" or "reject" based on user's decision
|
|
613
|
+
"""
|
|
614
|
+
# Import here to avoid circular dependency
|
|
615
|
+
from .workflow import WorkflowContext
|
|
616
|
+
from .context import get_current_context
|
|
617
|
+
|
|
618
|
+
# Use explicit context if provided during __init__
|
|
619
|
+
workflow_ctx = self.context
|
|
620
|
+
|
|
621
|
+
# If not provided, try to get from task-local contextvar
|
|
622
|
+
if workflow_ctx is None:
|
|
623
|
+
current = get_current_context()
|
|
624
|
+
if isinstance(current, WorkflowContext):
|
|
625
|
+
workflow_ctx = current
|
|
626
|
+
elif hasattr(current, '_workflow_entity'):
|
|
627
|
+
# Current context has workflow entity (is WorkflowContext)
|
|
628
|
+
workflow_ctx = current # type: ignore
|
|
629
|
+
|
|
630
|
+
if workflow_ctx is None:
|
|
631
|
+
raise ConfigurationError(
|
|
632
|
+
"RequestApprovalTool requires WorkflowContext. "
|
|
633
|
+
"Either pass context to __init__ or ensure tool is used within a workflow."
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
question = f"Action: {action}"
|
|
637
|
+
if details:
|
|
638
|
+
question += f"\n\nDetails:\n{details}"
|
|
639
|
+
question += "\n\nDo you approve?"
|
|
640
|
+
|
|
641
|
+
return await workflow_ctx.wait_for_user(
|
|
642
|
+
question,
|
|
643
|
+
input_type="approval",
|
|
644
|
+
options=[
|
|
645
|
+
{"id": "approve", "label": "Approve"},
|
|
646
|
+
{"id": "reject", "label": "Reject"}
|
|
647
|
+
]
|
|
648
|
+
)
|