perf(tools): shrink lazy tool catalog overhead
This commit is contained in:
parent
c2c95f5339
commit
48e12a06fa
|
|
@ -2370,7 +2370,7 @@ DEFAULT_CONFIG = {
|
|||
"listing": "auto",
|
||||
# Absolute cap on the embedded listing in tokens (chars/4
|
||||
# estimate), regardless of context size. Range 200..60000.
|
||||
"listing_max_tokens": 20000,
|
||||
"listing_max_tokens": 4000,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -231,6 +231,32 @@ class TestBridgeDispatch:
|
|||
result = dispatch_tool_search({}, current_tool_defs=[])
|
||||
assert "error" in json.loads(result)
|
||||
|
||||
def test_empty_search_keeps_connected_sources_discoverable(self):
|
||||
from tools.registry import registry
|
||||
from tools.tool_search import dispatch_tool_search
|
||||
|
||||
name = "recovery_catalog_create_record"
|
||||
tool_def = _td(name, "Create a record in the connected catalog service.")
|
||||
registry.register(
|
||||
name=name,
|
||||
handler=lambda args, **kwargs: "{}",
|
||||
schema=tool_def,
|
||||
toolset="mcp-recovery-catalog",
|
||||
)
|
||||
|
||||
result = json.loads(dispatch_tool_search(
|
||||
{"query": "unrelated vocabulary"},
|
||||
current_tool_defs=[tool_def],
|
||||
))
|
||||
|
||||
assert result["matches"] == []
|
||||
assert result["total_available"] == 1
|
||||
assert result["available_sources"] == [
|
||||
{"name": "recovery-catalog", "tool_count": 1},
|
||||
]
|
||||
assert "remain available" in result["hint"]
|
||||
assert "before concluding" in result["hint"]
|
||||
|
||||
|
||||
def test_resolve_underlying_call_parses_object_args(self):
|
||||
from tools.tool_search import resolve_underlying_call
|
||||
|
|
@ -403,11 +429,43 @@ class TestCatalogListing:
|
|||
from tools.tool_search import ToolSearchConfig
|
||||
cfg = ToolSearchConfig.from_raw(None)
|
||||
assert cfg.listing == "auto"
|
||||
assert cfg.listing_max_tokens == 20000
|
||||
assert cfg.listing_max_tokens == 4000
|
||||
# legacy bool shapes keep defaults too
|
||||
assert ToolSearchConfig.from_raw(True).listing == "auto"
|
||||
|
||||
|
||||
def test_default_listing_cap_bounds_fixed_catalog_overhead(self):
|
||||
"""The default manifest must not grow back to the old 20K-token cap."""
|
||||
from tools.registry import registry
|
||||
from tools.tool_search import (
|
||||
ToolSearchConfig,
|
||||
assemble_tool_defs,
|
||||
estimate_tokens_from_schemas,
|
||||
)
|
||||
|
||||
defs = []
|
||||
for i in range(500):
|
||||
name = f"lean_catalog_tool_{i:04d}"
|
||||
registry.register(
|
||||
name=name,
|
||||
handler=lambda args, **kwargs: "{}",
|
||||
schema=_td(name, "Perform a deliberately verbose connected service action."),
|
||||
toolset="mcp-lean-catalog",
|
||||
)
|
||||
defs.append(_td(name, "Perform a deliberately verbose connected service action."))
|
||||
|
||||
cfg = ToolSearchConfig.from_raw(None)
|
||||
result = assemble_tool_defs(defs, context_length=1_000_000, config=cfg)
|
||||
search = next(
|
||||
td for td in result.tool_defs
|
||||
if td["function"]["name"] == "tool_search"
|
||||
)
|
||||
description_tokens = estimate_tokens_from_schemas([search])
|
||||
# Includes the bridge schema around the listing, so allow modest
|
||||
# framing overhead above the 4K listing budget.
|
||||
assert description_tokens < 4500
|
||||
assert result.listing_form in {"names", "groups", "mixed"}
|
||||
|
||||
def test_short_desc_first_sentence_and_clip(self):
|
||||
from tools.tool_search import _short_desc
|
||||
assert _short_desc("Open an issue. Second sentence dropped.") == "Open an issue."
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class ToolSearchConfig:
|
|||
listing: str = "auto" # "auto" | "on" | "off"
|
||||
# Absolute cap on the embedded listing, regardless of context size.
|
||||
# Effective budget = min(listing_max_tokens, threshold_pct% of context).
|
||||
listing_max_tokens: int = 20000
|
||||
listing_max_tokens: int = 4000
|
||||
|
||||
@classmethod
|
||||
def from_raw(cls, raw: Any) -> "ToolSearchConfig":
|
||||
|
|
@ -143,7 +143,7 @@ class ToolSearchConfig:
|
|||
listing = listing_raw
|
||||
else:
|
||||
listing = "auto"
|
||||
listing_max_tokens = max(200, min(60000, _safe_int(raw.get("listing_max_tokens"), 20000)))
|
||||
listing_max_tokens = max(200, min(60000, _safe_int(raw.get("listing_max_tokens"), 4000)))
|
||||
|
||||
return cls(
|
||||
enabled=enabled,
|
||||
|
|
@ -512,7 +512,7 @@ def _listing_group_label(source_name: str) -> str:
|
|||
def build_catalog_listing(
|
||||
deferrable: List[Dict[str, Any]],
|
||||
*,
|
||||
max_tokens: int = 20000,
|
||||
max_tokens: int = 4000,
|
||||
) -> Optional[str]:
|
||||
"""Render a skills-style manifest of the deferred catalog.
|
||||
|
||||
|
|
@ -545,7 +545,7 @@ def build_catalog_listing(
|
|||
def build_catalog_listing_with_form(
|
||||
deferrable: List[Dict[str, Any]],
|
||||
*,
|
||||
max_tokens: int = 20000,
|
||||
max_tokens: int = 4000,
|
||||
) -> Tuple[Optional[str], str]:
|
||||
"""Like :func:`build_catalog_listing` but also reports the form used.
|
||||
|
||||
|
|
@ -861,6 +861,25 @@ def _format_search_hit(entry: CatalogEntry) -> Dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _available_source_summary(catalog: List[CatalogEntry]) -> List[Dict[str, Any]]:
|
||||
"""Return a compact, deterministic summary of connected deferred sources.
|
||||
|
||||
Included only when search returns no matches. This gives the model enough
|
||||
evidence to retry with a source/action query instead of treating a lexical
|
||||
miss as proof that the capability is unavailable, without adding anything
|
||||
to the fixed per-turn prompt.
|
||||
"""
|
||||
counts: Dict[str, int] = {}
|
||||
for entry in catalog:
|
||||
source_name = entry.source_name or entry.source or "other"
|
||||
label = _listing_group_label(source_name)
|
||||
counts[label] = counts.get(label, 0) + 1
|
||||
return [
|
||||
{"name": name, "tool_count": counts[name]}
|
||||
for name in sorted(counts)
|
||||
]
|
||||
|
||||
|
||||
def dispatch_tool_search(args: Dict[str, Any],
|
||||
*,
|
||||
current_tool_defs: List[Dict[str, Any]],
|
||||
|
|
@ -881,11 +900,20 @@ def dispatch_tool_search(args: Dict[str, Any],
|
|||
_, deferrable = classify_tools(current_tool_defs)
|
||||
catalog = build_catalog(deferrable)
|
||||
hits = search_catalog(catalog, query, limit=limit)
|
||||
return json.dumps({
|
||||
result: Dict[str, Any] = {
|
||||
"query": query,
|
||||
"total_available": len(catalog),
|
||||
"matches": [_format_search_hit(h) for h in hits],
|
||||
}, ensure_ascii=False)
|
||||
}
|
||||
if not hits and catalog:
|
||||
result["available_sources"] = _available_source_summary(catalog)
|
||||
result["hint"] = (
|
||||
"No lexical match was found, but the sources above are connected "
|
||||
"and their tools remain available. Retry tool_search with the "
|
||||
"service name plus a concrete action or object before concluding "
|
||||
"the capability is unavailable."
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
def dispatch_tool_describe(args: Dict[str, Any],
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ tools:
|
|||
search_default_limit: 5
|
||||
max_search_limit: 20
|
||||
listing: auto # embed a grouped name+description catalog manifest
|
||||
listing_max_tokens: 20000
|
||||
listing_max_tokens: 4000
|
||||
```
|
||||
|
||||
| Key | Default | Meaning |
|
||||
|
|
@ -90,7 +90,7 @@ tools:
|
|||
| `search_default_limit` | `5` | Hits returned when the model calls `tool_search` without a `limit`. |
|
||||
| `max_search_limit` | `20` | Hard upper bound the model can request via `limit`. Range 1–50. |
|
||||
| `listing` | `auto` | Embed a skills-style manifest of every deferred tool (name + first sentence of its description, ≤60 chars, grouped by MCP server) in the `tool_search` bridge description. `auto` includes it when it fits the budget (falling back to names-only, then to the tier-2 server summary); `on`/`off` force either way. |
|
||||
| `listing_max_tokens` | `20000` | Absolute cap on the embedded listing, regardless of context size. Range 200–60000. |
|
||||
| `listing_max_tokens` | `4000` | Absolute cap on the embedded listing, regardless of context size. Range 200–60000. Large catalogs degrade to names-only or per-server summaries, keeping full schemas available through search. |
|
||||
|
||||
### Why the listing exists
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue