From 624f31dce9a80849fc86a46e01d3771d773942dc Mon Sep 17 00:00:00 2001 From: TomuHirata Date: Thu, 18 Sep 2025 16:20:59 +0900 Subject: [PATCH 1/3] Add ToolCall.execute for smoother tool execution --- docs/docs/learn/programming/tools.md | 28 +++++++----- dspy/adapters/types/tool.py | 48 +++++++++++++++++++- tests/adapters/test_tool.py | 66 ++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/docs/docs/learn/programming/tools.md b/docs/docs/learn/programming/tools.md index 85429999f3..63e2704fea 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: Pass tools as a dict (most explicit) + result = call.execute(functions={"weather": weather, "calculator": calculator}) + + # Option 2: Pass Tool objects as a list + result = call.execute(functions=[dspy.Tool(weather), dspy.Tool(calculator)]) + + # Option 3: Automatic discovery (finds functions in locals/globals) + result = call.execute() # Automatically finds functions by name + + print(f"Result: {result}") ``` ## Using Native Tool Calling diff --git a/dspy/adapters/types/tool.py b/dspy/adapters/types/tool.py index 5cde703a41..5453307631 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,52 @@ 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 + import inspect + frame = inspect.currentframe().f_back + caller_globals = frame.f_globals + caller_locals = frame.f_locals + func = caller_locals.get(self.name) or caller_globals.get(self.name) + + 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. Pass the tool functions to the `execute` method.") + + try: + if self.args: + result = func(**self.args) + else: + result = func() + return result + except Exception as e: + raise Exception(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..bfc17b2049 100644 --- a/tests/adapters/test_tool.py +++ b/tests/adapters/test_tool.py @@ -540,3 +540,69 @@ 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 + 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) + + main() From fd42810c78562432d498c663c44dd70fc597ad87 Mon Sep 17 00:00:00 2001 From: TomuHirata Date: Thu, 18 Sep 2025 16:38:41 +0900 Subject: [PATCH 2/3] comment --- dspy/adapters/types/tool.py | 12 +++++++----- tests/adapters/test_tool.py | 11 +++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/dspy/adapters/types/tool.py b/dspy/adapters/types/tool.py index 5453307631..8ec4599251 100644 --- a/dspy/adapters/types/tool.py +++ b/dspy/adapters/types/tool.py @@ -289,11 +289,13 @@ def execute(self, functions: dict[str, Any] | list[Tool] | None = None) -> Any: if functions is None: # Automatic lookup in caller's globals and locals - import inspect frame = inspect.currentframe().f_back - caller_globals = frame.f_globals - caller_locals = frame.f_locals - func = caller_locals.get(self.name) or caller_globals.get(self.name) + 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) @@ -313,7 +315,7 @@ def execute(self, functions: dict[str, Any] | list[Tool] | None = None) -> Any: result = func() return result except Exception as e: - raise Exception(f"Error executing tool '{self.name}': {e}") from e + raise RuntimeError(f"Error executing tool '{self.name}': {e}") from e tool_calls: list[ToolCall] diff --git a/tests/adapters/test_tool.py b/tests/adapters/test_tool.py index bfc17b2049..876ff5fc79 100644 --- a/tests/adapters/test_tool.py +++ b/tests/adapters/test_tool.py @@ -600,9 +600,12 @@ def local_multiply(x: int, y: int) -> int: assert result2 == 28 # Test locals take precedence over globals - 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) + 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() From ac2a1d4d37ce34fb2af048538540775063bc086f Mon Sep 17 00:00:00 2001 From: TomuHirata Date: Tue, 30 Sep 2025 17:19:09 +0900 Subject: [PATCH 3/3] comment --- docs/docs/learn/programming/tools.md | 10 +++++----- dspy/adapters/types/tool.py | 9 +++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/docs/learn/programming/tools.md b/docs/docs/learn/programming/tools.md index 63e2704fea..2492ac56b8 100644 --- a/docs/docs/learn/programming/tools.md +++ b/docs/docs/learn/programming/tools.md @@ -144,15 +144,15 @@ for call in response.outputs.tool_calls: # Execute individual tool calls with different options: - # Option 1: Pass tools as a dict (most explicit) + # 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 2: Pass Tool objects as a list + # Option 3: Pass Tool objects as a list result = call.execute(functions=[dspy.Tool(weather), dspy.Tool(calculator)]) - # Option 3: Automatic discovery (finds functions in locals/globals) - result = call.execute() # Automatically finds functions by name - print(f"Result: {result}") ``` diff --git a/dspy/adapters/types/tool.py b/dspy/adapters/types/tool.py index 8ec4599251..153220c304 100644 --- a/dspy/adapters/types/tool.py +++ b/dspy/adapters/types/tool.py @@ -306,14 +306,11 @@ def execute(self, functions: dict[str, Any] | list[Tool] | None = None) -> Any: break if func is None: - raise ValueError(f"Tool function '{self.name}' not found. Pass the tool functions to the `execute` method.") + raise ValueError(f"Tool function '{self.name}' not found. Please pass the tool functions to the `execute` method.") try: - if self.args: - result = func(**self.args) - else: - result = func() - return result + args = self.args or {} + return func(**args) except Exception as e: raise RuntimeError(f"Error executing tool '{self.name}': {e}") from e