Skip to content

Commit 8068e01

Browse files
authored
fix: Remove argument sanitization (#3364)
## Summary Found a safer way to do this that won't break arguments that shouldn't be sanitized ## Type of change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] Tests added/updated (if applicable) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.)
1 parent e7d0ab8 commit 8068e01

File tree

7 files changed

+198
-17
lines changed

7 files changed

+198
-17
lines changed

libs/agno/agno/tools/apify.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def actor_function(**kwargs) -> str:
156156
actor_function.__doc__ = docstring
157157

158158
# Register the function with the toolkit
159-
self.register(actor_function, sanitize_arguments=False)
159+
self.register(actor_function)
160160
# Fix params schema
161161
self.functions[tool_name].parameters = props_to_json_schema(properties, required)
162162
log_info(f"Registered Apify Actor '{actor_id}' as function '{tool_name}'")

libs/agno/agno/tools/decorator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def tool(*args, **kwargs) -> Union[Function, Callable[[F], Function]]:
4848
name: Optional[str] - Override for the function name
4949
description: Optional[str] - Override for the function description
5050
strict: Optional[bool] - Flag for strict parameter checking
51-
sanitize_arguments: Optional[bool] - If True, arguments are sanitized before passing to function
51+
sanitize_arguments: Optional[bool] - If True, arguments are sanitized before passing to function (Deprecated)
5252
instructions: Optional[str] - Instructions for using the tool
5353
add_instructions: bool - If True, add instructions to the system message
5454
show_result: Optional[bool] - If True, shows the result after function call

libs/agno/agno/tools/function.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ class Function(BaseModel):
8383
entrypoint: Optional[Callable] = None
8484
# If True, the entrypoint processing is skipped and the Function is used as is.
8585
skip_entrypoint_processing: bool = False
86-
# If True, the arguments are sanitized before being passed to the function.
87-
sanitize_arguments: bool = True
86+
# If True, the arguments are sanitized before being passed to the function. (Deprecated)
87+
sanitize_arguments: bool = False
8888
# If True, the function call will show the result along with sending it to the model.
8989
show_result: bool = False
9090
# If True, the agent will stop after the function call.

libs/agno/agno/tools/toolkit.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,11 @@ def _register_tools(self) -> None:
105105
for tool in self.tools:
106106
self.register(tool)
107107

108-
def register(self, function: Callable[..., Any], sanitize_arguments: bool = True, name: Optional[str] = None):
108+
def register(self, function: Callable[..., Any], name: Optional[str] = None):
109109
"""Register a function with the toolkit.
110110
111111
Args:
112112
function: The callable to register
113-
sanitize_arguments: Whether to sanitize arguments before passing to the function
114113
name: Optional custom name for the function
115114
116115
Returns:
@@ -126,7 +125,6 @@ def register(self, function: Callable[..., Any], sanitize_arguments: bool = True
126125
f = Function(
127126
name=tool_name,
128127
entrypoint=function,
129-
sanitize_arguments=sanitize_arguments,
130128
cache_results=self.cache_results,
131129
cache_dir=self.cache_dir,
132130
cache_ttl=self.cache_ttl,

libs/agno/agno/utils/functions.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,12 @@ def get_function_call(
2828
function_call.call_id = call_id
2929
if arguments is not None and arguments != "":
3030
try:
31-
if function_to_call.sanitize_arguments:
32-
if "None" in arguments:
33-
arguments = arguments.replace("None", "null")
34-
if "True" in arguments:
35-
arguments = arguments.replace("True", "true")
36-
if "False" in arguments:
37-
arguments = arguments.replace("False", "false")
38-
_arguments = json.loads(arguments)
31+
try:
32+
_arguments = json.loads(arguments)
33+
except Exception:
34+
import ast
35+
36+
_arguments = ast.literal_eval(arguments)
3937
except Exception as e:
4038
log_error(f"Unable to decode function arguments:\n{arguments}\nError: {e}")
4139
function_call.error = (

libs/agno/tests/unit/tools/test_functions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,6 @@ def test_tool_decorator_with_config():
533533
strict=True,
534534
instructions="Custom instructions",
535535
add_instructions=False,
536-
sanitize_arguments=False,
537536
show_result=True,
538537
stop_after_tool_call=True,
539538
requires_confirmation=True,
@@ -551,7 +550,6 @@ def configured_func() -> str:
551550
assert configured_func.strict is True
552551
assert configured_func.instructions == "Custom instructions"
553552
assert configured_func.add_instructions is False
554-
assert configured_func.sanitize_arguments is False
555553
assert configured_func.show_result is True
556554
assert configured_func.stop_after_tool_call is True
557555
assert configured_func.requires_confirmation is True
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import json
2+
from typing import Dict
3+
4+
import pytest
5+
6+
from agno.tools.function import Function, FunctionCall
7+
from agno.utils.functions import get_function_call
8+
9+
10+
@pytest.fixture
11+
def sample_functions() -> Dict[str, Function]:
12+
return {
13+
"test_function": Function(
14+
name="test_function",
15+
description="A test function",
16+
parameters={
17+
"type": "object",
18+
"properties": {
19+
"param1": {"type": "string"},
20+
"param2": {"type": "integer"},
21+
"param3": {"type": "boolean"},
22+
},
23+
},
24+
),
25+
"test_function_2": Function(
26+
name="test_function_2",
27+
description="A test function 2",
28+
parameters={
29+
"type": "object",
30+
"properties": {
31+
"code": {"type": "string"},
32+
},
33+
},
34+
),
35+
}
36+
37+
38+
def test_get_function_call_basic(sample_functions):
39+
"""Test basic function call creation with valid arguments."""
40+
arguments = json.dumps({"param1": "test", "param2": 42, "param3": True})
41+
call_id = "test-call-123"
42+
43+
result = get_function_call(
44+
name="test_function",
45+
arguments=arguments,
46+
call_id=call_id,
47+
functions=sample_functions,
48+
)
49+
50+
assert result is not None
51+
assert isinstance(result, FunctionCall)
52+
assert result.function == sample_functions["test_function"]
53+
assert result.call_id == call_id
54+
assert result.arguments == {"param1": "test", "param2": 42, "param3": True}
55+
assert result.error is None
56+
57+
58+
def test_get_function_call_invalid_name(sample_functions):
59+
"""Test function call with non-existent function name."""
60+
result = get_function_call(
61+
name="non_existent_function",
62+
arguments='{"param1": "test"}',
63+
functions=sample_functions,
64+
)
65+
66+
assert result is None
67+
68+
69+
def test_get_function_call_no_functions():
70+
"""Test function call with no functions dictionary."""
71+
result = get_function_call(
72+
name="test_function",
73+
arguments='{"param1": "test"}',
74+
functions=None,
75+
)
76+
77+
assert result is None
78+
79+
80+
def test_get_function_call_invalid_json(sample_functions):
81+
"""Test function call with invalid JSON arguments."""
82+
result = get_function_call(
83+
name="test_function",
84+
arguments="invalid json",
85+
functions=sample_functions,
86+
)
87+
88+
assert result is not None
89+
assert result.error is not None
90+
assert "Error while decoding function arguments" in result.error
91+
92+
93+
def test_get_function_call_non_dict_arguments(sample_functions):
94+
"""Test function call with non-dictionary arguments."""
95+
result = get_function_call(
96+
name="test_function",
97+
arguments='["not", "a", "dict"]',
98+
functions=sample_functions,
99+
)
100+
101+
assert result is not None
102+
assert result.error is not None
103+
assert "Function arguments are not a valid JSON object" in result.error
104+
105+
106+
def test_get_function_call_argument(sample_functions):
107+
"""Test argument sanitization for boolean and null values."""
108+
arguments = json.dumps(
109+
{
110+
"param1": "None",
111+
"param2": "True",
112+
"param3": "False",
113+
"param4": " test ",
114+
}
115+
)
116+
117+
result = get_function_call(
118+
name="test_function",
119+
arguments=arguments,
120+
functions=sample_functions,
121+
)
122+
123+
assert result is not None
124+
assert result.arguments == {
125+
"param1": None,
126+
"param2": True,
127+
"param3": False,
128+
"param4": "test",
129+
}
130+
131+
132+
def test_get_function_call_argument_advanced(sample_functions):
133+
"""Test function call without argument sanitization."""
134+
arguments = '{"param1": None, "param2": True, "param3": False, "param4": "test"}'
135+
136+
result = get_function_call(
137+
name="test_function",
138+
arguments=arguments,
139+
functions=sample_functions,
140+
)
141+
142+
assert result is not None
143+
assert result.arguments == {
144+
"param1": None,
145+
"param2": True,
146+
"param3": False,
147+
"param4": "test",
148+
}
149+
150+
arguments = '{"code": "x = True; y = False; z = None;"}'
151+
152+
result = get_function_call(
153+
name="test_function_2",
154+
arguments=arguments,
155+
functions=sample_functions,
156+
)
157+
158+
assert result is not None
159+
assert result.arguments == {
160+
"code": "x = True; y = False; z = None;",
161+
}
162+
163+
164+
def test_get_function_call_empty_arguments(sample_functions):
165+
"""Test function call with empty arguments."""
166+
result = get_function_call(
167+
name="test_function",
168+
arguments="",
169+
functions=sample_functions,
170+
)
171+
172+
assert result is not None
173+
assert result.arguments is None
174+
assert result.error is None
175+
176+
177+
def test_get_function_call_no_arguments(sample_functions):
178+
"""Test function call with no arguments provided."""
179+
result = get_function_call(
180+
name="test_function",
181+
arguments=None,
182+
functions=sample_functions,
183+
)
184+
185+
assert result is not None
186+
assert result.arguments is None
187+
assert result.error is None

0 commit comments

Comments
 (0)