CAMEL-24322: Add tool-calling support via AiToolRegistry - #25289
Conversation
Extend the camel-openai component to discover and execute Camel route tools registered via the shared AiToolRegistry, alongside existing MCP tools. This implements Step 6 of the unified AI tool abstraction design (CAMEL-23382). Changes: - Add camel-ai-tool compile dependency to camel-openai - Add 'tags' configuration parameter to OpenAIConfiguration for filtering tools by tag from the shared AiToolRegistry - Create AiToolSpecToOpenAI converter that transforms AiToolSpec into OpenAI ChatCompletionFunctionTool using parametersJsonSchema - Extend OpenAIProducer to discover Camel route tools and dispatch them via AiToolExecutor in the agentic loop, with exchange isolation - Extend OpenAIToolExecutionProducer similarly for manual tool loops - Add AiToolSpecToOpenAITest with 8 test cases covering full spec, no params, no description, default type, required arrays, empty schema, invalid schema, and additionalProperties:false - Update error messages to reference generic "tool source" instead of MCP-specific wording Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gnodet
left a comment
There was a problem hiding this comment.
Well-structured integration of AiToolRegistry into the OpenAI component with good converter test coverage and correct backward compatibility. Five observations — two about code quality, three about documentation/security polish.
Additional observations (on lines not in the diff):
-
Stale descriptions in
OpenAIConfiguration.java:toolExecutionErrorStrategy(line 219) says "Strategy for handling exceptions thrown during MCP tool execution" but now also governs Camel route tool errors. Similarly,hallucinatedToolNameStrategy(line 230) says "tool not found in any MCP server" — the runtime error message was correctly updated to "tool source" but the annotations weren't. Both descriptions and the catalog JSON should be updated. -
Stale class Javadoc in
OpenAIToolExecutionProducer.java(line 48): Still says "Producer that executes MCP tool calls" — should be updated to reflect that this now handles both MCP and Camel route tools.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of @gnodet
|
|
||
| int maxIterations = config.getMaxToolIterations(); | ||
|
|
||
| Set<String> availableToolNames = new java.util.LinkedHashSet<>(); |
There was a problem hiding this comment.
FQCN violation: new java.util.LinkedHashSet<>() should use the simple class name with an import. The project convention states: "Do NOT use fully qualified class names in Java code." Note that OpenAIToolExecutionProducer in this same PR correctly imports java.util.LinkedHashSet.
| Set<String> availableToolNames = new java.util.LinkedHashSet<>(); | |
| Set<String> availableToolNames = new LinkedHashSet<>(); |
(Also add import java.util.LinkedHashSet; alongside the existing import java.util.LinkedHashMap;)
| @@ -633,6 +671,89 @@ private void processNonStreamingAgentic( | |||
| "Max tool iterations (%d) exceeded. Tools called: %s".formatted(maxIterations, toolCallsLog)); | |||
There was a problem hiding this comment.
Code duplication: discoverCamelRouteTools(), executeCamelRouteTool(), and the result-handling logic are duplicated nearly verbatim between OpenAIProducer and OpenAIToolExecutionProducer (~80 lines each). The duplication is slightly inconsistent — this class extracts result handling into handleCamelToolResult(), while OpenAIToolExecutionProducer inlines the same logic.
Consider extracting the shared code into a package-private helper class (similar to how AiToolSpecToOpenAI is already a shared utility).
| throw e; | ||
| } | ||
| LOG.warn("Camel route tool '{}' execution failed: {}", spec.getName(), e.getMessage(), e); | ||
| return "Error: Tool execution failed: " + e.getMessage(); |
There was a problem hiding this comment.
Security nit: The AiToolResult Javadoc warns: "Framework adapters MUST NOT return [ExecutionError.message()] verbatim to the LLM without sanitization." The handleCamelToolResult() method correctly returns the generic "Error: Tool execution failed" for ExecutionError, but this outer catch returns e.getMessage() which could include internal details. Consider using the same sanitized message:
| return "Error: Tool execution failed: " + e.getMessage(); | |
| return "Error: Tool execution failed"; |
The same pattern applies to OpenAIToolExecutionProducer at line 291.
Croway
left a comment
There was a problem hiding this comment.
Overall the shape is right and consistent with the precedents: AiToolSpecToOpenAI mirrors McpToolConverter, and discoverCamelRouteTools / exchange isolation via ExchangeHelper.createCopy match LangChain4jAgentProducer (CAMEL-23944). Sanitizing AiToolResult.ExecutionError before returning it to the LLM correctly follows the security note in AiToolResult. Tests use AssertJ and are package-private — conventions respected.
A few things below, the first one being the main blocker.
Blocking
1. The branch is stale — main refactored exactly the code this PR edits
CAMEL-23078 (029a51c) extracted all tool dispatch into McpToolCallExecutor (parallel execution, timeout, MDC propagation, returnDirect), and both producers now delegate to it:
// OpenAIProducer:550 / OpenAIToolExecutionProducer:142 on main
List<McpToolCallExecutor.ToolResult> results = toolCallExecutor.execute(toolCalls);This PR instead re-adds inline dispatch in both producers, against the pre-refactor code — hence the conflict. After rebasing, please move the Camel-route branch into McpToolCallExecutor.executeOne(...) (renaming the class to something source-agnostic, e.g. ToolCallExecutor). That single change:
- removes the ~120 duplicated lines (
executeCamelRouteTool+ result handling +discoverCamelRouteToolscurrently exist twice in this PR); - gives route tools
parallelToolExecution/parallelToolTimeoutfor free; - keeps hallucinated-name and error-strategy handling in one place — the stated reason that class exists ("cannot drift between the two");
- makes
returnDirect=falsefor route tools fall out oferrorResult/ToolResultnaturally, instead of the manualallReturnDirect = false.
One thing to watch while doing this: with parallel execution ExchangeHelper.createCopy(exchange, ...) would run on pool threads against a shared parent exchange. Safer to create the copies on the caller thread and pass them in.
2. FQCN in OpenAIProducer — will fail the build check
Set<String> availableToolNames = new java.util.LinkedHashSet<>();CLAUDE.md forbids FQCNs and OpenRewrite rewrites this during the build, so CI fails on the resulting uncommitted diff. OpenAIToolExecutionProducer imports it correctly — just inconsistent.
3. No documentation
src/main/docs/ is untouched. spring-ai-chat-component.adoc:506+ has a full "Camel route tools" section for the identical feature — camel-openai needs the equivalent (openai-mcp.adoc, or a new openai-tools.adoc cross-linked from openai-component.adoc). Also openai-component.adoc:1247 — "When MCP tools with autoToolExecution are active, streaming automatically falls back to non-streaming" — is now inaccurate, since this PR extends that fallback to route tools.
4. Test coverage stops at the converter
AiToolSpecToOpenAITest is good, but nothing exercises discoverCamelRouteTools, executeCamelRouteTool, argument-to-header passing, or the two-source dispatch / hallucination paths. The infrastructure for a real test already exists: OpenAIToolErrorStrategyTest drives OpenAIMock via @RegisterExtension with no network. A test with an ai-tool: route plus a mocked tool-call response would cover the actual feature; SpringAiChatToolsDiscoveryTest is the precedent for the discovery half.
Correctness
5. No dedup between MCP and route tool names
processInternal adds every MCP tool and then every route tool to paramsBuilder unconditionally. A name collision sends two functions with the same name to the API (400 from OpenAI), and at dispatch time the route tool silently shadows the MCP one. AiToolRegistry enforces uniqueness per tag but knows nothing about MCP. Suggest skipping duplicates when building the tool list, with a warning.
6. The tags description doesn't match the behaviour
It states "Tools with no tags (default pool) are always included", but discoverCamelRouteTools returns Map.of() when tags is empty, so the default pool is only reached once at least one tag is configured (AiToolRegistry.getToolsByTag merges defaultTools per queried tag). The early return matches the sibling components, so I'd reword rather than change behaviour — e.g. "…are included alongside the tools matching the configured tags". The text is baked into the generated catalog, so it needs a regen too.
7. Sanitization is inconsistent within the same method
AiToolResult.ExecutionError deliberately returns the generic "Error: Tool execution failed", but the enclosing catch (Exception e) returns "Error: Tool execution failed: " + e.getMessage(). Both feed a third-party LLM. Either policy is defensible — the toolExecutionErrorStrategy docs already warn that repromptModel sends raw exception messages — but they shouldn't disagree two lines apart.
8. A malformed schema on one tool breaks every request
AiToolSpecToOpenAI.toFunctionTool throws IllegalArgumentException at request-build time, so one bad tool in the tag set fails every chat completion on that endpoint, including requests that would never call it. Prefer logging and skipping the tool (or validating at discovery). Also narrow catch (Exception e) to JsonProcessingException — the current catch reports programming errors as "Failed to parse JSON Schema".
Quality / performance
- Re-parsing per exchange: every request re-parses each tool's JSON schema and rebuilds
FunctionParameters.AiToolRegistryListener(CAMEL-24309) was added precisely to let consumers cache and invalidate on registration changes. AiToolSpecToOpenAIduplicatesMcpToolConverter:55-58verbatim (thetype-defaulting +putAdditionalPropertyloop) — extract a shared helper.discoverCamelRouteToolsnow exists three times in the codebase (twice here, plus near-identical copies inLangChain4jAgentProducerandSpringAiChatProducer). Worth promoting tocamel-ai-toolasAiToolRegistry.getToolsByTags(String)in a follow-up, removing the copy from all call sites.- Nit:
tag.trim()is redundant —AiToolParameterHelper.splitTagsalready splits on\s*,\s*and trims. - Nit: a third
new ObjectMapper()in the component; consider reusing one.
Security
Nothing new of concern. Argument-to-header injection is handled upstream in AiToolExecutor (Camel* / org.apache.camel.* rejection, undeclared-argument filtering), tool execution is isolated in an exchange copy, and ExecutionError messages are not leaked verbatim. Item 7 is the only loose end.
Process
Per CLAUDE.md, AI-assisted branches should be pushed to a fork rather than apache/camel — this one is on the upstream repo (fix/CAMEL-24322). Minor, and pr-cleanup-branches.yml handles it, but worth noting.
Claude Code on behalf of Croway
Summary
Claude Code on behalf of gnodet
Extends the
camel-openaicomponent to discover and execute Camel route tools registered via the sharedAiToolRegistry, alongside existing MCP tools. This implements Step 6 of the unified AI tool abstraction design (CAMEL-23382).Route authors can now expose Camel routes as AI tools via
ai-tool:consumer endpoints and have them automatically available to OpenAI models during function-calling loops — no MCP server required.Changes
camel-ai-toolcompile dependency tocamel-openaitagsparameter toOpenAIConfigurationfor filtering tools by tag from the sharedAiToolRegistryAiToolSpecToOpenAIthat transformsAiToolSpecinto OpenAIChatCompletionFunctionToolusingparametersJsonSchema(follows the same pattern asMcpToolConverter)AiToolRegistry.getOrCreate(context), convert them to OpenAI function tools, and dispatch viaAiToolExecutor.execute()with exchange isolation (ExchangeHelper.createCopy)AiToolSpecToOpenAITestcovering full spec conversion, no parameters, no description, default type, required arrays, empty schema, invalid schema, andadditionalProperties:falseUsage Example
Test Plan
OpenAIToolErrorStrategyTest)AiToolSpecToOpenAITesttests passmvn formatter:format impsort:sort🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com