mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(backend): prevent suffix collision with user-named tools
The dedup logic now checks the full set of existing tool names before assigning a suffix. If a user-defined tool already uses e.g. 'my_tool_1', the algorithm skips to '_2'. Fixes the edge case flagged by Sentry.
This commit is contained in:
@@ -676,17 +676,27 @@ class OrchestratorBlock(Block):
|
||||
name = tool_func["function"]["name"]
|
||||
name_counts[name] = name_counts.get(name, 0) + 1
|
||||
|
||||
name_seen: dict[str, int] = {}
|
||||
# Collect all final names to avoid collisions with user-defined names
|
||||
all_names: set[str] = {
|
||||
tool_func["function"]["name"] for tool_func in return_tool_functions
|
||||
}
|
||||
|
||||
for tool_func in return_tool_functions:
|
||||
func = tool_func["function"]
|
||||
name = func["name"]
|
||||
if name_counts[name] > 1:
|
||||
idx = name_seen.get(name, 0) + 1
|
||||
name_seen[name] = idx
|
||||
suffix = f"_{idx}"
|
||||
# Anthropic tool names have a 64-char limit
|
||||
max_base = 64 - len(suffix)
|
||||
func["name"] = f"{name[:max_base]}{suffix}"
|
||||
# Find the next available suffix that doesn't collide
|
||||
idx = 1
|
||||
while True:
|
||||
suffix = f"_{idx}"
|
||||
# Anthropic tool names have a 64-char limit
|
||||
max_base = 64 - len(suffix)
|
||||
candidate = f"{name[:max_base]}{suffix}"
|
||||
if candidate not in all_names:
|
||||
break
|
||||
idx += 1
|
||||
func["name"] = candidate
|
||||
all_names.add(candidate)
|
||||
|
||||
# Enrich description with hardcoded defaults so the LLM can
|
||||
# distinguish between tools that share the same block type.
|
||||
|
||||
@@ -363,3 +363,41 @@ async def test_long_tool_name_truncated():
|
||||
for tool in tools:
|
||||
name = tool["function"]["name"]
|
||||
assert len(name) <= 64, f"Tool name exceeds 64 chars: {name!r} ({len(name)})"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_suffix_collision_with_user_named_tool():
|
||||
"""If a user-named tool is 'my_tool_1', dedup of 'my_tool' should skip to _2."""
|
||||
block = MatchTextPatternBlock()
|
||||
# Two nodes with same block name (will collide)
|
||||
node_a = _make_mock_node(block, "node_a", input_default={"match": "foo"})
|
||||
node_b = _make_mock_node(block, "node_b", input_default={"match": "bar"})
|
||||
|
||||
# A third node that a user has customized to match the _1 suffix pattern
|
||||
base = OrchestratorBlock.cleanup(block.name)
|
||||
node_c = _make_mock_node(block, "node_c", metadata={"customized_name": f"{base}_1"})
|
||||
|
||||
link_a = _make_mock_link("tools_^_a_~_text", "text", "node_a", "orch")
|
||||
link_b = _make_mock_link("tools_^_b_~_text", "text", "node_b", "orch")
|
||||
link_c = _make_mock_link("tools_^_c_~_text", "text", "node_c", "orch")
|
||||
|
||||
mock_db = AsyncMock()
|
||||
mock_db.get_connected_output_nodes.return_value = [
|
||||
(link_a, node_a),
|
||||
(link_b, node_b),
|
||||
(link_c, node_c),
|
||||
]
|
||||
|
||||
with patch(
|
||||
"backend.blocks.orchestrator.get_database_manager_async_client",
|
||||
return_value=mock_db,
|
||||
):
|
||||
tools = await OrchestratorBlock._create_tool_node_signatures("orch")
|
||||
|
||||
names = [t["function"]["name"] for t in tools]
|
||||
assert len(set(names)) == len(names), f"Tool names are not unique: {names}"
|
||||
# The user-named tool keeps its name
|
||||
assert f"{base}_1" in names
|
||||
# The duplicates should skip _1 (taken) and use _2, _3
|
||||
assert f"{base}_2" in names
|
||||
assert f"{base}_3" in names
|
||||
|
||||
Reference in New Issue
Block a user