Repository: stanfordnlp/dspy

Issue:
[Bug] New defaults handling for Tools creates TypeError if assigned "Any"
### What happened?

`TypeError: 'str' object does not support item assignment` occurs a tool argument has a default value but no type hint: https://github.com/stanfordnlp/dspy/blob/main/dspy/primitives/tool.py#L122

### Steps to reproduce

```
def my_tool(limit = 120):
    return True

agent  = dspy.ReAct(SomeSig, tools=[my_tool])
```
Gives the following error:
```
File ...dspy/predict/react.py:24, in ReAct.__init__(self, signature, tools, max_iters)
     21 self.signature = signature = ensure_signature(signature)
     22 self.max_iters = max_iters
---> 24 tools = [t if isinstance(t, Tool) else Tool(t) for t in tools]
     25 tools = {tool.name: tool for tool in tools}
     27 inputs = ", ".join([f"`{k}`" for k in signature.input_fields.keys()])

File .../dspy/primitives/tool.py:61, in Tool.__init__(self, func, name, desc, args, arg_types, arg_desc)
     58 self.arg_types = arg_types
     59 self.arg_desc = arg_desc
---> 61 self._parse_function(func, arg_desc)

File .../dspy/primitives/tool.py:124, in Tool._parse_function(self, func, arg_desc)
    122     args[k] = TypeAdapter(v).json_schema() or "Any"
    123 if default_values[k] is not inspect.Parameter.empty:
--> 124     args[k]["default"] = default_values[k]
    125 if arg_desc and k in arg_desc:
    126     args[k]["description"] = arg_desc[k]

TypeError: 'str' object does not support item assignment
```
Adding a type hint does not give the error, e.g. `def my_tool(limit: int = 120):`

### DSPy version

2.6.5

Discussion:
@okhat: Ah. Not sure how we missed this in our tests. Thanks @jason-faro — mind making a PR?@chenmoneygithub: Thank for reporting the issue! I made a fix #8089 

Solution patch:
diff --git a/dspy/primitives/tool.py b/dspy/primitives/tool.py
index 3e9a718228..8b3d1fa9e9 100644
--- a/dspy/primitives/tool.py
+++ b/dspy/primitives/tool.py
@@ -119,7 +119,7 @@ def _parse_function(self, func: Callable, arg_desc: dict[str, str] = None):
                 v_json_schema = self._resolve_pydantic_schema(v)
                 args[k] = v_json_schema
             else:
-                args[k] = TypeAdapter(v).json_schema() or "Any"
+                args[k] = TypeAdapter(v).json_schema()
             if default_values[k] is not inspect.Parameter.empty:
                 args[k]["default"] = default_values[k]
             if arg_desc and k in arg_desc:
@@ -137,7 +137,8 @@ def __call__(self, **kwargs):
                 raise ValueError(f"Arg {k} is not in the tool's args.")
             try:
                 instance = v.model_dump() if hasattr(v, "model_dump") else v
-                if not self.args[k] == "Any":
+                type_str = self.args[k].get("type")
+                if type_str is not None and type_str != "Any":
                     validate(instance=instance, schema=self.args[k])
             except ValidationError as e:
                 raise ValueError(f"Arg {k} is invalid: {e.message}")


Test patch:
diff --git a/tests/primitives/test_tool.py b/tests/primitives/test_tool.py
index 6b49f24f3f..02e8170be1 100644
--- a/tests/primitives/test_tool.py
+++ b/tests/primitives/test_tool.py
@@ -150,3 +150,12 @@ def test_invalid_function_call():
 def test_parameter_desc():
     tool = Tool(dummy_function, arg_desc={"x": "The x parameter"})
     assert tool.args["x"]["description"] == "The x parameter"
+
+
+def test_tool_with_default_args_without_type_hints():
+    def foo(x=100):
+        return x
+
+    tool = Tool(foo)
+    assert tool.args["x"]["default"] == 100
+    assert not hasattr(tool.args["x"], "type")