mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(utils): skip malformed response block types (#6614)
Co-authored-by: songjianglin <270247441+2017955725@users.noreply.github.com>
This commit is contained in:
parent
05297813ae
commit
3234563db8
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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([]) == ""
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user