fix(orchestrator): use descriptive suffixes for duplicate tool names

Instead of opaque _1, _2 suffixes, derive meaningful labels from
hardcoded defaults (e.g. search_topic_sports) so the LLM can
distinguish tool variants by name. Falls back to numeric suffixes
when defaults don't provide a useful label.
This commit is contained in:
Zamil Majdy
2026-03-27 13:13:54 +07:00
parent 7e62fdae48
commit 3f5c2b93cd

View File

@@ -259,12 +259,36 @@ def get_pending_tool_calls(conversation_history: list[Any] | None) -> dict[str,
return {call_id: count for call_id, count in pending_calls.items() if count > 0}
def _derive_suffix_from_defaults(defaults: dict[str, Any]) -> str:
"""Derive a short, descriptive suffix from hardcoded defaults.
Returns a cleaned string like ``_topic_sports`` derived from the first
default's key and value, giving the LLM a meaningful hint about how
this tool variant differs from its siblings. Falls back to ``""`` if
no usable label can be derived.
"""
if not defaults:
return ""
key, val = next(iter(defaults.items()))
# Use the value as a label when it's a short string; otherwise use the key.
if isinstance(val, str) and 1 <= len(val) <= 30:
label = f"{key}_{val}"
else:
label = key
# Sanitise to [a-zA-Z0-9_] and trim
label = re.sub(r"[^a-zA-Z0-9]+", "_", label).strip("_").lower()
return f"_{label}" if label else ""
def _disambiguate_tool_names(tools: list[dict[str, Any]]) -> None:
"""Ensure all tool names are unique (Anthropic API requires this).
When multiple nodes use the same block type, they get the same tool name.
This appends _1, _2, etc. and enriches descriptions with hardcoded defaults
so the LLM can distinguish them. Mutates the list in place.
This derives a descriptive suffix from hardcoded defaults (e.g.
``search_topic_sports``) so the LLM can distinguish tool variants by name.
Falls back to numeric suffixes (``_1``, ``_2``) when defaults don't
provide a useful label. Also enriches descriptions with the full
pre-configured values. Mutates the list in place.
Malformed tools (missing ``function`` or ``function.name``) are silently
skipped so the caller never crashes on unexpected input.
@@ -301,13 +325,27 @@ def _disambiguate_tool_names(tools: list[dict[str, Any]]) -> None:
continue
counters[name] = counters.get(name, 0) + 1
# Skip suffixes that collide with existing (e.g. user-named) tools
while True:
suffix = f"_{counters[name]}"
candidate = f"{name[: 64 - len(suffix)]}{suffix}"
if candidate not in taken:
break
counters[name] += 1
# Try a descriptive suffix first; fall back to numeric.
desc_suffix = _derive_suffix_from_defaults(defaults)
if desc_suffix:
candidate = f"{name[: 64 - len(desc_suffix)]}{desc_suffix}"
if candidate in taken:
# Descriptive suffix collided — append counter to de-dup
num_suffix = f"{desc_suffix}_{counters[name]}"
candidate = f"{name[: 64 - len(num_suffix)]}{num_suffix}"
else:
candidate = None
if not candidate or candidate in taken:
# Pure numeric fallback
while True:
suffix = f"_{counters[name]}"
candidate = f"{name[: 64 - len(suffix)]}{suffix}"
if candidate not in taken:
break
counters[name] += 1
func["name"] = candidate
taken.add(candidate)