deer-flow/backend/tests/test_model_response.py
yetuge 97ed0e9797
fix(agents): skip content blocks with unhashable type values (#6664)
* fix(agents): skip content blocks with unhashable type values

has_visible_content and ParentContextSnapshot.from_state classified a
content block's type with set membership (`in {"text", "output_text"}`
plus the _MEDIA_BLOCK_TYPES frozenset). `type: []` / `type: {}` are
legal JSON from a provider, and hashing an unhashable value raises
TypeError instead of skipping the block:

- has_visible_content runs on every model response through the
  llm-error-handling (_raise_for_empty_response), terminal-response and
  model-length middlewares, so one malformed block turned a completed
  response into a failed run instead of a skipped block;
- from_state builds the history every background subagent dispatch is
  seeded from, so the same block failed the dispatch.

Probe the type by equality with tuples so a non-string type simply fails
the probe, mirroring #6614's fix for extract_response_text and the
isinstance guard clarification already uses for form field types. The
media constant becomes a tuple too, so the second probe in the same loop
cannot move the crash one line down.

Tests pin the RED-to-GREEN cycle at both call sites: unit-level
has_visible_content, middleware-level empty-response classification,
and the dispatch snapshot across human/ai/tool messages.

Fixes #6663

Signed-off-by: yetuge <2219677952@qq.com>

* fix(agents): warn when a snapshot skips a non-string-typed block

Review follow-up on #6664: with the malformed block now skipped
instead of raising, a provider that systematically emits non-string
types would degrade the child's history with nothing in the logs to
explain it. The elif chain cannot log on fall-through —
reasoning/signature and tool-use exclusions are deliberate and must
stay quiet — so the signal keys on the type not being a str, matching
the guard the review suggested.

has_visible_content keeps its silence on purpose: the empty-response
retry already surfaces the symptom there.

Fixes #6663

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-10-11 17:45:06 +08:00

39 lines
1.9 KiB
Python

"""Shared model-response classification helpers."""
from langchain_core.messages import AIMessage
from deerflow.agents.middlewares.model_response import has_visible_content
def test_has_visible_content_reads_plain_string_content() -> None:
assert has_visible_content(AIMessage(content="visible answer")) is True
assert has_visible_content(AIMessage(content=" \n ")) is False
assert has_visible_content(AIMessage(content=[])) is False
def test_has_visible_content_reads_text_and_output_text_blocks() -> None:
message = AIMessage(content=[{"type": "text", "text": "visible answer"}])
assert has_visible_content(message) is True
message = AIMessage(content=[{"type": "output_text", "text": "visible answer"}])
assert has_visible_content(message) is True
def test_has_visible_content_ignores_whitespace_only_and_textless_blocks() -> None:
assert has_visible_content(AIMessage(content=[{"type": "text", "text": " "}])) is False
assert has_visible_content(AIMessage(content=[{"type": "text"}])) is False
assert has_visible_content(AIMessage(content=[{"type": "text", "text": 42}])) is False
assert has_visible_content(AIMessage(content=[{"type": "reasoning", "reasoning": "thinking"}])) is False
def test_has_visible_content_keeps_visible_text_around_an_unhashable_block_type() -> None:
# `type: []` / `type: {}` are legal JSON from a provider; probing the type
# by equality skips the block instead of raising TypeError out of the
# middleware chain (llm error handling, terminal response, model length).
message = AIMessage(content=[{"type": [], "text": "ignored"}, {"type": "text", "text": "visible answer"}])
assert has_visible_content(message) is True
def test_has_visible_content_skips_unhashable_block_types_without_other_text() -> None:
message = AIMessage(content=[{"type": {}, "text": "ignored"}])
assert has_visible_content(message) is False