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.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. 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
+ )