diff --git a/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py index 628a60e88..b7b0950e6 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/dynamic_context_middleware.py @@ -53,6 +53,11 @@ def _extract_date(content: str) -> str | None: return m.group(1) if m else None +def is_dynamic_context_reminder(message: object) -> bool: + """Return whether *message* is a hidden dynamic-context reminder.""" + return isinstance(message, HumanMessage) and bool(message.additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY)) + + def _last_injected_date(messages: list) -> str | None: """Scan messages in reverse and return the most recently injected date. @@ -61,7 +66,7 @@ def _last_injected_date(messages: list) -> str | None: are not mistakenly treated as injected reminders. """ for msg in reversed(messages): - if isinstance(msg, HumanMessage) and msg.additional_kwargs.get(_DYNAMIC_CONTEXT_REMINDER_KEY): + if is_dynamic_context_reminder(msg): content_str = msg.content if isinstance(msg.content, str) else str(msg.content) return _extract_date(content_str) return None diff --git a/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py index 01080be14..b259ce4a4 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/title_middleware.py @@ -9,6 +9,7 @@ from langchain.agents.middleware import AgentMiddleware from langgraph.config import get_config from langgraph.runtime import Runtime +from deerflow.agents.middlewares.dynamic_context_middleware import is_dynamic_context_reminder from deerflow.config.title_config import get_title_config from deerflow.models import create_chat_model @@ -61,6 +62,10 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]): return "" + @staticmethod + def _is_user_message_for_title(message: object) -> bool: + return getattr(message, "type", None) == "human" and not is_dynamic_context_reminder(message) + def _should_generate_title(self, state: TitleMiddlewareState) -> bool: """Check if we should generate a title for this thread.""" config = self._get_title_config() @@ -77,7 +82,7 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]): return False # Count user and assistant messages - user_messages = [m for m in messages if m.type == "human"] + user_messages = [m for m in messages if self._is_user_message_for_title(m)] assistant_messages = [m for m in messages if m.type == "ai"] # Generate title after first complete exchange @@ -91,7 +96,7 @@ class TitleMiddleware(AgentMiddleware[TitleMiddlewareState]): config = self._get_title_config() messages = state.get("messages", []) - user_msg_content = next((m.content for m in messages if m.type == "human"), "") + user_msg_content = next((m.content for m in messages if self._is_user_message_for_title(m)), "") assistant_msg_content = next((m.content for m in messages if m.type == "ai"), "") user_msg = self._normalize_content(user_msg_content) diff --git a/backend/tests/test_title_middleware_core_logic.py b/backend/tests/test_title_middleware_core_logic.py index ede4dc0a4..5395f816e 100644 --- a/backend/tests/test_title_middleware_core_logic.py +++ b/backend/tests/test_title_middleware_core_logic.py @@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, MagicMock from langchain_core.messages import AIMessage, HumanMessage from deerflow.agents.middlewares import title_middleware as title_middleware_module +from deerflow.agents.middlewares.dynamic_context_middleware import _DYNAMIC_CONTEXT_REMINDER_KEY from deerflow.agents.middlewares.title_middleware import TitleMiddleware from deerflow.config.title_config import TitleConfig, get_title_config, set_title_config @@ -44,6 +45,22 @@ class TestTitleMiddlewareCoreLogic: assert middleware._should_generate_title(state) is True + def test_should_generate_title_with_dynamic_context_reminder(self): + _set_test_title_config(enabled=True) + middleware = TitleMiddleware() + state = { + "messages": [ + HumanMessage( + content="\nUser prefers Python.\n", + additional_kwargs={_DYNAMIC_CONTEXT_REMINDER_KEY: True}, + ), + HumanMessage(content="帮我总结这段代码"), + AIMessage(content="好的,我先看结构"), + ] + } + + assert middleware._should_generate_title(state) is True + def test_should_not_generate_title_when_disabled_or_already_set(self): middleware = TitleMiddleware() @@ -243,6 +260,25 @@ class TestTitleMiddlewareCoreLogic: prompt, _ = middleware._build_title_prompt(state) assert "" not in prompt + def test_build_title_prompt_uses_real_user_message_with_dynamic_context_reminder(self): + _set_test_title_config(enabled=True) + middleware = TitleMiddleware() + state = { + "messages": [ + HumanMessage( + content="\nUser prefers Python.\n", + additional_kwargs={_DYNAMIC_CONTEXT_REMINDER_KEY: True}, + ), + HumanMessage(content="请帮我写测试"), + AIMessage(content="好的"), + ] + } + + prompt, user_msg = middleware._build_title_prompt(state) + assert user_msg == "请帮我写测试" + assert "" not in prompt + assert "User prefers Python" not in prompt + def test_generate_title_async_strips_think_tags_in_response(self, monkeypatch): """Async title generation strips blocks from the model response.""" _set_test_title_config(max_chars=50)