Skip to content

Commit cc18ce9

Browse files
chiehmin-weiclaude
andcommitted
Add exclude_params feature to function_tool
This commit adds the exclude_params feature to function_tool and function_schema, allowing users to exclude specific parameters from the JSON schema presented to the LLM while still making them available to the function with their default values. - Added exclude_params parameter to function_schema and function_tool - Modified parameter processing to skip excluded parameters - Added validation to ensure excluded parameters have default values - Updated documentation in tools.md with examples - Added tests for the new feature 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 47fa8e8 commit cc18ce9

File tree

5 files changed

+202
-3
lines changed

5 files changed

+202
-3
lines changed

docs/tools.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ for tool in agent.tools:
100100

101101
1. You can use any Python types as arguments to your functions, and the function can be sync or async.
102102
2. Docstrings, if present, are used to capture descriptions and argument descriptions
103-
3. Functions can optionally take the `context` (must be the first argument). You can also set overrides, like the name of the tool, description, which docstring style to use, etc.
103+
3. Functions can optionally take the `context` (must be the first argument). You can also set overrides, like the name of the tool, description, which docstring style to use, and exclude specific parameters from the schema, etc.
104104
4. You can pass the decorated functions to the list of tools.
105105

106106
??? note "Expand to see output"
@@ -284,6 +284,45 @@ async def run_my_agent() -> str:
284284
return str(result.final_output)
285285
```
286286

287+
## Excluding parameters from the schema
288+
289+
Sometimes, you might want to exclude certain parameters from the JSON schema that's presented to the LLM, while still making them available to your function with their default values. This can be useful for:
290+
291+
- Keeping implementation details hidden from the LLM
292+
- Simplifying the tool interface presented to the model
293+
- Maintaining backward compatibility when adding new parameters
294+
- Supporting internal parameters that should always use default values
295+
296+
You can do this using the `exclude_params` parameter of the `@function_tool` decorator:
297+
298+
```python
299+
from typing import Optional
300+
from agents import function_tool, RunContextWrapper
301+
302+
@function_tool(exclude_params=["timestamp", "internal_id"])
303+
def search_database(
304+
query: str,
305+
limit: int = 10,
306+
timestamp: Optional[str] = None,
307+
internal_id: Optional[str] = None
308+
) -> str:
309+
"""
310+
Search the database for records matching the query.
311+
312+
Args:
313+
query: The search query string
314+
limit: Maximum number of results to return
315+
timestamp: The timestamp to use for the search (hidden from schema)
316+
internal_id: Internal tracking ID for telemetry (hidden from schema)
317+
"""
318+
# Implementation...
319+
```
320+
321+
In this example:
322+
- The LLM will only see `query` and `limit` parameters in the tool schema
323+
- `timestamp` and `internal_id` will be automatically set to their default values when the function runs
324+
- All excluded parameters must have default values (either `None` or a specific value)
325+
287326
## Handling errors in function tools
288327

289328
When you create a function tool via `@function_tool`, you can pass a `failure_error_function`. This is a function that provides an error response to the LLM in case the tool call crashes.

src/agents/function_schema.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,22 @@ def to_call_args(self, data: BaseModel) -> tuple[list[Any], dict[str, Any]]:
4545
positional_args: list[Any] = []
4646
keyword_args: dict[str, Any] = {}
4747
seen_var_positional = False
48+
49+
# Get excluded parameter defaults if they exist
50+
excluded_param_defaults = getattr(self.params_pydantic_model, "__excluded_param_defaults__", {})
4851

4952
# Use enumerate() so we can skip the first parameter if it's context.
5053
for idx, (name, param) in enumerate(self.signature.parameters.items()):
5154
# If the function takes a RunContextWrapper and this is the first parameter, skip it.
5255
if self.takes_context and idx == 0:
5356
continue
5457

55-
value = getattr(data, name, None)
58+
# For excluded parameters, use their default value
59+
if name in excluded_param_defaults:
60+
value = excluded_param_defaults[name]
61+
else:
62+
value = getattr(data, name, None)
63+
5664
if param.kind == param.VAR_POSITIONAL:
5765
# e.g. *args: extend positional args and mark that *args is now seen
5866
positional_args.extend(value or [])
@@ -190,6 +198,7 @@ def function_schema(
190198
description_override: str | None = None,
191199
use_docstring_info: bool = True,
192200
strict_json_schema: bool = True,
201+
exclude_params: list[str] | None = None,
193202
) -> FuncSchema:
194203
"""
195204
Given a python function, extracts a `FuncSchema` from it, capturing the name, description,
@@ -208,6 +217,9 @@ def function_schema(
208217
the schema adheres to the "strict" standard the OpenAI API expects. We **strongly**
209218
recommend setting this to True, as it increases the likelihood of the LLM providing
210219
correct JSON input.
220+
exclude_params: If provided, these parameters will be excluded from the JSON schema
221+
presented to the LLM. The parameters will still be available to the function with
222+
their default values. All excluded parameters must have default values.
211223
212224
Returns:
213225
A `FuncSchema` object containing the function's name, description, parameter descriptions,
@@ -231,11 +243,24 @@ def function_schema(
231243
takes_context = False
232244
filtered_params = []
233245

246+
# Store default values for excluded parameters
247+
excluded_param_defaults = {}
248+
234249
if params:
235250
first_name, first_param = params[0]
236251
# Prefer the evaluated type hint if available
237252
ann = type_hints.get(first_name, first_param.annotation)
238-
if ann != inspect._empty:
253+
254+
# Check if this parameter should be excluded
255+
if exclude_params and first_name in exclude_params:
256+
# Ensure the parameter has a default value
257+
if first_param.default is inspect._empty:
258+
raise UserError(
259+
f"Parameter '{first_name}' specified in exclude_params must have a default value"
260+
)
261+
# Store default value
262+
excluded_param_defaults[first_name] = first_param.default
263+
elif ann != inspect._empty:
239264
origin = get_origin(ann) or ann
240265
if origin is RunContextWrapper:
241266
takes_context = True # Mark that the function takes context
@@ -246,6 +271,17 @@ def function_schema(
246271

247272
# For parameters other than the first, raise error if any use RunContextWrapper.
248273
for name, param in params[1:]:
274+
# Check if this parameter should be excluded
275+
if exclude_params and name in exclude_params:
276+
# Ensure the parameter has a default value
277+
if param.default is inspect._empty:
278+
raise UserError(
279+
f"Parameter '{name}' specified in exclude_params must have a default value"
280+
)
281+
# Store default value
282+
excluded_param_defaults[name] = param.default
283+
continue
284+
249285
ann = type_hints.get(name, param.annotation)
250286
if ann != inspect._empty:
251287
origin = get_origin(ann) or ann
@@ -326,6 +362,9 @@ def function_schema(
326362

327363
# 3. Dynamically build a Pydantic model
328364
dynamic_model = create_model(f"{func_name}_args", __base__=BaseModel, **fields)
365+
366+
# Store excluded parameter defaults in the model for later use
367+
setattr(dynamic_model, "__excluded_param_defaults__", excluded_param_defaults)
329368

330369
# 4. Build JSON schema from that model
331370
json_schema = dynamic_model.model_json_schema()

src/agents/tool.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ def function_tool(
262262
use_docstring_info: bool = True,
263263
failure_error_function: ToolErrorFunction | None = None,
264264
strict_mode: bool = True,
265+
exclude_params: list[str] | None = None,
265266
) -> FunctionTool:
266267
"""Overload for usage as @function_tool (no parentheses)."""
267268
...
@@ -276,6 +277,7 @@ def function_tool(
276277
use_docstring_info: bool = True,
277278
failure_error_function: ToolErrorFunction | None = None,
278279
strict_mode: bool = True,
280+
exclude_params: list[str] | None = None,
279281
) -> Callable[[ToolFunction[...]], FunctionTool]:
280282
"""Overload for usage as @function_tool(...)."""
281283
...
@@ -290,6 +292,7 @@ def function_tool(
290292
use_docstring_info: bool = True,
291293
failure_error_function: ToolErrorFunction | None = default_tool_error_function,
292294
strict_mode: bool = True,
295+
exclude_params: list[str] | None = None,
293296
) -> FunctionTool | Callable[[ToolFunction[...]], FunctionTool]:
294297
"""
295298
Decorator to create a FunctionTool from a function. By default, we will:
@@ -318,16 +321,34 @@ def function_tool(
318321
If False, it allows non-strict JSON schemas. For example, if a parameter has a default
319322
value, it will be optional, additional properties are allowed, etc. See here for more:
320323
https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas
324+
exclude_params: If provided, these parameters will be excluded from the JSON schema
325+
presented to the LLM. The parameters will still be available to the function with
326+
their default values. All excluded parameters must have default values.
321327
"""
322328

323329
def _create_function_tool(the_func: ToolFunction[...]) -> FunctionTool:
330+
# Check that all excluded parameters have default values
331+
if exclude_params:
332+
sig = inspect.signature(the_func)
333+
for param_name in exclude_params:
334+
if param_name not in sig.parameters:
335+
raise UserError(
336+
f"Parameter '{param_name}' specified in exclude_params doesn't exist in function {the_func.__name__}"
337+
)
338+
param = sig.parameters[param_name]
339+
if param.default is inspect._empty:
340+
raise UserError(
341+
f"Parameter '{param_name}' specified in exclude_params must have a default value"
342+
)
343+
324344
schema = function_schema(
325345
func=the_func,
326346
name_override=name_override,
327347
description_override=description_override,
328348
docstring_style=docstring_style,
329349
use_docstring_info=use_docstring_info,
330350
strict_json_schema=strict_mode,
351+
exclude_params=exclude_params,
331352
)
332353

333354
async def _on_invoke_tool_impl(ctx: RunContextWrapper[Any], input: str) -> Any:

tests/test_function_schema.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,67 @@ def func_with_mapping(test_one: Mapping[str, int]) -> str:
439439

440440
with pytest.raises(UserError):
441441
function_schema(func_with_mapping)
442+
443+
444+
def function_with_optional_params(a: int, b: int = 5, c: str = "default"):
445+
"""Function with multiple optional parameters."""
446+
return f"{a}-{b}-{c}"
447+
448+
449+
def test_exclude_params_feature():
450+
"""Test the exclude_params feature works correctly."""
451+
# Test excluding a single optional parameter
452+
func_schema = function_schema(
453+
function_with_optional_params,
454+
exclude_params=["c"],
455+
)
456+
457+
# Verify 'c' is not in the schema properties
458+
assert "c" not in func_schema.params_json_schema.get("properties", {})
459+
460+
# Verify the excluded parameter defaults are stored
461+
excluded_defaults = getattr(func_schema.params_pydantic_model, "__excluded_param_defaults__", {})
462+
assert "c" in excluded_defaults
463+
assert excluded_defaults["c"] == "default"
464+
465+
# Test function still works correctly with excluded parameter
466+
valid_input = {"a": 10, "b": 20}
467+
parsed = func_schema.params_pydantic_model(**valid_input)
468+
args, kwargs_dict = func_schema.to_call_args(parsed)
469+
result = function_with_optional_params(*args, **kwargs_dict)
470+
assert result == "10-20-default" # 'c' should use its default value
471+
472+
# Test excluding multiple parameters
473+
func_schema_multi = function_schema(
474+
function_with_optional_params,
475+
exclude_params=["b", "c"],
476+
)
477+
478+
# Verify both 'b' and 'c' are not in the schema properties
479+
assert "b" not in func_schema_multi.params_json_schema.get("properties", {})
480+
assert "c" not in func_schema_multi.params_json_schema.get("properties", {})
481+
482+
# Test function still works correctly with multiple excluded parameters
483+
valid_input = {"a": 10}
484+
parsed = func_schema_multi.params_pydantic_model(**valid_input)
485+
args, kwargs_dict = func_schema_multi.to_call_args(parsed)
486+
result = function_with_optional_params(*args, **kwargs_dict)
487+
assert result == "10-5-default" # 'b' and 'c' should use their default values
488+
489+
490+
def function_with_required_param(a: int, b: str):
491+
"""Function with required parameters only."""
492+
return f"{a}-{b}"
493+
494+
495+
def test_exclude_params_requires_default_value():
496+
"""Test that excluding a parameter without a default value raises an error."""
497+
# Attempt to exclude a parameter without a default value
498+
with pytest.raises(UserError) as excinfo:
499+
function_schema(
500+
function_with_required_param,
501+
exclude_params=["b"],
502+
)
503+
504+
# Check the error message
505+
assert "must have a default value" in str(excinfo.value)

tests/test_function_tool_decorator.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,39 @@ async def test_extract_descriptions_from_docstring():
233233
"additionalProperties": False,
234234
}
235235
)
236+
237+
238+
@function_tool(exclude_params=["timestamp"])
239+
def function_with_excluded_param(
240+
city: str, country: str = "US", timestamp: Optional[str] = None
241+
) -> str:
242+
"""Get the weather for a given city with timestamp.
243+
244+
Args:
245+
city: The city to get the weather for.
246+
country: The country the city is in.
247+
timestamp: The timestamp for the weather data (hidden from schema).
248+
"""
249+
time_str = f" at {timestamp}" if timestamp else ""
250+
return f"The weather in {city}, {country}{time_str} is sunny."
251+
252+
253+
@pytest.mark.asyncio
254+
async def test_exclude_params_from_schema():
255+
"""Test that excluded parameters are not included in the schema."""
256+
tool = function_with_excluded_param
257+
258+
# Check that the parameter is not in the schema
259+
assert "timestamp" not in tool.params_json_schema.get("properties", {})
260+
261+
# Check that only non-excluded parameters are required
262+
assert set(tool.params_json_schema.get("required", [])) == {"city"}
263+
264+
# Test function still works with excluded parameter
265+
input_data = {"city": "Seattle", "country": "US"}
266+
output = await tool.on_invoke_tool(ctx_wrapper(), json.dumps(input_data))
267+
assert output == "The weather in Seattle, US is sunny."
268+
269+
# Test function works when we supply a default excluded parameter value in the code
270+
function_result = function_with_excluded_param("Seattle", "US", "2023-05-29T12:00:00Z")
271+
assert function_result == "The weather in Seattle, US at 2023-05-29T12:00:00Z is sunny."

0 commit comments

Comments
 (0)