Skip to content

CAMEL-24322: Add tool-calling support via AiToolRegistry - #25289

Open
gnodet wants to merge 2 commits into
mainfrom
fix/CAMEL-24322
Open

CAMEL-24322: Add tool-calling support via AiToolRegistry#25289
gnodet wants to merge 2 commits into
mainfrom
fix/CAMEL-24322

Conversation

@gnodet

@gnodet gnodet commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Claude Code on behalf of gnodet

Extends 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).

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

  • Dependency: Add camel-ai-tool compile dependency to camel-openai
  • Configuration: Add tags parameter to OpenAIConfiguration for filtering tools by tag from the shared AiToolRegistry
  • Converter: Create AiToolSpecToOpenAI that transforms AiToolSpec into OpenAI ChatCompletionFunctionTool using parametersJsonSchema (follows the same pattern as McpToolConverter)
  • OpenAIProducer: Extend the agentic loop to discover Camel route tools via AiToolRegistry.getOrCreate(context), convert them to OpenAI function tools, and dispatch via AiToolExecutor.execute() with exchange isolation (ExchangeHelper.createCopy)
  • OpenAIToolExecutionProducer: Same extension for manual tool-loop routes
  • Error handling: Both producers check Camel route tools first, then fall back to MCP; hallucinated tool name and tool execution error strategies apply to both sources
  • Generated files: Updated catalog JSON, endpoint DSL factory, configurer, and URI factory
  • Tests: 8 unit tests in AiToolSpecToOpenAITest covering full spec conversion, no parameters, no description, default type, required arrays, empty schema, invalid schema, and additionalProperties:false

Usage Example

// Register a route as an AI tool
from("ai-tool:getWeather?description=Get current weather&tags=weather"
    + "&parameters.city=string&parameters.city.description=City name&parameters.city.required=true")
    .process(exchange -> {
        String city = exchange.getIn().getHeader("city", String.class);
        exchange.getIn().setBody("Sunny, 22°C in " + city);
    });

// OpenAI will auto-discover and call it
from("direct:chat")
    .to("openai:chat-completion?model=gpt-4o&tags=weather&autoToolExecution=true");

Test Plan

  • All 163 existing unit tests pass (including updated OpenAIToolErrorStrategyTest)
  • 8 new AiToolSpecToOpenAITest tests pass
  • Code formatted with mvn formatter:format impsort:sort
  • Generated files regenerated and committed (catalog JSON, endpoint DSL factory)
  • CI build passes

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

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>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@gnodet
gnodet marked this pull request as ready for review August 3, 2026 22:08
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  1. 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.

  2. 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<>();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
return "Error: Tool execution failed: " + e.getMessage();
return "Error: Tool execution failed";

The same pattern applies to OpenAIToolExecutionProducer at line 291.

@zbendhiba
zbendhiba requested review from Croway and zbendhiba August 5, 2026 08:16

@Croway Croway left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + discoverCamelRouteTools currently exist twice in this PR);
  • gives route tools parallelToolExecution / parallelToolTimeout for free;
  • keeps hallucinated-name and error-strategy handling in one place — the stated reason that class exists ("cannot drift between the two");
  • makes returnDirect=false for route tools fall out of errorResult/ToolResult naturally, instead of the manual allReturnDirect = 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.
  • AiToolSpecToOpenAI duplicates McpToolConverter:55-58 verbatim (the type-defaulting + putAdditionalProperty loop) — extract a shared helper.
  • discoverCamelRouteTools now exists three times in the codebase (twice here, plus near-identical copies in LangChain4jAgentProducer and SpringAiChatProducer). Worth promoting to camel-ai-tool as AiToolRegistry.getToolsByTags(String) in a follow-up, removing the copy from all call sites.
  • Nit: tag.trim() is redundant — AiToolParameterHelper.splitTags already 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants