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
This commit is contained in:
哈基米 2026-09-14 18:07:21 +08:00 committed by GitHub
parent 0a0d768107
commit ba998a92ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 2 deletions

View File

@ -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

View File

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