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( {