agentrun-sdk 0.1.2__py3-none-any.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 agentrun-sdk might be problematic. Click here for more details.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- agentrun_wrapper/tools/code_interpreter_client.py +186 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""Tool decorator for SDK.
|
|
2
|
+
|
|
3
|
+
This module provides the @tool decorator that transforms Python functions into SDK Agent tools with automatic metadata
|
|
4
|
+
extraction and validation.
|
|
5
|
+
|
|
6
|
+
The @tool decorator performs several functions:
|
|
7
|
+
|
|
8
|
+
1. Extracts function metadata (name, description, parameters) from docstrings and type hints
|
|
9
|
+
2. Generates a JSON schema for input validation
|
|
10
|
+
3. Handles two different calling patterns:
|
|
11
|
+
- Standard function calls (func(arg1, arg2))
|
|
12
|
+
- Tool use calls (agent.my_tool(param1="hello", param2=123))
|
|
13
|
+
4. Provides error handling and result formatting
|
|
14
|
+
5. Works with both standalone functions and class methods
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
```python
|
|
18
|
+
from strands import Agent, tool
|
|
19
|
+
|
|
20
|
+
@tool
|
|
21
|
+
def my_tool(param1: str, param2: int = 42) -> dict:
|
|
22
|
+
'''
|
|
23
|
+
Tool description - explain what it does.
|
|
24
|
+
|
|
25
|
+
#Args:
|
|
26
|
+
param1: Description of first parameter.
|
|
27
|
+
param2: Description of second parameter (default: 42).
|
|
28
|
+
|
|
29
|
+
#Returns:
|
|
30
|
+
A dictionary with the results.
|
|
31
|
+
'''
|
|
32
|
+
result = do_something(param1, param2)
|
|
33
|
+
return {
|
|
34
|
+
"status": "success",
|
|
35
|
+
"content": [{"text": f"Result: {result}"}]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
agent = Agent(tools=[my_tool])
|
|
39
|
+
agent.my_tool(param1="hello", param2=123)
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import functools
|
|
45
|
+
import inspect
|
|
46
|
+
import logging
|
|
47
|
+
from typing import (
|
|
48
|
+
Any,
|
|
49
|
+
Callable,
|
|
50
|
+
Generic,
|
|
51
|
+
Optional,
|
|
52
|
+
ParamSpec,
|
|
53
|
+
Type,
|
|
54
|
+
TypeVar,
|
|
55
|
+
Union,
|
|
56
|
+
get_type_hints,
|
|
57
|
+
overload,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
import docstring_parser
|
|
61
|
+
from pydantic import BaseModel, Field, create_model
|
|
62
|
+
from typing_extensions import override
|
|
63
|
+
|
|
64
|
+
from ..types.tools import AgentTool, JSONSchema, ToolGenerator, ToolSpec, ToolUse
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Type for wrapped function
|
|
70
|
+
T = TypeVar("T", bound=Callable[..., Any])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FunctionToolMetadata:
|
|
74
|
+
"""Helper class to extract and manage function metadata for tool decoration.
|
|
75
|
+
|
|
76
|
+
This class handles the extraction of metadata from Python functions including:
|
|
77
|
+
|
|
78
|
+
- Function name and description from docstrings
|
|
79
|
+
- Parameter names, types, and descriptions
|
|
80
|
+
- Return type information
|
|
81
|
+
- Creation of Pydantic models for input validation
|
|
82
|
+
|
|
83
|
+
The extracted metadata is used to generate a tool specification that can be used by Strands Agent to understand and
|
|
84
|
+
validate tool usage.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, func: Callable[..., Any]) -> None:
|
|
88
|
+
"""Initialize with the function to process.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
func: The function to extract metadata from.
|
|
92
|
+
Can be a standalone function or a class method.
|
|
93
|
+
"""
|
|
94
|
+
self.func = func
|
|
95
|
+
self.signature = inspect.signature(func)
|
|
96
|
+
self.type_hints = get_type_hints(func)
|
|
97
|
+
|
|
98
|
+
# Parse the docstring with docstring_parser
|
|
99
|
+
doc_str = inspect.getdoc(func) or ""
|
|
100
|
+
self.doc = docstring_parser.parse(doc_str)
|
|
101
|
+
|
|
102
|
+
# Get parameter descriptions from parsed docstring
|
|
103
|
+
self.param_descriptions = {
|
|
104
|
+
param.arg_name: param.description or f"Parameter {param.arg_name}" for param in self.doc.params
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Create a Pydantic model for validation
|
|
108
|
+
self.input_model = self._create_input_model()
|
|
109
|
+
|
|
110
|
+
def _create_input_model(self) -> Type[BaseModel]:
|
|
111
|
+
"""Create a Pydantic model from function signature for input validation.
|
|
112
|
+
|
|
113
|
+
This method analyzes the function's signature, type hints, and docstring to create a Pydantic model that can
|
|
114
|
+
validate input data before passing it to the function.
|
|
115
|
+
|
|
116
|
+
Special parameters like 'self', 'cls', and 'agent' are excluded from the model.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A Pydantic BaseModel class customized for the function's parameters.
|
|
120
|
+
"""
|
|
121
|
+
field_definitions: dict[str, Any] = {}
|
|
122
|
+
|
|
123
|
+
for name, param in self.signature.parameters.items():
|
|
124
|
+
# Skip special parameters
|
|
125
|
+
if name in ("self", "cls", "agent"):
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Get parameter type and default
|
|
129
|
+
param_type = self.type_hints.get(name, Any)
|
|
130
|
+
default = ... if param.default is inspect.Parameter.empty else param.default
|
|
131
|
+
description = self.param_descriptions.get(name, f"Parameter {name}")
|
|
132
|
+
|
|
133
|
+
# Create Field with description and default
|
|
134
|
+
field_definitions[name] = (param_type, Field(default=default, description=description))
|
|
135
|
+
|
|
136
|
+
# Create model name based on function name
|
|
137
|
+
model_name = f"{self.func.__name__.capitalize()}Tool"
|
|
138
|
+
|
|
139
|
+
# Create and return the model
|
|
140
|
+
if field_definitions:
|
|
141
|
+
return create_model(model_name, **field_definitions)
|
|
142
|
+
else:
|
|
143
|
+
# Handle case with no parameters
|
|
144
|
+
return create_model(model_name)
|
|
145
|
+
|
|
146
|
+
def extract_metadata(self) -> ToolSpec:
|
|
147
|
+
"""Extract metadata from the function to create a tool specification.
|
|
148
|
+
|
|
149
|
+
This method analyzes the function to create a standardized tool specification that Strands Agent can use to
|
|
150
|
+
understand and interact with the tool.
|
|
151
|
+
|
|
152
|
+
The specification includes:
|
|
153
|
+
|
|
154
|
+
- name: The function name (or custom override)
|
|
155
|
+
- description: The function's docstring
|
|
156
|
+
- inputSchema: A JSON schema describing the expected parameters
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A dictionary containing the tool specification.
|
|
160
|
+
"""
|
|
161
|
+
func_name = self.func.__name__
|
|
162
|
+
|
|
163
|
+
# Extract function description from docstring, preserving paragraph breaks
|
|
164
|
+
description = inspect.getdoc(self.func)
|
|
165
|
+
if description:
|
|
166
|
+
description = description.strip()
|
|
167
|
+
else:
|
|
168
|
+
description = func_name
|
|
169
|
+
|
|
170
|
+
# Get schema directly from the Pydantic model
|
|
171
|
+
input_schema = self.input_model.model_json_schema()
|
|
172
|
+
|
|
173
|
+
# Clean up Pydantic-specific schema elements
|
|
174
|
+
self._clean_pydantic_schema(input_schema)
|
|
175
|
+
|
|
176
|
+
# Create tool specification
|
|
177
|
+
tool_spec: ToolSpec = {"name": func_name, "description": description, "inputSchema": {"json": input_schema}}
|
|
178
|
+
|
|
179
|
+
return tool_spec
|
|
180
|
+
|
|
181
|
+
def _clean_pydantic_schema(self, schema: dict[str, Any]) -> None:
|
|
182
|
+
"""Clean up Pydantic schema to match Strands' expected format.
|
|
183
|
+
|
|
184
|
+
Pydantic's JSON schema output includes several elements that aren't needed for Strands Agent tools and could
|
|
185
|
+
cause validation issues. This method removes those elements and simplifies complex type structures.
|
|
186
|
+
|
|
187
|
+
Key operations:
|
|
188
|
+
|
|
189
|
+
1. Remove Pydantic-specific metadata (title, $defs, etc.)
|
|
190
|
+
2. Process complex types like Union and Optional to simpler formats
|
|
191
|
+
3. Handle nested property structures recursively
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
schema: The Pydantic-generated JSON schema to clean up (modified in place).
|
|
195
|
+
"""
|
|
196
|
+
# Remove Pydantic metadata
|
|
197
|
+
keys_to_remove = ["title", "additionalProperties"]
|
|
198
|
+
for key in keys_to_remove:
|
|
199
|
+
if key in schema:
|
|
200
|
+
del schema[key]
|
|
201
|
+
|
|
202
|
+
# Process properties to clean up anyOf and similar structures
|
|
203
|
+
if "properties" in schema:
|
|
204
|
+
for _prop_name, prop_schema in schema["properties"].items():
|
|
205
|
+
# Handle anyOf constructs (common for Optional types)
|
|
206
|
+
if "anyOf" in prop_schema:
|
|
207
|
+
any_of = prop_schema["anyOf"]
|
|
208
|
+
# Handle Optional[Type] case (represented as anyOf[Type, null])
|
|
209
|
+
if len(any_of) == 2 and any(item.get("type") == "null" for item in any_of):
|
|
210
|
+
# Find the non-null type
|
|
211
|
+
for item in any_of:
|
|
212
|
+
if item.get("type") != "null":
|
|
213
|
+
# Copy the non-null properties to the main schema
|
|
214
|
+
for k, v in item.items():
|
|
215
|
+
prop_schema[k] = v
|
|
216
|
+
# Remove the anyOf construct
|
|
217
|
+
del prop_schema["anyOf"]
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
# Clean up nested properties recursively
|
|
221
|
+
if "properties" in prop_schema:
|
|
222
|
+
self._clean_pydantic_schema(prop_schema)
|
|
223
|
+
|
|
224
|
+
# Remove any remaining Pydantic metadata from properties
|
|
225
|
+
for key in keys_to_remove:
|
|
226
|
+
if key in prop_schema:
|
|
227
|
+
del prop_schema[key]
|
|
228
|
+
|
|
229
|
+
def validate_input(self, input_data: dict[str, Any]) -> dict[str, Any]:
|
|
230
|
+
"""Validate input data using the Pydantic model.
|
|
231
|
+
|
|
232
|
+
This method ensures that the input data meets the expected schema before it's passed to the actual function. It
|
|
233
|
+
converts the data to the correct types when possible and raises informative errors when not.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
input_data: A dictionary of parameter names and values to validate.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
A dictionary with validated and converted parameter values.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
ValueError: If the input data fails validation, with details about what failed.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
# Validate with Pydantic model
|
|
246
|
+
validated = self.input_model(**input_data)
|
|
247
|
+
|
|
248
|
+
# Return as dict
|
|
249
|
+
return validated.model_dump()
|
|
250
|
+
except Exception as e:
|
|
251
|
+
# Re-raise with more detailed error message
|
|
252
|
+
error_msg = str(e)
|
|
253
|
+
raise ValueError(f"Validation failed for input parameters: {error_msg}") from e
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
P = ParamSpec("P") # Captures all parameters
|
|
257
|
+
R = TypeVar("R") # Return type
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class DecoratedFunctionTool(AgentTool, Generic[P, R]):
|
|
261
|
+
"""An AgentTool that wraps a function that was decorated with @tool.
|
|
262
|
+
|
|
263
|
+
This class adapts Python functions decorated with @tool to the AgentTool interface. It handles both direct
|
|
264
|
+
function calls and tool use invocations, maintaining the function's
|
|
265
|
+
original behavior while adding tool capabilities.
|
|
266
|
+
|
|
267
|
+
The class is generic over the function's parameter types (P) and return type (R) to maintain type safety.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
_tool_name: str
|
|
271
|
+
_tool_spec: ToolSpec
|
|
272
|
+
_tool_func: Callable[P, R]
|
|
273
|
+
_metadata: FunctionToolMetadata
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
tool_name: str,
|
|
278
|
+
tool_spec: ToolSpec,
|
|
279
|
+
tool_func: Callable[P, R],
|
|
280
|
+
metadata: FunctionToolMetadata,
|
|
281
|
+
):
|
|
282
|
+
"""Initialize the decorated function tool.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
tool_name: The name to use for the tool (usually the function name).
|
|
286
|
+
tool_spec: The tool specification containing metadata for Agent integration.
|
|
287
|
+
tool_func: The original function being decorated.
|
|
288
|
+
metadata: The FunctionToolMetadata object with extracted function information.
|
|
289
|
+
"""
|
|
290
|
+
super().__init__()
|
|
291
|
+
|
|
292
|
+
self._tool_name = tool_name
|
|
293
|
+
self._tool_spec = tool_spec
|
|
294
|
+
self._tool_func = tool_func
|
|
295
|
+
self._metadata = metadata
|
|
296
|
+
|
|
297
|
+
functools.update_wrapper(wrapper=self, wrapped=self._tool_func)
|
|
298
|
+
|
|
299
|
+
def __get__(self, instance: Any, obj_type: Optional[Type] = None) -> "DecoratedFunctionTool[P, R]":
|
|
300
|
+
"""Descriptor protocol implementation for proper method binding.
|
|
301
|
+
|
|
302
|
+
This method enables the decorated function to work correctly when used as a class method.
|
|
303
|
+
It binds the instance to the function call when accessed through an instance.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
instance: The instance through which the descriptor is accessed, or None when accessed through the class.
|
|
307
|
+
obj_type: The class through which the descriptor is accessed.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
A new DecoratedFunctionTool with the instance bound to the function if accessed through an instance,
|
|
311
|
+
otherwise returns self.
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
```python
|
|
315
|
+
class MyClass:
|
|
316
|
+
@tool
|
|
317
|
+
def my_tool():
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
instance = MyClass()
|
|
321
|
+
# instance of DecoratedFunctionTool that works as you'd expect
|
|
322
|
+
tool = instance.my_tool
|
|
323
|
+
```
|
|
324
|
+
"""
|
|
325
|
+
if instance is not None and not inspect.ismethod(self._tool_func):
|
|
326
|
+
# Create a bound method
|
|
327
|
+
tool_func = self._tool_func.__get__(instance, instance.__class__)
|
|
328
|
+
return DecoratedFunctionTool(self._tool_name, self._tool_spec, tool_func, self._metadata)
|
|
329
|
+
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
333
|
+
"""Call the original function with the provided arguments.
|
|
334
|
+
|
|
335
|
+
This method enables the decorated function to be called directly with its original signature,
|
|
336
|
+
preserving the normal function call behavior.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
*args: Positional arguments to pass to the function.
|
|
340
|
+
**kwargs: Keyword arguments to pass to the function.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The result of the original function call.
|
|
344
|
+
"""
|
|
345
|
+
return self._tool_func(*args, **kwargs)
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def tool_name(self) -> str:
|
|
349
|
+
"""Get the name of the tool.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
The tool name as a string.
|
|
353
|
+
"""
|
|
354
|
+
return self._tool_name
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def tool_spec(self) -> ToolSpec:
|
|
358
|
+
"""Get the tool specification.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The tool specification dictionary containing metadata for Agent integration.
|
|
362
|
+
"""
|
|
363
|
+
return self._tool_spec
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def tool_type(self) -> str:
|
|
367
|
+
"""Get the type of the tool.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
The string "function" indicating this is a function-based tool.
|
|
371
|
+
"""
|
|
372
|
+
return "function"
|
|
373
|
+
|
|
374
|
+
@override
|
|
375
|
+
async def stream(self, tool_use: ToolUse, invocation_state: dict[str, Any], **kwargs: Any) -> ToolGenerator:
|
|
376
|
+
"""Stream the tool with a tool use specification.
|
|
377
|
+
|
|
378
|
+
This method handles tool use streams from a Strands Agent. It validates the input,
|
|
379
|
+
calls the function, and formats the result according to the expected tool result format.
|
|
380
|
+
|
|
381
|
+
Key operations:
|
|
382
|
+
|
|
383
|
+
1. Extract tool use ID and input parameters
|
|
384
|
+
2. Validate input against the function's expected parameters
|
|
385
|
+
3. Call the function with validated input
|
|
386
|
+
4. Format the result as a standard tool result
|
|
387
|
+
5. Handle and format any errors that occur
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
tool_use: The tool use specification from the Agent.
|
|
391
|
+
invocation_state: Context for the tool invocation, including agent state.
|
|
392
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
393
|
+
|
|
394
|
+
Yields:
|
|
395
|
+
Tool events with the last being the tool result.
|
|
396
|
+
"""
|
|
397
|
+
# This is a tool use call - process accordingly
|
|
398
|
+
tool_use_id = tool_use.get("toolUseId", "unknown")
|
|
399
|
+
tool_input = tool_use.get("input", {})
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
# Validate input against the Pydantic model
|
|
403
|
+
validated_input = self._metadata.validate_input(tool_input)
|
|
404
|
+
|
|
405
|
+
# Pass along the agent if provided and expected by the function
|
|
406
|
+
if "agent" in invocation_state and "agent" in self._metadata.signature.parameters:
|
|
407
|
+
validated_input["agent"] = invocation_state.get("agent")
|
|
408
|
+
|
|
409
|
+
# "Too few arguments" expected, hence the type ignore
|
|
410
|
+
if inspect.iscoroutinefunction(self._tool_func):
|
|
411
|
+
result = await self._tool_func(**validated_input) # type: ignore
|
|
412
|
+
else:
|
|
413
|
+
result = await asyncio.to_thread(self._tool_func, **validated_input) # type: ignore
|
|
414
|
+
|
|
415
|
+
# FORMAT THE RESULT for Strands Agent
|
|
416
|
+
if isinstance(result, dict) and "status" in result and "content" in result:
|
|
417
|
+
# Result is already in the expected format, just add toolUseId
|
|
418
|
+
result["toolUseId"] = tool_use_id
|
|
419
|
+
yield result
|
|
420
|
+
else:
|
|
421
|
+
# Wrap any other return value in the standard format
|
|
422
|
+
# Always include at least one content item for consistency
|
|
423
|
+
yield {
|
|
424
|
+
"toolUseId": tool_use_id,
|
|
425
|
+
"status": "success",
|
|
426
|
+
"content": [{"text": str(result)}],
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
except ValueError as e:
|
|
430
|
+
# Special handling for validation errors
|
|
431
|
+
error_msg = str(e)
|
|
432
|
+
yield {
|
|
433
|
+
"toolUseId": tool_use_id,
|
|
434
|
+
"status": "error",
|
|
435
|
+
"content": [{"text": f"Error: {error_msg}"}],
|
|
436
|
+
}
|
|
437
|
+
except Exception as e:
|
|
438
|
+
# Return error result with exception details for any other error
|
|
439
|
+
error_type = type(e).__name__
|
|
440
|
+
error_msg = str(e)
|
|
441
|
+
yield {
|
|
442
|
+
"toolUseId": tool_use_id,
|
|
443
|
+
"status": "error",
|
|
444
|
+
"content": [{"text": f"Error: {error_type} - {error_msg}"}],
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def supports_hot_reload(self) -> bool:
|
|
449
|
+
"""Check if this tool supports automatic reloading when modified.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
Always true for function-based tools.
|
|
453
|
+
"""
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
@override
|
|
457
|
+
def get_display_properties(self) -> dict[str, str]:
|
|
458
|
+
"""Get properties to display in UI representations.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Function properties (e.g., function name).
|
|
462
|
+
"""
|
|
463
|
+
properties = super().get_display_properties()
|
|
464
|
+
properties["Function"] = self._tool_func.__name__
|
|
465
|
+
return properties
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# Handle @decorator
|
|
469
|
+
@overload
|
|
470
|
+
def tool(__func: Callable[P, R]) -> DecoratedFunctionTool[P, R]: ...
|
|
471
|
+
# Handle @decorator()
|
|
472
|
+
@overload
|
|
473
|
+
def tool(
|
|
474
|
+
description: Optional[str] = None,
|
|
475
|
+
inputSchema: Optional[JSONSchema] = None,
|
|
476
|
+
name: Optional[str] = None,
|
|
477
|
+
) -> Callable[[Callable[P, R]], DecoratedFunctionTool[P, R]]: ...
|
|
478
|
+
# Suppressing the type error because we want callers to be able to use both `tool` and `tool()` at the
|
|
479
|
+
# call site, but the actual implementation handles that and it's not representable via the type-system
|
|
480
|
+
def tool( # type: ignore
|
|
481
|
+
func: Optional[Callable[P, R]] = None,
|
|
482
|
+
description: Optional[str] = None,
|
|
483
|
+
inputSchema: Optional[JSONSchema] = None,
|
|
484
|
+
name: Optional[str] = None,
|
|
485
|
+
) -> Union[DecoratedFunctionTool[P, R], Callable[[Callable[P, R]], DecoratedFunctionTool[P, R]]]:
|
|
486
|
+
"""Decorator that transforms a Python function into a Strands tool.
|
|
487
|
+
|
|
488
|
+
This decorator seamlessly enables a function to be called both as a regular Python function and as a Strands tool.
|
|
489
|
+
It extracts metadata from the function's signature, docstring, and type hints to generate an OpenAPI-compatible tool
|
|
490
|
+
specification.
|
|
491
|
+
|
|
492
|
+
When decorated, a function:
|
|
493
|
+
|
|
494
|
+
1. Still works as a normal function when called directly with arguments
|
|
495
|
+
2. Processes tool use API calls when provided with a tool use dictionary
|
|
496
|
+
3. Validates inputs against the function's type hints and parameter spec
|
|
497
|
+
4. Formats return values according to the expected Strands tool result format
|
|
498
|
+
5. Provides automatic error handling and reporting
|
|
499
|
+
|
|
500
|
+
The decorator can be used in two ways:
|
|
501
|
+
- As a simple decorator: `@tool`
|
|
502
|
+
- With parameters: `@tool(name="custom_name", description="Custom description")`
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
func: The function to decorate. When used as a simple decorator, this is the function being decorated.
|
|
506
|
+
When used with parameters, this will be None.
|
|
507
|
+
description: Optional custom description to override the function's docstring.
|
|
508
|
+
inputSchema: Optional custom JSON schema to override the automatically generated schema.
|
|
509
|
+
name: Optional custom name to override the function's name.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
An AgentTool that also mimics the original function when invoked
|
|
513
|
+
|
|
514
|
+
Example:
|
|
515
|
+
```python
|
|
516
|
+
@tool
|
|
517
|
+
def my_tool(name: str, count: int = 1) -> str:
|
|
518
|
+
# Does something useful with the provided parameters.
|
|
519
|
+
#
|
|
520
|
+
# Parameters:
|
|
521
|
+
# name: The name to process
|
|
522
|
+
# count: Number of times to process (default: 1)
|
|
523
|
+
#
|
|
524
|
+
# Returns:
|
|
525
|
+
# A message with the result
|
|
526
|
+
return f"Processed {name} {count} times"
|
|
527
|
+
|
|
528
|
+
agent = Agent(tools=[my_tool])
|
|
529
|
+
agent.my_tool(name="example", count=3)
|
|
530
|
+
# Returns: {
|
|
531
|
+
# "toolUseId": "123",
|
|
532
|
+
# "status": "success",
|
|
533
|
+
# "content": [{"text": "Processed example 3 times"}]
|
|
534
|
+
# }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Example with parameters:
|
|
538
|
+
```python
|
|
539
|
+
@tool(name="custom_tool", description="A tool with a custom name and description")
|
|
540
|
+
def my_tool(name: str, count: int = 1) -> str:
|
|
541
|
+
return f"Processed {name} {count} times"
|
|
542
|
+
```
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
def decorator(f: T) -> "DecoratedFunctionTool[P, R]":
|
|
546
|
+
# Create function tool metadata
|
|
547
|
+
tool_meta = FunctionToolMetadata(f)
|
|
548
|
+
tool_spec = tool_meta.extract_metadata()
|
|
549
|
+
if name is not None:
|
|
550
|
+
tool_spec["name"] = name
|
|
551
|
+
if description is not None:
|
|
552
|
+
tool_spec["description"] = description
|
|
553
|
+
if inputSchema is not None:
|
|
554
|
+
tool_spec["inputSchema"] = inputSchema
|
|
555
|
+
|
|
556
|
+
tool_name = tool_spec.get("name", f.__name__)
|
|
557
|
+
|
|
558
|
+
if not isinstance(tool_name, str):
|
|
559
|
+
raise ValueError(f"Tool name must be a string, got {type(tool_name)}")
|
|
560
|
+
|
|
561
|
+
return DecoratedFunctionTool(tool_name, tool_spec, f, tool_meta)
|
|
562
|
+
|
|
563
|
+
# Handle both @tool and @tool() syntax
|
|
564
|
+
if func is None:
|
|
565
|
+
# Need to ignore type-checking here since it's hard to represent the support
|
|
566
|
+
# for both flows using the type system
|
|
567
|
+
return decorator
|
|
568
|
+
|
|
569
|
+
return decorator(func)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Tool execution functionality for the event loop."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Optional, cast
|
|
7
|
+
|
|
8
|
+
from opentelemetry import trace as trace_api
|
|
9
|
+
|
|
10
|
+
from ..telemetry.metrics import EventLoopMetrics, Trace
|
|
11
|
+
from ..telemetry.tracer import get_tracer
|
|
12
|
+
from ..tools.tools import InvalidToolUseNameException, validate_tool_use
|
|
13
|
+
from ..types.content import Message
|
|
14
|
+
from ..types.tools import RunToolHandler, ToolGenerator, ToolResult, ToolUse
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run_tools(
|
|
20
|
+
handler: RunToolHandler,
|
|
21
|
+
tool_uses: list[ToolUse],
|
|
22
|
+
event_loop_metrics: EventLoopMetrics,
|
|
23
|
+
invalid_tool_use_ids: list[str],
|
|
24
|
+
tool_results: list[ToolResult],
|
|
25
|
+
cycle_trace: Trace,
|
|
26
|
+
parent_span: Optional[trace_api.Span] = None,
|
|
27
|
+
) -> ToolGenerator:
|
|
28
|
+
"""Execute tools concurrently.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
handler: Tool handler processing function.
|
|
32
|
+
tool_uses: List of tool uses to execute.
|
|
33
|
+
event_loop_metrics: Metrics collection object.
|
|
34
|
+
invalid_tool_use_ids: List of invalid tool use IDs.
|
|
35
|
+
tool_results: List to populate with tool results.
|
|
36
|
+
cycle_trace: Parent trace for the current cycle.
|
|
37
|
+
parent_span: Parent span for the current cycle.
|
|
38
|
+
|
|
39
|
+
Yields:
|
|
40
|
+
Events of the tool stream. Tool results are appended to `tool_results`.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
async def work(
|
|
44
|
+
tool_use: ToolUse,
|
|
45
|
+
worker_id: int,
|
|
46
|
+
worker_queue: asyncio.Queue,
|
|
47
|
+
worker_event: asyncio.Event,
|
|
48
|
+
stop_event: object,
|
|
49
|
+
) -> ToolResult:
|
|
50
|
+
tracer = get_tracer()
|
|
51
|
+
tool_call_span = tracer.start_tool_call_span(tool_use, parent_span)
|
|
52
|
+
|
|
53
|
+
tool_name = tool_use["name"]
|
|
54
|
+
tool_trace = Trace(f"Tool: {tool_name}", parent_id=cycle_trace.id, raw_name=tool_name)
|
|
55
|
+
tool_start_time = time.time()
|
|
56
|
+
with trace_api.use_span(tool_call_span):
|
|
57
|
+
try:
|
|
58
|
+
async for event in handler(tool_use):
|
|
59
|
+
worker_queue.put_nowait((worker_id, event))
|
|
60
|
+
await worker_event.wait()
|
|
61
|
+
worker_event.clear()
|
|
62
|
+
|
|
63
|
+
result = cast(ToolResult, event)
|
|
64
|
+
finally:
|
|
65
|
+
worker_queue.put_nowait((worker_id, stop_event))
|
|
66
|
+
|
|
67
|
+
tool_success = result.get("status") == "success"
|
|
68
|
+
tool_duration = time.time() - tool_start_time
|
|
69
|
+
message = Message(role="user", content=[{"toolResult": result}])
|
|
70
|
+
event_loop_metrics.add_tool_usage(tool_use, tool_duration, tool_trace, tool_success, message)
|
|
71
|
+
cycle_trace.add_child(tool_trace)
|
|
72
|
+
|
|
73
|
+
tracer.end_tool_call_span(tool_call_span, result)
|
|
74
|
+
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
tool_uses = [tool_use for tool_use in tool_uses if tool_use.get("toolUseId") not in invalid_tool_use_ids]
|
|
78
|
+
worker_queue: asyncio.Queue[tuple[int, Any]] = asyncio.Queue()
|
|
79
|
+
worker_events = [asyncio.Event() for _ in tool_uses]
|
|
80
|
+
stop_event = object()
|
|
81
|
+
|
|
82
|
+
workers = [
|
|
83
|
+
asyncio.create_task(work(tool_use, worker_id, worker_queue, worker_events[worker_id], stop_event))
|
|
84
|
+
for worker_id, tool_use in enumerate(tool_uses)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
worker_count = len(workers)
|
|
88
|
+
while worker_count:
|
|
89
|
+
worker_id, event = await worker_queue.get()
|
|
90
|
+
if event is stop_event:
|
|
91
|
+
worker_count -= 1
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
yield event
|
|
95
|
+
worker_events[worker_id].set()
|
|
96
|
+
|
|
97
|
+
tool_results.extend([worker.result() for worker in workers])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def validate_and_prepare_tools(
|
|
101
|
+
message: Message,
|
|
102
|
+
tool_uses: list[ToolUse],
|
|
103
|
+
tool_results: list[ToolResult],
|
|
104
|
+
invalid_tool_use_ids: list[str],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Validate tool uses and prepare them for execution.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
message: Current message.
|
|
110
|
+
tool_uses: List to populate with tool uses.
|
|
111
|
+
tool_results: List to populate with tool results for invalid tools.
|
|
112
|
+
invalid_tool_use_ids: List to populate with invalid tool use IDs.
|
|
113
|
+
"""
|
|
114
|
+
# Extract tool uses from message
|
|
115
|
+
for content in message["content"]:
|
|
116
|
+
if isinstance(content, dict) and "toolUse" in content:
|
|
117
|
+
tool_uses.append(content["toolUse"])
|
|
118
|
+
|
|
119
|
+
# Validate tool uses
|
|
120
|
+
# Avoid modifying original `tool_uses` variable during iteration
|
|
121
|
+
tool_uses_copy = tool_uses.copy()
|
|
122
|
+
for tool in tool_uses_copy:
|
|
123
|
+
try:
|
|
124
|
+
validate_tool_use(tool)
|
|
125
|
+
except InvalidToolUseNameException as e:
|
|
126
|
+
# Replace the invalid toolUse name and return invalid name error as ToolResult to the LLM as context
|
|
127
|
+
tool_uses.remove(tool)
|
|
128
|
+
tool["name"] = "INVALID_TOOL_NAME"
|
|
129
|
+
invalid_tool_use_ids.append(tool["toolUseId"])
|
|
130
|
+
tool_uses.append(tool)
|
|
131
|
+
tool_results.append(
|
|
132
|
+
{
|
|
133
|
+
"toolUseId": tool["toolUseId"],
|
|
134
|
+
"status": "error",
|
|
135
|
+
"content": [{"text": f"Error: {str(e)}"}],
|
|
136
|
+
}
|
|
137
|
+
)
|