diff --git a/docs/docs/learn/programming/tools.md b/docs/docs/learn/programming/tools.md index 85429999f3..2492ac56b8 100644 --- a/docs/docs/learn/programming/tools.md +++ b/docs/docs/learn/programming/tools.md @@ -106,11 +106,11 @@ response = predictor( # Execute the tool calls for call in response.outputs.tool_calls: - if call.name in tools: - result = tools[call.name](**call.args) - print(f"Tool: {call.name}") - print(f"Args: {call.args}") - print(f"Result: {result}") + # Execute the tool call + result = call.execute() + print(f"Tool: {call.name}") + print(f"Args: {call.args}") + print(f"Result: {result}") ``` ### Understanding `dspy.Tool` @@ -134,7 +134,7 @@ print(str(tool)) # Full tool description ### Understanding `dspy.ToolCalls` -The `dspy.ToolCalls` type represents the output from a model that can make tool calls: +The `dspy.ToolCalls` type represents the output from a model that can make tool calls. Each individual tool call can be executed using the `execute` method: ```python # After getting a response with tool calls @@ -142,10 +142,18 @@ for call in response.outputs.tool_calls: print(f"Tool name: {call.name}") print(f"Arguments: {call.args}") - # Execute the tool - if call.name in tools: - result = tools[call.name](**call.args) - print(f"Result: {result}") + # Execute individual tool calls with different options: + + # Option 1: Automatic discovery (finds functions in locals/globals) + result = call.execute() # Automatically finds functions by name + + # Option 2: Pass tools as a dict (most explicit) + result = call.execute(functions={"weather": weather, "calculator": calculator}) + + # Option 3: Pass Tool objects as a list + result = call.execute(functions=[dspy.Tool(weather), dspy.Tool(calculator)]) + + print(f"Result: {result}") ``` ## Using Native Tool Calling diff --git a/dspy/adapters/types/tool.py b/dspy/adapters/types/tool.py index 5cde703a41..153220c304 100644 --- a/dspy/adapters/types/tool.py +++ b/dspy/adapters/types/tool.py @@ -1,6 +1,6 @@ import asyncio import inspect -from typing import TYPE_CHECKING, Any, Callable, Type, get_origin, get_type_hints +from typing import TYPE_CHECKING, Any, Callable, get_origin, get_type_hints import pydantic from jsonschema import ValidationError, validate @@ -269,6 +269,51 @@ def format(self): }, } + def execute(self, functions: dict[str, Any] | list[Tool] | None = None) -> Any: + """Execute this individual tool call and return its result. + + Args: + functions: Functions to search for the tool. Can be: + - Dict mapping tool names to functions: {"tool_name": function} + - List of Tool objects: [Tool(function), ...] + - None: Will search in caller's locals and globals (automatic lookup) + + Returns: + The result from executing this tool call. + + Raises: + ValueError: If the tool function cannot be found. + Exception: Any exception raised by the tool function. + """ + func = None + + if functions is None: + # Automatic lookup in caller's globals and locals + frame = inspect.currentframe().f_back + try: + caller_globals = frame.f_globals + caller_locals = frame.f_locals + func = caller_locals.get(self.name) or caller_globals.get(self.name) + finally: + del frame + + elif isinstance(functions, dict): + func = functions.get(self.name) + elif isinstance(functions, list): + for tool in functions: + if tool.name == self.name: + func = tool.func + break + + if func is None: + raise ValueError(f"Tool function '{self.name}' not found. Please pass the tool functions to the `execute` method.") + + try: + args = self.args or {} + return func(**args) + except Exception as e: + raise RuntimeError(f"Error executing tool '{self.name}': {e}") from e + tool_calls: list[ToolCall] @classmethod diff --git a/tests/adapters/test_tool.py b/tests/adapters/test_tool.py index d9c38912f8..876ff5fc79 100644 --- a/tests/adapters/test_tool.py +++ b/tests/adapters/test_tool.py @@ -540,3 +540,72 @@ def test_tool_convert_input_schema_to_tool_args_lang_chain(): "bar": "The bar.", "baz": "No description provided. (Required)", } + + + + +def test_tool_call_execute(): + def get_weather(city: str) -> str: + return f"The weather in {city} is sunny" + + def add_numbers(a: int, b: int) -> int: + return a + b + + tools = [ + dspy.Tool(get_weather), + dspy.Tool(add_numbers) + ] + + tool_call = dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Berlin"}) + result = tool_call.execute(functions=tools) + assert result == "The weather in Berlin is sunny" + + # Test individual tool call with function dict + tool_call2 = dspy.ToolCalls.ToolCall(name="add_numbers", args={"a": 7, "b": 13}) + result2 = tool_call2.execute(functions={"add_numbers": add_numbers}) + assert result2 == 20 + + # Test individual tool call with no arguments + def get_pi(): + return 3.14159 + + tool_call3 = dspy.ToolCalls.ToolCall(name="get_pi", args={}) + result3 = tool_call3.execute(functions={"get_pi": get_pi}) + assert result3 == 3.14159 + + # Test error case + tool_call4 = dspy.ToolCalls.ToolCall(name="nonexistent", args={}) + try: + tool_call4.execute(functions=tools) + assert False, "Should have raised ValueError" + except ValueError as e: + assert "not found" in str(e) + + +def test_tool_call_execute_with_local_functions(): + def main(): + def local_add(a: int, b: int) -> int: + return a + b + + def local_multiply(x: int, y: int) -> int: + return x * y + + # Test individual execution with local function + tool_call1 = dspy.ToolCalls.ToolCall(name="local_add", args={"a": 10, "b": 15}) + result1 = tool_call1.execute() # Should find local function automatically + assert result1 == 25 + + tool_call2 = dspy.ToolCalls.ToolCall(name="local_multiply", args={"x": 4, "y": 7}) + result2 = tool_call2.execute() # Should find local function automatically + assert result2 == 28 + + # Test locals take precedence over globals + try: + globals()["local_add"] = lambda a, b: a + b + 1000 + precedence_call = dspy.ToolCalls.ToolCall(name="local_add", args={"a": 1, "b": 2}) + result = precedence_call.execute() + assert result == 3 # Should use local function (1+2=3), not global (1+2+1000=1003) + finally: + globals().pop("local_add", None) + + main()