fix(utils): skip malformed response block types (#6614)

Co-authored-by: songjianglin <270247441+2017955725@users.noreply.github.com>
This commit is contained in:
jiujiu 2026-10-11 08:25:53 +08:00 committed by GitHub
parent 05297813ae
commit 3234563db8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 44 additions and 2 deletions

View File

@ -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 `<think-tank>` and `<think:note>` in follow-up suggestions and polished drafts; only the exact `<think>` name (optionally followed by attributes) starts a reasoning block. Self-closing `<think/>` and `<think />` tags are empty reasoning blocks and leave the following answer intact. Delimiters inside quoted attributes, such as `<think note=">"/>`, also leave the following answer intact.

View File

@ -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 `<think>` prefixes in model output do not rescan the same
suffix. Keep its case-insensitive tag handling, optional whitespace before

View File

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

View File

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

View File

@ -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([]) == ""