mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-10 09:14:45 +08:00
130 lines
4.8 KiB
Python
130 lines
4.8 KiB
Python
|
|
"""Regression tests for ToolMessage content normalization in serialization.
|
||
|
|
|
||
|
|
Ensures that structured content (list-of-blocks) is properly extracted to
|
||
|
|
plain text, preventing raw Python repr strings from reaching the UI.
|
||
|
|
|
||
|
|
See: https://github.com/bytedance/deer-flow/issues/1149
|
||
|
|
"""
|
||
|
|
|
||
|
|
from langchain_core.messages import ToolMessage
|
||
|
|
|
||
|
|
from deerflow.client import DeerFlowClient
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# _serialize_message
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
class TestSerializeToolMessageContent:
|
||
|
|
"""DeerFlowClient._serialize_message should normalize ToolMessage content."""
|
||
|
|
|
||
|
|
def test_string_content(self):
|
||
|
|
msg = ToolMessage(content="ok", tool_call_id="tc1", name="search")
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == "ok"
|
||
|
|
assert result["type"] == "tool"
|
||
|
|
|
||
|
|
def test_list_of_blocks_content(self):
|
||
|
|
"""List-of-blocks should be extracted, not repr'd."""
|
||
|
|
msg = ToolMessage(
|
||
|
|
content=[{"type": "text", "text": "hello world"}],
|
||
|
|
tool_call_id="tc1",
|
||
|
|
name="search",
|
||
|
|
)
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == "hello world"
|
||
|
|
# Must NOT contain Python repr artifacts
|
||
|
|
assert "[" not in result["content"]
|
||
|
|
assert "{" not in result["content"]
|
||
|
|
|
||
|
|
def test_multiple_text_blocks(self):
|
||
|
|
"""Multiple full text blocks should be joined with newlines."""
|
||
|
|
msg = ToolMessage(
|
||
|
|
content=[
|
||
|
|
{"type": "text", "text": "line 1"},
|
||
|
|
{"type": "text", "text": "line 2"},
|
||
|
|
],
|
||
|
|
tool_call_id="tc1",
|
||
|
|
name="search",
|
||
|
|
)
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == "line 1\nline 2"
|
||
|
|
|
||
|
|
def test_string_chunks_are_joined_without_newlines(self):
|
||
|
|
"""Chunked string payloads should not get artificial separators."""
|
||
|
|
msg = ToolMessage(
|
||
|
|
content=["{\"a\"", ": \"b\"}"] ,
|
||
|
|
tool_call_id="tc1",
|
||
|
|
name="search",
|
||
|
|
)
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == '{"a": "b"}'
|
||
|
|
|
||
|
|
def test_mixed_string_chunks_and_blocks(self):
|
||
|
|
"""String chunks stay contiguous, but text blocks remain separated."""
|
||
|
|
msg = ToolMessage(
|
||
|
|
content=["prefix", "-continued", {"type": "text", "text": "block text"}],
|
||
|
|
tool_call_id="tc1",
|
||
|
|
name="search",
|
||
|
|
)
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == "prefix-continued\nblock text"
|
||
|
|
|
||
|
|
def test_mixed_blocks_with_non_text(self):
|
||
|
|
"""Non-text blocks (e.g. image) should be skipped gracefully."""
|
||
|
|
msg = ToolMessage(
|
||
|
|
content=[
|
||
|
|
{"type": "text", "text": "found results"},
|
||
|
|
{"type": "image_url", "image_url": {"url": "http://img.png"}},
|
||
|
|
],
|
||
|
|
tool_call_id="tc1",
|
||
|
|
name="view_image",
|
||
|
|
)
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == "found results"
|
||
|
|
|
||
|
|
def test_empty_list_content(self):
|
||
|
|
msg = ToolMessage(content=[], tool_call_id="tc1", name="search")
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == ""
|
||
|
|
|
||
|
|
def test_plain_string_in_list(self):
|
||
|
|
"""Bare strings inside a list should be kept."""
|
||
|
|
msg = ToolMessage(
|
||
|
|
content=["plain text block"],
|
||
|
|
tool_call_id="tc1",
|
||
|
|
name="search",
|
||
|
|
)
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
assert result["content"] == "plain text block"
|
||
|
|
|
||
|
|
def test_unknown_content_type_falls_back(self):
|
||
|
|
"""Unexpected types should not crash — return str()."""
|
||
|
|
msg = ToolMessage(content=42, tool_call_id="tc1", name="calc")
|
||
|
|
result = DeerFlowClient._serialize_message(msg)
|
||
|
|
# int → not str, not list → falls to str()
|
||
|
|
assert result["content"] == "42"
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# _extract_text (already existed, but verify it also covers ToolMessage paths)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
|
||
|
|
class TestExtractText:
|
||
|
|
"""DeerFlowClient._extract_text should handle all content shapes."""
|
||
|
|
|
||
|
|
def test_string_passthrough(self):
|
||
|
|
assert DeerFlowClient._extract_text("hello") == "hello"
|
||
|
|
|
||
|
|
def test_list_text_blocks(self):
|
||
|
|
assert DeerFlowClient._extract_text(
|
||
|
|
[{"type": "text", "text": "hi"}]
|
||
|
|
) == "hi"
|
||
|
|
|
||
|
|
def test_empty_list(self):
|
||
|
|
assert DeerFlowClient._extract_text([]) == ""
|
||
|
|
|
||
|
|
def test_fallback_non_iterable(self):
|
||
|
|
assert DeerFlowClient._extract_text(123) == "123"
|