Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions docs/docs/learn/programming/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -134,18 +134,26 @@ 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
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
Expand Down
47 changes: 46 additions & 1 deletion dspy/adapters/types/tool.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions tests/adapters/test_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we define it as a function before def main()?

Copy link
Collaborator Author

@TomeHirata TomeHirata Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's still registered in locals in that case since globals are module-level variables.

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()