fix edge case casting and more test cases

This commit is contained in:
Barry Wang
2026-03-05 16:51:09 +08:00
parent 9e42ccb51e
commit 667613d594
2 changed files with 150 additions and 6 deletions

View File

@@ -57,7 +57,8 @@ class Tool(ABC):
def cast_params(self, params: dict[str, Any]) -> dict[str, Any]: def cast_params(self, params: dict[str, Any]) -> dict[str, Any]:
""" """
Attempt to cast parameters to match schema types. Attempt to cast parameters to match schema types.
Returns modified params dict. Raises ValueError if casting is impossible. Returns modified params dict. If casting fails, returns original value
and logs a debug message, allowing validation to catch the error.
""" """
schema = self.parameters or {} schema = self.parameters or {}
if schema.get("type", "object") != "object": if schema.get("type", "object") != "object":
@@ -107,7 +108,7 @@ class Tool(ABC):
if target_type == "integer": if target_type == "integer":
if isinstance(val, bool): if isinstance(val, bool):
# Don't silently convert bool to int # Don't silently convert bool to int
raise ValueError(f"Cannot cast bool to integer") raise ValueError("Cannot cast bool to integer")
if isinstance(val, str): if isinstance(val, str):
return int(val) return int(val)
if isinstance(val, (int, float)): if isinstance(val, (int, float)):
@@ -116,7 +117,7 @@ class Tool(ABC):
elif target_type == "number": elif target_type == "number":
if isinstance(val, bool): if isinstance(val, bool):
# Don't silently convert bool to number # Don't silently convert bool to number
raise ValueError(f"Cannot cast bool to number") raise ValueError("Cannot cast bool to number")
if isinstance(val, str): if isinstance(val, str):
return float(val) return float(val)
if isinstance(val, (int, float)): if isinstance(val, (int, float)):
@@ -130,7 +131,13 @@ class Tool(ABC):
elif target_type == "boolean": elif target_type == "boolean":
if isinstance(val, str): if isinstance(val, str):
return val.lower() in ("true", "1", "yes") val_lower = val.lower()
if val_lower in ("true", "1", "yes"):
return True
elif val_lower in ("false", "0", "no"):
return False
# For other strings, raise error to let validation handle it
raise ValueError(f"Cannot convert string '{val}' to boolean")
return bool(val) return bool(val)
elif target_type == "array": elif target_type == "array":
@@ -142,10 +149,11 @@ class Tool(ABC):
# Preserve None vs empty array distinction # Preserve None vs empty array distinction
if val is None: if val is None:
return val return val
# Try to convert single value to array # Empty string → empty array
if val == "": if val == "":
return [] return []
return [val] # Don't auto-wrap single values, let validation catch the error
raise ValueError(f"Cannot convert {type(val).__name__} to array")
elif target_type == "object": elif target_type == "object":
if isinstance(val, dict): if isinstance(val, dict):

View File

@@ -225,3 +225,139 @@ def test_cast_params_preserves_empty_string() -> None:
) )
result = tool.cast_params({"name": ""}) result = tool.cast_params({"name": ""})
assert result["name"] == "" assert result["name"] == ""
def test_cast_params_bool_string_false() -> None:
"""Test that 'false', '0', 'no' strings convert to False."""
tool = CastTestTool(
{
"type": "object",
"properties": {"flag": {"type": "boolean"}},
}
)
assert tool.cast_params({"flag": "false"})["flag"] is False
assert tool.cast_params({"flag": "False"})["flag"] is False
assert tool.cast_params({"flag": "0"})["flag"] is False
assert tool.cast_params({"flag": "no"})["flag"] is False
assert tool.cast_params({"flag": "NO"})["flag"] is False
def test_cast_params_bool_string_invalid() -> None:
"""Invalid boolean strings should not be cast."""
tool = CastTestTool(
{
"type": "object",
"properties": {"flag": {"type": "boolean"}},
}
)
# Invalid strings should be preserved (validation will catch them)
result = tool.cast_params({"flag": "random"})
assert result["flag"] == "random"
result = tool.cast_params({"flag": "maybe"})
assert result["flag"] == "maybe"
def test_cast_params_invalid_string_to_int() -> None:
"""Invalid strings should not be cast to integer."""
tool = CastTestTool(
{
"type": "object",
"properties": {"count": {"type": "integer"}},
}
)
result = tool.cast_params({"count": "abc"})
assert result["count"] == "abc" # Original value preserved
result = tool.cast_params({"count": "12.5.7"})
assert result["count"] == "12.5.7"
def test_cast_params_invalid_string_to_number() -> None:
"""Invalid strings should not be cast to number."""
tool = CastTestTool(
{
"type": "object",
"properties": {"rate": {"type": "number"}},
}
)
result = tool.cast_params({"rate": "not_a_number"})
assert result["rate"] == "not_a_number"
def test_cast_params_none_values() -> None:
"""Test None handling for different types."""
tool = CastTestTool(
{
"type": "object",
"properties": {
"name": {"type": "string"},
"count": {"type": "integer"},
"items": {"type": "array"},
"config": {"type": "object"},
},
}
)
result = tool.cast_params(
{
"name": None,
"count": None,
"items": None,
"config": None,
}
)
# None should be preserved for all types
assert result["name"] is None
assert result["count"] is None
assert result["items"] is None
assert result["config"] is None
def test_cast_params_single_value_not_auto_wrapped_to_array() -> None:
"""Single values should NOT be automatically wrapped into arrays."""
tool = CastTestTool(
{
"type": "object",
"properties": {"items": {"type": "array"}},
}
)
# Non-array values should be preserved (validation will catch them)
result = tool.cast_params({"items": 5})
assert result["items"] == 5 # Not wrapped to [5]
result = tool.cast_params({"items": "text"})
assert result["items"] == "text" # Not wrapped to ["text"]
def test_cast_params_empty_string_to_array() -> None:
"""Empty string should convert to empty array."""
tool = CastTestTool(
{
"type": "object",
"properties": {"items": {"type": "array"}},
}
)
result = tool.cast_params({"items": ""})
assert result["items"] == []
def test_cast_params_empty_string_to_object() -> None:
"""Empty string should convert to empty object."""
tool = CastTestTool(
{
"type": "object",
"properties": {"config": {"type": "object"}},
}
)
result = tool.cast_params({"config": ""})
assert result["config"] == {}
def test_cast_params_float_to_int() -> None:
"""Float values should be cast to integers."""
tool = CastTestTool(
{
"type": "object",
"properties": {"count": {"type": "integer"}},
}
)
result = tool.cast_params({"count": 42.7})
assert result["count"] == 42
assert isinstance(result["count"], int)