From 09c027692f1aeb0afba9459c75d9db311a18cfce Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Sun, 7 Dec 2025 14:19:25 -0800 Subject: [PATCH] tests: assistant msg validation error (#6536) * add regression test for dict content in AssistantMessage Tests the fix for pydantic validation error when send_message tool returns dict content like {'tofu': 1, 'mofu': 1, 'bofu': 1}. The test verifies that dict content is properly serialized to JSON string before creating AssistantMessage. * improve type annotation for validate_function_response Changed return type from Any to str | dict[str, Any] to match actual behavior. This enables static type checkers (pyright, mypy) to catch type mismatches like the AssistantMessage bug. With proper type annotations, pyright would have caught: error: Argument of type "str | dict[str, Any]" cannot be assigned to parameter "content" of type "str" This prevents future bugs where dict is passed to string-only fields. * add regression test for dict content in AssistantMessage Moved test into existing test_message_manager.py suite alongside other message conversion tests. Tests the fix for pydantic validation error when send_message tool returns dict content like {'tofu': 1, 'mofu': 1, 'bofu': 1}. The test verifies that dict content is properly serialized to JSON string before creating AssistantMessage. --- letta/utils.py | 4 +- tests/managers/test_message_manager.py | 67 ++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/letta/utils.py b/letta/utils.py index 02c0ff23..7525ca84 100644 --- a/letta/utils.py +++ b/letta/utils.py @@ -854,7 +854,9 @@ def parse_json(string) -> dict: raise e -def validate_function_response(function_response: Any, return_char_limit: int, strict: bool = False, truncate: bool = True) -> Any: +def validate_function_response( + function_response: Any, return_char_limit: int, strict: bool = False, truncate: bool = True +) -> str | dict[str, Any]: """Check to make sure that a function used by Letta returned a valid response. Truncates to return_char_limit if necessary. This makes sure that we can coerce the function_response into a string or dict that meets our criteria. We handle some soft coercion. diff --git a/tests/managers/test_message_manager.py b/tests/managers/test_message_manager.py index 06b4e967..b62b32a8 100644 --- a/tests/managers/test_message_manager.py +++ b/tests/managers/test_message_manager.py @@ -1019,3 +1019,70 @@ async def test_convert_tool_calls_only_assistant_tools(server: SyncServer, sarah # check assistant messages content (they appear in reverse order) assert letta_messages[0].content == "Second message" assert letta_messages[1].content == "First message" + + +@pytest.mark.asyncio +async def test_convert_assistant_message_with_dict_content(server: SyncServer, sarah_agent, default_user): + """Test that send_message with dict content is properly serialized to JSON string + + Regression test for bug where dict content like {'tofu': 1, 'mofu': 1, 'bofu': 1} + caused pydantic validation error because AssistantMessage.content expects a string. + """ + import json + + # Test case 1: Simple dict as message content + tool_calls = [ + OpenAIToolCall( + id="call_1", + type="function", + function=OpenAIFunction(name="send_message", arguments='{"message": {"tofu": 1, "mofu": 1, "bofu": 1}}'), + ), + ] + + message = PydanticMessage( + agent_id=sarah_agent.id, + role=MessageRole.assistant, + content=[TextContent(text="Sending structured data...")], + tool_calls=tool_calls, + ) + + # convert with assistant mode - should not raise validation error + letta_messages = message.to_letta_messages(use_assistant_message=True) + + assert len(letta_messages) == 2 + assert letta_messages[0].message_type == "assistant_message" + assert letta_messages[1].message_type == "reasoning_message" + + # check that dict was serialized to JSON string + assistant_msg = letta_messages[0] + assert isinstance(assistant_msg.content, str) + + # verify the JSON-serialized content can be parsed back + parsed_content = json.loads(assistant_msg.content) + assert parsed_content == {"tofu": 1, "mofu": 1, "bofu": 1} + + # Test case 2: Nested dict with various types + tool_calls_nested = [ + OpenAIToolCall( + id="call_2", + type="function", + function=OpenAIFunction( + name="send_message", + arguments='{"message": {"status": "success", "data": {"count": 42, "items": ["a", "b"]}, "meta": null}}', + ), + ), + ] + + message_nested = PydanticMessage( + agent_id=sarah_agent.id, + role=MessageRole.assistant, + content=[TextContent(text="Sending complex data...")], + tool_calls=tool_calls_nested, + ) + + letta_messages_nested = message_nested.to_letta_messages(use_assistant_message=True) + assistant_msg_nested = letta_messages_nested[0] + + assert isinstance(assistant_msg_nested.content, str) + parsed_nested = json.loads(assistant_msg_nested.content) + assert parsed_nested == {"status": "success", "data": {"count": 42, "items": ["a", "b"]}, "meta": None}