diff --git a/python/copilot/tools.py b/python/copilot/tools.py index 762b79c45..2de29584f 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -351,7 +351,11 @@ def _normalize_result(result: Any) -> ToolResult: # Everything else gets JSON-serialized (with Pydantic model support) def default(obj: Any) -> Any: if isinstance(obj, BaseModel): - return obj.model_dump() + # mode="json" coerces datetime/UUID/Decimal/Enum fields to JSON-native + # types. The default mode="python" leaves them as native objects, which + # json.dumps then passes back into this hook and we reject as + # unserializable -- turning an ordinary tool result into a failure. + return obj.model_dump(mode="json") raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") try: diff --git a/python/test_tools.py b/python/test_tools.py index 646f17c03..fa8329cd1 100644 --- a/python/test_tools.py +++ b/python/test_tools.py @@ -1,6 +1,10 @@ """Unit tests for define_tool""" +import datetime +import decimal +import enum import json +import uuid import pytest from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -389,6 +393,33 @@ class Item(BaseModel): assert parsed == [{"name": "a", "value": 1}, {"name": "b", "value": 2}] assert result.result_type == "success" + def test_pydantic_model_with_non_primitive_fields_is_serialized(self): + class IssueState(enum.StrEnum): + OPEN = "open" + CLOSED = "closed" + + class Issue(BaseModel): + id: uuid.UUID + created_at: datetime.datetime + price: decimal.Decimal + state: IssueState + + issue = Issue( + id=uuid.UUID("6b686a99-0000-4000-8000-000000000000"), + created_at=datetime.datetime(2026, 8, 1, 1, 49, 10), + price=decimal.Decimal("1.5"), + state=IssueState.OPEN, + ) + result = _normalize_result(issue) + parsed = json.loads(result.text_result_for_llm) + assert parsed == { + "id": "6b686a99-0000-4000-8000-000000000000", + "created_at": "2026-08-01T01:49:10", + "price": "1.5", + "state": "open", + } + assert result.result_type == "success" + def test_raises_for_unserializable_value(self): # Functions cannot be JSON serialized with pytest.raises(TypeError, match="Failed to serialize"):