Skip to content
This repository was archived by the owner on Mar 19, 2026. It is now read-only.

Commit 4c02725

Browse files
authored
Merge pull request #394 from teocns/fix/tool-parameters-parsing
tools: prioritizes explicitly passed parameters over generated ones
2 parents f259fa8 + 06967d0 commit 4c02725

File tree

2 files changed

+88
-13
lines changed

2 files changed

+88
-13
lines changed

src/controlflow/tools/tools.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,27 @@ def from_function(
118118
):
119119
name = name or fn.__name__
120120
description = description or fn.__doc__ or ""
121-
122121
signature = inspect.signature(fn)
123-
try:
124-
parameters = TypeAdapter(fn).json_schema()
125-
except PydanticSchemaGenerationError:
126-
raise ValueError(
127-
f'Could not generate a schema for tool "{name}". '
128-
"Tool functions must have type hints that are compatible with Pydantic."
129-
)
122+
123+
# If parameters are provided in kwargs, use those instead of generating them
124+
if "parameters" in kwargs:
125+
parameters = kwargs.pop("parameters") # Custom parameters are respected
126+
else:
127+
try:
128+
parameters = TypeAdapter(fn).json_schema()
129+
except PydanticSchemaGenerationError:
130+
raise ValueError(
131+
f'Could not generate a schema for tool "{name}". '
132+
"Tool functions must have type hints that are compatible with Pydantic."
133+
)
130134

131135
# load parameter descriptions
132136
if include_param_descriptions:
133137
for param in signature.parameters.values():
138+
# ensure we only try to add descriptions for parameters that exist in the schema
139+
if param.name not in parameters.get("properties", {}):
140+
continue
141+
134142
# handle Annotated type hints
135143
if typing.get_origin(param.annotation) is Annotated:
136144
param_description = " ".join(

tests/tools/test_tools.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
import controlflow
88
from controlflow.agents.agent import Agent
99
from controlflow.llm.messages import ToolMessage
10-
from controlflow.tools.tools import (
11-
Tool,
12-
handle_tool_call,
13-
tool,
14-
)
10+
from controlflow.tools.tools import Tool, handle_tool_call, tool
1511

1612

1713
@pytest.mark.parametrize("style", ["decorator", "class"])
@@ -170,6 +166,77 @@ def add(a: int, b: float) -> float:
170166
elif style == "decorator":
171167
tool(add)
172168

169+
def test_custom_parameters(self, style):
170+
"""Test that custom parameters override generated ones."""
171+
172+
def add(a: int, b: float):
173+
return a + b
174+
175+
custom_params = {
176+
"type": "object",
177+
"properties": {
178+
"x": {"type": "number", "description": "Custom parameter"},
179+
"y": {"type": "string"},
180+
},
181+
"required": ["x"],
182+
}
183+
184+
if style == "class":
185+
tool_obj = Tool.from_function(add, parameters=custom_params)
186+
elif style == "decorator":
187+
tool_obj = tool(add, parameters=custom_params)
188+
189+
assert tool_obj.parameters == custom_params
190+
assert "a" not in tool_obj.parameters["properties"]
191+
assert "b" not in tool_obj.parameters["properties"]
192+
assert (
193+
tool_obj.parameters["properties"]["x"]["description"] == "Custom parameter"
194+
)
195+
196+
def test_custom_parameters_with_annotations(self, style):
197+
"""Test that annotations still work with custom parameters if param names match."""
198+
199+
def process(x: Annotated[float, "The x value"], y: str):
200+
return x
201+
202+
custom_params = {
203+
"type": "object",
204+
"properties": {"x": {"type": "number"}, "y": {"type": "string"}},
205+
"required": ["x"],
206+
}
207+
208+
if style == "class":
209+
tool_obj = Tool.from_function(process, parameters=custom_params)
210+
elif style == "decorator":
211+
tool_obj = tool(process, parameters=custom_params)
212+
213+
assert tool_obj.parameters["properties"]["x"]["description"] == "The x value"
214+
assert "description" not in tool_obj.parameters["properties"]["y"]
215+
216+
def test_custom_parameters_ignore_descriptions(self, style):
217+
"""Test that include_param_descriptions=False works with custom parameters."""
218+
219+
def process(x: Annotated[float, "The x value"], y: str):
220+
return x
221+
222+
custom_params = {
223+
"type": "object",
224+
"properties": {"x": {"type": "number"}, "y": {"type": "string"}},
225+
"required": ["x"],
226+
}
227+
228+
if style == "class":
229+
tool_obj = Tool.from_function(
230+
process, parameters=custom_params, include_param_descriptions=False
231+
)
232+
elif style == "decorator":
233+
tool_obj = tool(
234+
process, parameters=custom_params, include_param_descriptions=False
235+
)
236+
237+
assert "description" not in tool_obj.parameters["properties"]["x"]
238+
assert "description" not in tool_obj.parameters["properties"]["y"]
239+
173240

174241
class TestToolFunctions:
175242
def test_non_serializable_return_value(self):

0 commit comments

Comments
 (0)