Skip to content

[camel-main] Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag filtering for langchain4j - #8926

Merged
JiriOndrusek merged 2 commits into
apache:camel-mainfrom
JiriOndrusek:ai-bridge-camel-main
Jul 31, 2026
Merged

[camel-main] Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag filtering for langchain4j#8926
JiriOndrusek merged 2 commits into
apache:camel-mainfrom
JiriOndrusek:ai-bridge-camel-main

Conversation

@JiriOndrusek

Copy link
Copy Markdown
Contributor

Depends on: #8923 (adds the camel-quarkus-ai-tool extension). This PR builds on top of it.

Summary

Fixes #8768

  • Bridges Camel's AiToolRegistry to langchain4j's ToolProvider SPI via CamelAiToolProvider in extensions-support/langchain4j. When both camel-ai-tool and quarkus-langchain4j are on the classpath, the deployment processor auto-registers CamelAiToolProvider as a CDI bean — all Camel ai-tool: routes become available to @RegisterAiService AI services without explicit wiring.
  • Introduces @CamelAiTools("tag") annotation for per-AI-service tool filtering via a CDI interceptor + ThreadLocal pattern. Services annotated with a tag only see tools matching that tag; services without the annotation see all tools.
  • Users must add camel-langchain4j-agent as an explicit dependency (it is optional in the support module and not brought in transitively). This is documented in usage.adoc.

Key files

File Purpose
CamelAiToolProvider.java ToolProvider implementation bridging AiToolRegistry → langchain4j
CamelAiTools.java @InterceptorBinding annotation carrying the tag value
CamelAiToolsInterceptor.java CDI interceptor setting ThreadLocal tag before AI service calls
SupportQuarkusLangchain4jProcessor.java Build-time discovery of @CamelAiTools and CDI bean registration
usage.adoc User-facing documentation with dependency note

Test plan

  • ./mvnw verify -f integration-tests/ai-tool-langchain4j — 6 mock tests pass (weather/admin chat, tag registry filtering, wrong-tag isolation)
  • ./mvnw verify -f integration-tests/ai-tool-langchain4j -Dollama.test=true -Pollama — real LLM tests with Ollama
  • wrongTagShouldNotExposeWeatherTool — mock model conditionally calls getWeather only if visible; confirms test fails when interceptor is broken

🤖 Generated with Claude Code

@JiriOndrusek JiriOndrusek changed the title Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag filtering for langchain4j [camel-main] Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag filtering for langchain4j Jul 27, 2026
@JiriOndrusek

Copy link
Copy Markdown
Contributor Author

@zbendhiba , @jamesnetherton FYI.
The approach seems to be working well.
(the first commit is a part of #8923, therefore you can ignore it)
(the last commit is autogenerated stuff not related to the PC you can ignore it)

@jamesnetherton

Copy link
Copy Markdown
Contributor

Looks good to me mostly. Some feedback:

1. AiToolPresent should also check for camel-langchain4j-agent

AiToolPresent only verifies AiToolRegistry (from camel-ai-tool). But the build steps it gates also register CamelAiToolProvider, whose static initializer eagerly calls resolveToToolSpecification() — which requires AiToolSpecToLangChain4j from camel-langchain4j-agent.

If a user has camel-ai-tool + camel-quarkus-support-langchain4j but forgets the agent dependency, the application crashes at startup with ExceptionInInitializerError. Adding a second class check makes this fail gracefully — the bean simply isn't registered:

@Override
public boolean getAsBoolean() {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    try {
        cl.loadClass("org.apache.camel.component.ai.tool.AiToolRegistry");
        cl.loadClass("org.apache.camel.component.langchain4j.agent.AiToolSpecToLangChain4j");
        return true;
    } catch (ClassNotFoundException e) {
        return false;
    }
}

2. Eager static initializer is fragile

private static final Method TO_TOOL_SPECIFICATION = resolveToToolSpecification();

This runs when CamelAiToolProvider is first loaded. The configureCamelAiToolTags build step accesses CamelAiToolProvider.TAG_MAP (a non-constant static field), which triggers class initialization during STATIC_INIT. If the converter class is absent, the application fails to start — even if the user hasn't configured any ai-tool: routes yet.

Lazy initialization would limit the failure to when provideTools() is actually called, giving a more actionable error:

private static class ConverterHolder {
    static final Method METHOD = resolveToToolSpecification();
}
// Then use ConverterHolder.METHOD in toToolSpecification()

(If finding 1 is addressed, this becomes less critical since the build step won't run at all without both dependencies — but it's still a nice defensive layer.)


3. ThreadLocal save/restore for nested interceptor calls

CamelAiToolsInterceptor unconditionally clears the ThreadLocal in its finally block. If one @CamelAiTools-annotated service calls another (nested on the same thread), the outer tag is lost:

outer interceptor sets "tagA"
  inner interceptor sets "tagB"
    inner service executes (sees "tagB") ✓
  inner finally clears tag
outer service continues (sees null, should see "tagA") ✗

Low-probability scenario, but an easy fix — save and restore:

@AroundInvoke
Object aroundInvoke(InvocationContext ctx) throws Exception {
    Class<?> targetClass = ctx.getTarget().getClass();
    String tag = resolveTag(targetClass);
    String previous = CamelAiToolProvider.getCurrentTag();
    if (tag != null) {
        CamelAiToolProvider.setCurrentTag(tag);
    }
    try {
        return ctx.proceed();
    } finally {
        if (previous != null) {
            CamelAiToolProvider.setCurrentTag(previous);
        } else {
            CamelAiToolProvider.clearCurrentTag();
        }
    }
}

(Requires adding a static String getCurrentTag() method to CamelAiToolProvider that returns CURRENT_TAG.get().)


4. Consider Gizmo code generation instead of runtime reflection

CamelAiToolProvider uses Class.forName() + getMethod() + invoke() to call AiToolSpecToLangChain4j.toToolSpecification() reflectively. This works, but runtime reflection is not idiomatic in Quarkus extensions — the philosophy is to push work to build time.

A Gizmo-generated bridge class would eliminate the reflection, the ReflectiveClassBuildItem, and the eager static initializer:

  1. Define an interface in the runtime module: AiToolSpecConverter { ToolSpecification toToolSpecification(AiToolSpec spec); }
  2. Generate an implementation at build time using Gizmo (all class references as strings — no compile-time dependency on the agent jar)
  3. CamelAiToolProvider injects the interface via CDI

This would also make findings 2 and 3's static initializer concerns moot, since the Gizmo class only exists if the build step runs.


5. Empty @CamelAiTools("") tag value not validated

The Jandex scan checks for annotation.value() == null (handles @CamelAiTools with no explicit value), but @CamelAiTools("") passes through and creates a mapping with an empty-string tag. registry.getToolsByTag("") would return only default-pool tools (no tool is tagged with ""), which could silently confuse users. Should also reject blank values:

String tagValue = annotation.value().asString();
if (tagValue == null || tagValue.isBlank()) {
    LOG.warnf("@CamelAiTools on %s has no/blank value — skipping", className);
    continue;
}

@JiriOndrusek
JiriOndrusek force-pushed the ai-bridge-camel-main branch from ae5f68f to 4716c42 Compare July 28, 2026 08:37
@JiriOndrusek

Copy link
Copy Markdown
Contributor Author

I addressed the feedback in the new commit (which can be squashed with the original one).
The commit fixes concerns, adds doc and fixes some more issues.
I also forgot to mention, that this PR requires apache/camel#25154 to be merged in Camel.

@jamesnetherton

Copy link
Copy Markdown
Contributor

Feel free to merge apache/camel#25154 so we can move this forwards.

@JiriOndrusek
JiriOndrusek force-pushed the ai-bridge-camel-main branch from 4716c42 to 6aae5ec Compare July 28, 2026 09:31
@JiriOndrusek
JiriOndrusek marked this pull request as ready for review July 28, 2026 09:32
@JiriOndrusek
JiriOndrusek force-pushed the ai-bridge-camel-main branch 2 times, most recently from ffaff10 to fcc0d6b Compare July 30, 2026 12:23
@JiriOndrusek

Copy link
Copy Markdown
Contributor Author

PR is rebased and squashed into 1 commit.

@JiriOndrusek
JiriOndrusek force-pushed the ai-bridge-camel-main branch from f571c63 to e62bdb8 Compare July 30, 2026 15:15
JiriOndrusek and others added 2 commits July 30, 2026 17:23
…ag filtering for langchain4j

Bridges Camel's AiToolRegistry to langchain4j's ToolProvider SPI in
extensions-support/langchain4j. When both camel-ai-tool and
quarkus-langchain4j are on the classpath, the deployment processor
auto-registers CamelAiToolProvider as a CDI bean, making Camel AI tools
available to @RegisterAiService AI services without explicit wiring.

Introduces @CamelAiTools("tag") annotation for per-AI-service tool
filtering via a CDI interceptor + ThreadLocal pattern. Tag resolution:
ThreadLocal present -> getToolsByTag(), absent -> getAllTools(). Services
without @CamelAiTools always see all tools.

Includes integration tests with mock ToolCallingChatModels verifying
end-to-end tool invocation and tag isolation across weather + admin
AI services, plus an Ollama-based test behind -Pollama.

Document LangChain4j integration and @CamelAiTools tag filtering in ai-tool usage.adoc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JiriOndrusek
JiriOndrusek force-pushed the ai-bridge-camel-main branch from e62bdb8 to 59a416a Compare July 30, 2026 15:23
@JiriOndrusek
JiriOndrusek merged commit cb4229c into apache:camel-main Jul 31, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants