From 3234563db8363a7325877255a15bc1cb8a607e35 Mon Sep 17 00:00:00 2001 From: jiujiu <2017955725@qq.com> Date: Sun, 11 Oct 2026 08:25:53 +0800 Subject: [PATCH] fix(utils): skip malformed response block types (#6614) Co-authored-by: songjianglin <270247441+2017955725@users.noreply.github.com> --- README.md | 2 +- .../packages/harness/deerflow/utils/AGENTS.md | 6 +++++ .../harness/deerflow/utils/llm_text.py | 2 +- backend/tests/test_input_polish_router.py | 25 +++++++++++++++++++ backend/tests/test_utils_llm_text.py | 11 ++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e09209731a..a5405b6621 100644 --- a/README.md +++ b/README.md @@ -1820,7 +1820,7 @@ Rebuild with `make up` after changing the managed extension set. See `config.example.yaml` and the [reference extension](examples/deerflow-extension-example/) for a complete example. -Gateway-generated follow-up suggestions now normalize both plain-string model output and block/list-style rich content before parsing the JSON array response, so provider-specific content wrappers do not silently drop suggestions. +Gateway-generated follow-up suggestions now normalize both plain-string model output and block/list-style rich content before parsing the JSON array response, so provider-specific content wrappers do not silently drop suggestions. Follow-up suggestions and input polishing skip content blocks with non-string types, preserving the surrounding text instead of failing the request Backend response cleanup preserves unrelated tag names such as `` and `` in follow-up suggestions and polished drafts; only the exact `` name (optionally followed by attributes) starts a reasoning block. Self-closing `` and `` tags are empty reasoning blocks and leave the following answer intact. Delimiters inside quoted attributes, such as ``, also leave the following answer intact. diff --git a/backend/packages/harness/deerflow/utils/AGENTS.md b/backend/packages/harness/deerflow/utils/AGENTS.md index 250f24133c..1a96df8c34 100644 --- a/backend/packages/harness/deerflow/utils/AGENTS.md +++ b/backend/packages/harness/deerflow/utils/AGENTS.md @@ -30,6 +30,12 @@ which takes a whole message and uses different list/mapping semantics. Regression coverage lives in `tests/test_utils_messages.py`, `tests/test_subagent_executor.py`, and `tests/test_task_continuity.py`. +`llm_text.py::extract_response_text` ignores content blocks with non-string +`type` values, including lists and mappings, while retaining adjacent text and +output-text blocks. Tests cover extraction and the one-shot input-polish path +with real LangChain messages in `tests/test_utils_llm_text.py` and +`tests/test_input_polish_router.py`. + `llm_text.py::strip_think_blocks` scans opening and closing tags forward so repeated unclosed `` prefixes in model output do not rescan the same suffix. Keep its case-insensitive tag handling, optional whitespace before diff --git a/backend/packages/harness/deerflow/utils/llm_text.py b/backend/packages/harness/deerflow/utils/llm_text.py index 5e5dd5cc1e..c267ffe6cd 100644 --- a/backend/packages/harness/deerflow/utils/llm_text.py +++ b/backend/packages/harness/deerflow/utils/llm_text.py @@ -145,7 +145,7 @@ def extract_response_text(content: object) -> str: for block in content: if isinstance(block, str): parts.append(block) - elif isinstance(block, dict) and block.get("type") in {"text", "output_text"}: + elif isinstance(block, dict) and block.get("type") in ("text", "output_text"): text = block.get("text") if isinstance(text, str): parts.append(text) diff --git a/backend/tests/test_input_polish_router.py b/backend/tests/test_input_polish_router.py index fe22a070b8..b9223b8d10 100644 --- a/backend/tests/test_input_polish_router.py +++ b/backend/tests/test_input_polish_router.py @@ -4,6 +4,8 @@ from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import HTTPException +from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel +from langchain_core.messages import AIMessage from app.gateway.routers import input_polish from deerflow.utils import oneshot_llm @@ -193,3 +195,26 @@ def test_polish_input_validates_and_sends_normalized_text(monkeypatch): human_content = messages[-1].content assert "summarize report" in human_content assert " summarize report " not in human_content + + +@pytest.mark.parametrize("block_type", [[], {}]) +def test_polish_input_ignores_invalid_block_types(monkeypatch, block_type: object) -> None: + response = AIMessage( + content=[ + {"type": block_type, "text": "ignored"}, + {"type": "text", "text": "Please summarize the report."}, + ], + ) + model = FakeMessagesListChatModel(responses=[response]) + monkeypatch.setattr(oneshot_llm, "create_chat_model", lambda **kwargs: model) + + result = asyncio.run( + input_polish.polish_input.__wrapped__( + input_polish.InputPolishRequest(text="summarize report"), + request=None, + config=_config(), + ), + ) + + assert result.rewritten_text == "Please summarize the report." + assert result.changed is True diff --git a/backend/tests/test_utils_llm_text.py b/backend/tests/test_utils_llm_text.py index 543b28be3f..e44846ea81 100644 --- a/backend/tests/test_utils_llm_text.py +++ b/backend/tests/test_utils_llm_text.py @@ -190,6 +190,17 @@ def test_extract_response_text_skips_blocks_with_non_string_text() -> None: assert extract_response_text(content) == "ok" +@pytest.mark.parametrize("block_type", [[], {}, ["text"], {"type": "text"}, None, False, 0, "image"]) +def test_extract_response_text_skips_invalid_types_and_preserves_text(block_type: object) -> None: + content = [ + "intro", + {"type": block_type, "text": "ignored"}, + {"type": "text", "text": "body"}, + {"type": "output_text", "text": "ending"}, + ] + assert extract_response_text(content) == "intro\nbody\nending" + + def test_extract_response_text_returns_empty_for_empty_list() -> None: assert extract_response_text([]) == ""