From ba998a92ac4b14808785d8325c1bdab5de90a116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=93=88=E5=9F=BA=E7=B1=B3?= <140241684+BlueX888@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:07:21 +0800 Subject: [PATCH] fix(mcp): treat a Human Input Card reply as the current user request (#5426) McpRoutingMiddleware picked the latest user message with is_real_user_message, which rejects every hide_from_ui message. A Human Input Card reply is hidden but is still the user's current request, so when the routing keyword lived only in the clarification answer the deferred MCP tool was never auto-promoted and the model had to call tool_search by hand. Switch _latest_user_message to is_genuine_user_message, the same predicate summarization_middleware already uses for this reason (#5416). Fixes #5425 --- .../middlewares/mcp_routing_middleware.py | 11 ++++++-- .../tests/test_mcp_routing_auto_promote.py | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/backend/packages/harness/deerflow/agents/middlewares/mcp_routing_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/mcp_routing_middleware.py index 2aa744f4f..92ba8e983 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/mcp_routing_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/mcp_routing_middleware.py @@ -11,9 +11,10 @@ from langchain.agents.middleware import AgentMiddleware from langchain_core.messages import HumanMessage from langgraph.runtime import Runtime +from deerflow.agents.middlewares.message_utils import is_genuine_user_message from deerflow.agents.middlewares.tool_promotion_audit_middleware import record_tool_promotion from deerflow.config.tool_search_config import clamp_auto_promote_top_k -from deerflow.utils.messages import get_original_user_content_text, is_real_user_message +from deerflow.utils.messages import get_original_user_content_text logger = logging.getLogger(__name__) @@ -73,8 +74,14 @@ class McpRoutingMiddleware(AgentMiddleware[AgentState]): @staticmethod def _latest_user_message(messages: list[Any]) -> HumanMessage | None: + """Latest user-authored message, including a hidden Human Input Card reply. + + The card reply is hidden from the UI but is still the user's current + request, so ``is_genuine_user_message`` — not ``is_real_user_message``, + which drops every ``hide_from_ui`` message — decides this. + """ for message in reversed(messages): - if is_real_user_message(message): + if is_genuine_user_message(message): return message return None diff --git a/backend/tests/test_mcp_routing_auto_promote.py b/backend/tests/test_mcp_routing_auto_promote.py index 6ae6a9f8f..83f54b731 100644 --- a/backend/tests/test_mcp_routing_auto_promote.py +++ b/backend/tests/test_mcp_routing_auto_promote.py @@ -99,6 +99,31 @@ def test_matching_uses_latest_real_human_message_only(): assert middleware._matched_names({"messages": [HumanMessage(content="metrics", name="summary"), HumanMessage(content="orders", additional_kwargs={"hide_from_ui": True})]}) == [] +def test_matching_uses_a_hidden_human_input_card_reply_as_the_latest_message(): + """A card reply is hidden from the UI but is still the user's current request.""" + middleware = McpRoutingMiddleware( + {"postgres_query": {"priority": 100, "keywords": ["orders"]}}, + "hash1", + 3, + ) + reply = HumanMessage( + content='For your clarification "Which dataset?", my answer is: orders', + additional_kwargs={ + "hide_from_ui": True, + "human_input_response": { + "version": 1, + "kind": "human_input_response", + "source": "ask_clarification", + "request_id": "clarification:call-abc", + "response_kind": "text", + "value": "orders", + }, + }, + ) + + assert middleware._matched_names({"messages": [HumanMessage(content="no match now"), reply]}) == ["postgres_query"] + + def test_matching_supports_casefold_chinese_priority_tiebreak_and_top_k(): middleware = McpRoutingMiddleware( {