mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
* feat(mcp): auto-promote deferred MCP tools from routing hints
When tool_search.enabled=true defers MCP tool schemas, PR1 routing hints
still require the model to spend a tool_search discovery round trip before
it can call the tool the routing metadata already points at. This adds a
McpRoutingMiddleware that matches the latest user message against PR1
routing keywords and promotes the matching deferred schemas before the
model call, removing that round trip.
Design (soft routing, opt-in, additive):
- Matches only the latest real HumanMessage (shared is_real_user_message
helper, reused by SkillActivationMiddleware so the two cannot drift);
case-insensitive substring match, no tokenizer dependency.
- Ordering: priority desc, then tool name asc; capped by the new global
tool_search.auto_promote_top_k (default 3, clamped 1..5). Does not add or
consume a per-tool auto_promote_top_k (PR1 schema unchanged); a per-tool
value is ignored with a DEBUG note.
- Returns a plain {"promoted": ...} state update (not a Command) and relies
on ThreadState.merge_promoted for union/dedupe, so auto-promote and a
model-triggered tool_search converge on the same catalog hash.
- Installed before DeferredToolFilterMiddleware on every deferred-tool path
(lead agent, subagent, embedded client, webhook via shared builders);
a construction-time assert rejects the reversed order. catalog_hash is
None / no routing index is a complete no-op, so bootstrap and ACP skip it.
- Privacy: never executes tools, never promotes policy-filtered tools, adds
no routing keywords or matched tool names to trace metadata or INFO/WARN
logs.
No behavior change when tool_search.enabled=false.
Tests: index construction, matching semantics, middleware state updates,
same-cycle deferred-filter interaction, lead/subagent/embedded-client
builder wiring + order invariant, config clamping, config.example.yaml
parseability, and privacy assertions.
* refactor(mcp): address auto-promote review nits
- executor: access app_config.tool_search.auto_promote_top_k directly to match
the lead-agent and embedded-client paths (drop the over-defensive getattr that
masked missing config); update the subagent test mock to carry tool_search.
- tool_search / mcp_routing_middleware: cross-reference the duplicated routing
priority/keyword normalization between the builder and the middleware's
defensive _normalize_index so they cannot silently drift.
- MCP_SERVER.md: document that auto-promote keyword matching is a case-insensitive
substring test (not word-boundary), advising distinctive keywords.
53 lines
1.7 KiB
Python
53 lines
1.7 KiB
Python
"""Configuration for deferred tool loading via tool_search."""
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
AUTO_PROMOTE_TOP_K_MIN = 1
|
|
AUTO_PROMOTE_TOP_K_MAX = 5
|
|
|
|
|
|
def clamp_auto_promote_top_k(value: int) -> int:
|
|
"""Clamp the global MCP routing auto-promote breadth to PR2's range."""
|
|
return max(AUTO_PROMOTE_TOP_K_MIN, min(AUTO_PROMOTE_TOP_K_MAX, int(value)))
|
|
|
|
|
|
class ToolSearchConfig(BaseModel):
|
|
"""Configuration for deferred tool loading via tool_search.
|
|
|
|
When enabled, MCP tools are not loaded into the agent's context directly.
|
|
Instead, they are listed by name in the system prompt and discoverable
|
|
via the tool_search tool at runtime.
|
|
"""
|
|
|
|
enabled: bool = Field(
|
|
default=False,
|
|
description="Defer tools and enable tool_search",
|
|
)
|
|
auto_promote_top_k: int = Field(
|
|
default=3,
|
|
description="Maximum number of deferred MCP tool schemas auto-promoted from routing metadata per model call",
|
|
)
|
|
|
|
@field_validator("auto_promote_top_k")
|
|
@classmethod
|
|
def _clamp_auto_promote_top_k(cls, value: int) -> int:
|
|
return clamp_auto_promote_top_k(value)
|
|
|
|
|
|
_tool_search_config: ToolSearchConfig | None = None
|
|
|
|
|
|
def get_tool_search_config() -> ToolSearchConfig:
|
|
"""Get the tool search config, loading from AppConfig if needed."""
|
|
global _tool_search_config
|
|
if _tool_search_config is None:
|
|
_tool_search_config = ToolSearchConfig()
|
|
return _tool_search_config
|
|
|
|
|
|
def load_tool_search_config_from_dict(data: dict) -> ToolSearchConfig:
|
|
"""Load tool search config from a dict (called during AppConfig loading)."""
|
|
global _tool_search_config
|
|
_tool_search_config = ToolSearchConfig.model_validate(data)
|
|
return _tool_search_config
|