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} 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: def _disambiguate_tool_names(tools: list[dict[str, Any]]) -> None:
"""Ensure all tool names are unique (Anthropic API requires this). """Ensure all tool names are unique (Anthropic API requires this).
When multiple nodes use the same block type, they get the same tool name. 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 This derives a descriptive suffix from hardcoded defaults (e.g.
so the LLM can distinguish them. Mutates the list in place. ``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 Malformed tools (missing ``function`` or ``function.name``) are silently
skipped so the caller never crashes on unexpected input. skipped so the caller never crashes on unexpected input.
@@ -301,13 +325,27 @@ def _disambiguate_tool_names(tools: list[dict[str, Any]]) -> None:
continue continue
counters[name] = counters.get(name, 0) + 1 counters[name] = counters.get(name, 0) + 1
# Skip suffixes that collide with existing (e.g. user-named) tools
while True: # Try a descriptive suffix first; fall back to numeric.
suffix = f"_{counters[name]}" desc_suffix = _derive_suffix_from_defaults(defaults)
candidate = f"{name[: 64 - len(suffix)]}{suffix}" if desc_suffix:
if candidate not in taken: candidate = f"{name[: 64 - len(desc_suffix)]}{desc_suffix}"
break if candidate in taken:
counters[name] += 1 # 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 func["name"] = candidate
taken.add(candidate) taken.add(candidate)