fix(agents): sanitize custom tool names before sending to the LLM (CRM-499) - #63
Conversation
…M (CRM-499)
A custom HTTP tool whose name contains a space/accent/punctuation was sent
verbatim as tools[].function.name, which OpenAI/Anthropic reject (must match
^[a-zA-Z0-9_-]+$). The 400 surfaced as an opaque 500 and left the agent unable
to answer, with no hint of which tool was at fault.
Add sanitize_tool_name() and apply it where the ADK function name is set
(tool_builder.py, custom_tools.py). We dispatch these tools by __name__, so the
sanitized name is self-consistent; the readable label stays in the description.
This also repairs existing agents with invalid tool names, no operator action
needed.
MCP tools are intentionally NOT sanitized: their function names come from the
MCP protocol (already valid) and are the key the toolset uses to route
execution back to the server, so renaming would break the call. The seeded MCP
display names ("Brave Search", "Sequential Thinking") are only used for
routing/logging, never as function.name.
Tests: tests/unit/test_tool_name_sanitization.py (16 cases) proves every input
maps to ^[a-zA-Z0-9_-]+$, including the client's "testando ferramenta".
Reviewer's GuideSanitizes custom HTTP tool names before they reach the LLM, keeping ADK dispatch consistent while preventing provider rejections caused by spaces, accents, punctuation, or excessive length; MCP names remain untouched because they are protocol routing keys. Sequence diagram for sanitized custom tool executionsequenceDiagram
participant Agent as Agent
participant Builder as Custom tool builder
participant Sanitizer as sanitize_tool_name
participant LLM as LLM provider
participant Tool as Custom HTTP tool
Agent->>Builder: Create custom HTTP tool
Builder->>Sanitizer: sanitize_tool_name(name)
Sanitizer-->>Builder: Valid function name
Builder->>Tool: Set http_tool.__name__
Builder->>LLM: Send function.name
LLM-->>Agent: Accept tool definition
Agent->>Tool: Dispatch by __name__
Tool-->>Agent: Return HTTP result
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/utils/tool_naming.py" line_range="43-44" />
<code_context>
+ return FALLBACK_TOOL_NAME
+
+ sanitized = _INVALID_RUN.sub("_", name)
+ sanitized = _REPEAT_UNDERSCORE.sub("_", sanitized).strip("_-")
+ sanitized = sanitized[:MAX_TOOL_NAME_LENGTH].strip("_-")
+ return sanitized or FALLBACK_TOOL_NAME
</code_context>
<issue_to_address>
**issue (bug_risk):** Sanitization is not injective: distinct custom tools such as `"a b"` and `"a_b"`, or two names differing after the 64th character, receive the same `__name__`. Both tools are then registered with the same provider function name, so tool declarations collide and dispatch cannot reliably identify which endpoint to execute.
**Triggers:** When an agent contains custom tools whose sanitized names are equal.
**Suggested fix:** Detect sanitized-name collisions while building the tool list and reject or disambiguate them before constructing the provider payload.
</issue_to_address>
### Comment 2
<location path="src/services/adk/custom_tools.py" line_range="243" />
<code_context>
+ # space/accent breaks the whole turn). We dispatch this tool by
+ # __name__, so the sanitized name stays self-consistent; the readable
+ # label lives in __doc__/description.
+ http_tool.__name__ = sanitize_tool_name(name)
return FunctionTool(func=http_tool)
</code_context>
<issue_to_address>
**issue (bug_risk):** The duplicate-suppression check compares a raw `http_tool_config["name"]` with `built_from_ids`, which now contains sanitized `tool.func.__name__` values. When a tool is present both in `custom_tool_ids` and in the expanded `http_tools` config with an invalid name, the check fails and builds the same tool twice under the same sanitized name.
**Triggers:** When an agent has both `custom_tool_ids` and the expanded `http_tools` representation of a tool whose name requires sanitization.
**Suggested fix:** Compare `sanitize_tool_name(http_tool_config.get("name"))` with `built_from_ids`, or perform duplicate suppression using a stable tool ID.
</issue_to_address>
### Comment 3
<location path="src/utils/tool_naming.py" line_range="43" />
<code_context>
+ return FALLBACK_TOOL_NAME
+
+ sanitized = _INVALID_RUN.sub("_", name)
+ sanitized = _REPEAT_UNDERSCORE.sub("_", sanitized).strip("_-")
+ sanitized = sanitized[:MAX_TOOL_NAME_LENGTH].strip("_-")
+ return sanitized or FALLBACK_TOOL_NAME
</code_context>
<issue_to_address>
**issue:** `sanitize_tool_name()` collapses repeated underscores even though `foo__bar` already matches the documented provider pattern and the docstring promises that already-valid names pass through unchanged except for length capping. This silently changes existing tool identifiers and can create additional collisions.
**Triggers:** When an existing custom tool name contains consecutive underscores.
**Suggested fix:** Only collapse underscores introduced by invalid runs, or update the documented contract and explicitly account for the resulting identifier changes.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 3 findings to address first, and sanitization, truncation, or collisions can cause distinct custom tools to expose the same name and dispatch a model tool call incorrectly, potentially invoking the wrong HTTP action. Reverting prevents future misrouting, but any external request already made would need to be checked or repaired separately.
Blocking findings: src/utils/tool_naming.py:44, src/services/adk/custom_tools.py:243, src/utils/tool_naming.py:43
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
…e (CRM-499) Integration guard over CustomToolBuilder and ToolBuilder: a tool named "testando ferramenta" must build with __name__ == "testando_ferramenta", and a valid name is preserved. Reverting the sanitize call at either _create_http_tool site makes these fail (the raw space survives). Runs in the container/CI lane (imports google.adk), alongside the standalone sanitizer unit test.
|
Cobertura de integração adicionada (commit
Negative-provable: reverter o Roda no lane container/CI (importa |
Sanitizing __name__ made it diverge from the raw name the expanded config still
carries, and the dedup compared the two spellings. An agent holding both
custom_tool_ids and the http_tools those ids expand to therefore built the tool
twice under one function name — the same payload the provider rejects, so the
sanitization was undone by the guard it walked past.
Both builders now compare on the sanitized name, and each _create_http_tool is
handed the names already taken so two tools never answer to the same one:
distinct names can collapse alike ("minha ferramenta" and "minha_ferramenta") or
share their first 64 chars. The name is settled before FunctionTool captures it,
since the declaration is generated from the live __name__ while dispatch uses
the captured one — renaming later would split them.
sanitize_tool_name also returns a name the providers already accept untouched:
squeezing "my__tool" into "my_tool" renamed tools that work today for no gain.
A rename is logged with both spellings so the name the LLM sees can be traced
back to the catalog.
Problema (CRM-499)
Custom tool com espaço/acento/pontuação no nome derruba o chat do agente com 500 opaco. A OpenAI/Anthropic exigem
tools[].function.nameem^[a-zA-Z0-9_-]+$:Root cause
O processor mandava o nome cru como
function.name(linhas originais, pré-fix):tool_builder.pyecustom_tools.py, emhttp_tool.__name__ = name. Nome inválido → 400 do provedor → 500 opaco (sem dizer qual tool).Fix
src/utils/tool_naming.py::sanitize_tool_name()— coage p/^[a-zA-Z0-9_-]+, colapsa runs inválidos em_, apara bordas, cap 64 (limite OpenAI), fallback"tool".__name__da custom tool é definido:tool_builder.py:267ecustom_tools.py:243. Dispatch é por__name__, então o nome sanitizado é auto-consistente; o rótulo legível continua nadescription.MCP: verificado — NÃO precisa (e não deve) ser sanitizado
O
namedo MCP server no banco (ex.: "Brave Search") é usado só p/ roteamento/log (mcp_service.py:157), não virafunction.name. OMCPToolset(tool_filter=agent_tools)expõe as tools pelos nomes do protocolo MCP (brave_web_search, válidos). Renomear MCP quebraria o mapeamento name→execução. Então o display name com espaço não causa o 500 — corrige a suspeita do report original.Testes
tests/unit/test_tool_name_sanitization.py— todo input vira^[a-zA-Z0-9_-]+, incl."testando ferramenta" → "testando_ferramenta", acentos, cap 64, fallback.tests/unit/test_custom_tools.py::TestHttpToolNameSanitizationnos dois builders (CustomToolBuilder,ToolBuilder) — falha se a sanitização for revertida em qualquer um dos dois_create_http_tool.py_compilelimpo. A suite completa (importagoogle.adk) roda no container/CI, não localmente.Follow-ups (fora deste PR)
Card: CRM-499
Summary by Sourcery
Prevent custom tool names that violate LLM provider constraints from causing agent requests to fail.
Bug Fixes:
Enhancements:
Tests: