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.
This commit is contained in:
Kian Jones
2025-12-07 14:19:25 -08:00
committed by Caren Thomas
parent a18caf69f7
commit 09c027692f
2 changed files with 70 additions and 1 deletions

View File

@@ -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.

View File

@@ -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}