mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-25 23:48:00 +00:00
fix(agent): reserve ellipsis room so the local title respects max_chars (#4052)
`_fallback_title` sliced the user message to `min(max_chars, 50)` and then
appended a three-character ellipsis, so the returned title could be three
characters longer than the configured cap. `_parse_title`, six lines above,
slices the model's answer to `max_chars` exactly -- both read the same
`TitleConfig.max_chars`, only one honoured it.
This is the default path, not an error branch: `config.example.yaml` ships
`title.model_name: null` ("null = fast local fallback"), so every title is
produced here unless the operator opts into a title model. `max_chars` is a
documented key with a pydantic range of 10..200; any value in 10..52 makes a
long first message overshoot its cap.
Reserve room for the ellipsis before slicing. At the shipped `max_chars: 60`
the body is still 50 characters, so default output is unchanged.
The existing `test_sync_generate_title_respects_fallback_truncation` asserted
the shape of the truncation but never its length -- at its own `max_chars=50`
it was passing on a 53-character title. It now asserts the bound it is named
after.
This commit is contained in:
parent
8ade23d779
commit
ca18cf0b24
@ -162,7 +162,11 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]):
|
||||
config = self._get_title_config()
|
||||
fallback_chars = min(config.max_chars, 50)
|
||||
if len(user_msg) > fallback_chars:
|
||||
return user_msg[:fallback_chars].rstrip() + "..."
|
||||
# Reserve room for the ellipsis so this path honours ``max_chars``
|
||||
# exactly as ``_parse_title`` does on the model path.
|
||||
ellipsis = "..."
|
||||
body = min(fallback_chars, config.max_chars - len(ellipsis))
|
||||
return user_msg[:body].rstrip() + ellipsis
|
||||
return user_msg if user_msg else "New Conversation"
|
||||
|
||||
def _get_runnable_config(self) -> dict[str, Any]:
|
||||
|
||||
@ -4,6 +4,7 @@ import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
from langgraph.constants import TAG_NOSTREAM
|
||||
|
||||
@ -319,6 +320,51 @@ class TestTitleMiddlewareCoreLogic:
|
||||
result = middleware._generate_title_result(state)
|
||||
assert result["title"].endswith("...")
|
||||
assert result["title"].startswith("这是一个非常长的问题描述")
|
||||
assert len(result["title"]) <= 50
|
||||
|
||||
@pytest.mark.parametrize("max_chars", [10, 20, 40, 49, 50, 52, 53, 60, 200])
|
||||
def test_fallback_title_never_exceeds_max_chars(self, max_chars):
|
||||
"""``max_chars`` bounds the fallback title, not just its body.
|
||||
|
||||
The ellipsis is part of the returned title, so a body of exactly
|
||||
``min(max_chars, 50)`` characters overshot the configured cap by three.
|
||||
``model_name: null`` is the shipped default, so this is the path every
|
||||
title takes out of the box -- not an error branch.
|
||||
"""
|
||||
_set_test_title_config(max_chars=max_chars, model_name=None)
|
||||
middleware = TitleMiddleware()
|
||||
|
||||
title = middleware._fallback_title("x" * 200)
|
||||
|
||||
assert len(title) <= max_chars
|
||||
assert title.endswith("...")
|
||||
|
||||
@pytest.mark.parametrize("max_chars", [10, 20, 50])
|
||||
def test_fallback_title_honours_the_same_cap_as_the_model_path(self, max_chars):
|
||||
"""Both title paths read ``config.max_chars``; both must respect it.
|
||||
|
||||
``_parse_title`` slices the model's answer to ``max_chars`` exactly. The
|
||||
local path is the other half of the same contract.
|
||||
"""
|
||||
_set_test_title_config(max_chars=max_chars, model_name=None)
|
||||
middleware = TitleMiddleware()
|
||||
long_text = "x" * 200
|
||||
|
||||
assert len(middleware._parse_title(long_text)) <= max_chars
|
||||
assert len(middleware._fallback_title(long_text)) <= max_chars
|
||||
|
||||
def test_fallback_title_keeps_default_config_output_unchanged(self):
|
||||
"""The default ``max_chars=60`` leaves room for the ellipsis already.
|
||||
|
||||
Reserving that room must not shorten titles that were never over the
|
||||
cap, so the shipped configuration keeps emitting a 50-character body.
|
||||
"""
|
||||
_set_test_title_config(max_chars=60, model_name=None)
|
||||
middleware = TitleMiddleware()
|
||||
|
||||
title = middleware._fallback_title("x" * 200)
|
||||
|
||||
assert title == "x" * 50 + "..."
|
||||
|
||||
def test_parse_title_strips_think_tags(self):
|
||||
"""Title model responses with <think>...</think> blocks are stripped before use."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user