You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ja...@apache.org on 2026/08/11 14:25:26 UTC
(camel-quarkus) 03/11: Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag filtering for langchain4j
This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit 799a354a1cc5b5f81ebf377cca0b50acdcac0ee7
Author: Jiri Ondrusek <on...@gmail.com>
AuthorDate: Mon Jul 27 16:55:48 2026 +0200
Fixes #8768. Add CamelAiToolProvider bridge and @CamelAiTools tag 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 <no...@anthropic.com>
---
.../ROOT/pages/reference/extensions/ai-tool.adoc | 150 ++++++++++++++
.../ROOT/pages/reference/extensions/core.adoc | 20 ++
extensions-support/langchain4j/deployment/pom.xml | 5 +
.../langchain4j/deployment/AiToolPresent.java | 36 ++++
.../SupportQuarkusLangchain4jProcessor.java | 111 ++++++++++-
extensions-support/langchain4j/runtime/pom.xml | 10 +
...ain4jRecorder.java => AiToolSpecConverter.java} | 28 +--
.../support/langchain4j/CamelAiToolProvider.java | 129 ++++++++++++
.../support/langchain4j/CamelAiTools.java | 48 +++++
.../langchain4j/CamelAiToolsInterceptor.java | 73 +++++++
.../langchain4j/QuarkusLangchain4jRecorder.java | 5 +
extensions/ai-tool/runtime/src/main/doc/usage.adoc | 97 +++++++++
integration-tests/ai-tool-langchain4j/README.adoc | 15 ++
integration-tests/ai-tool-langchain4j/pom.xml | 222 +++++++++++++++++++++
.../it/AiToolLangchain4jOllamaResource.java | 54 +++++
.../langchain4j/it/AiToolLangchain4jResource.java | 90 +++++++++
.../langchain4j/it/AiToolLangchain4jRoutes.java | 40 ++++
.../it/model/AbstractToolCallingChatModel.java | 93 +++++++++
.../it/model/AdminToolCallingChatModel.java | 45 +++++
.../it/model/WeatherToolCallingChatModel.java | 45 +++++
.../it/model/WrongTagToolCallingChatModel.java | 78 ++++++++
.../langchain4j/it/service/AdminAiService.java | 31 +++
.../langchain4j/it/service/WeatherAiService.java | 31 +++
.../it/service/WeatherAiServiceOllama.java | 30 +++
.../it/service/WeatherAiServiceOllamaWrongTag.java | 30 +++
.../it/service/WeatherAiServiceWrongTag.java | 31 +++
.../src/main/resources/application.properties | 18 ++
.../tool/langchain4j/it/AiToolLangchain4jIT.java | 23 +++
.../langchain4j/it/AiToolLangchain4jOllamaIT.java | 25 +++
.../it/AiToolLangchain4jOllamaTest.java | 68 +++++++
.../tool/langchain4j/it/AiToolLangchain4jTest.java | 97 +++++++++
integration-tests/pom.xml | 2 +
tooling/scripts/test-categories.yaml | 2 +
33 files changed, 1762 insertions(+), 20 deletions(-)
diff --git a/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc b/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc
new file mode 100644
index 0000000000..1012044469
--- /dev/null
+++ b/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc
@@ -0,0 +1,150 @@
+// Do not edit directly!
+// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page
+[id="extensions-ai-tool"]
+= AI Tool
+:linkattrs:
+:cq-artifact-id: camel-quarkus-ai-tool
+:cq-native-supported: true
+:cq-status: Stable
+:cq-status-deprecation: Stable
+:cq-description: Framework-agnostic consumer endpoint that registers a Camel route as an LLM tool in the shared AiToolRegistry.
+:cq-deprecated: false
+:cq-jvm-since: 3.39.0
+:cq-native-since: 3.39.0
+
+ifeval::[{doc-show-badges} == true]
+[.badges]
+[.badge-key]##JVM since##[.badge-supported]##3.39.0## [.badge-key]##Native since##[.badge-supported]##3.39.0##
+endif::[]
+
+Framework-agnostic consumer endpoint that registers a Camel route as an LLM tool in the shared AiToolRegistry.
+
+[id="extensions-ai-tool-whats-inside"]
+== What's inside
+
+* xref:{cq-camel-components}::ai-tool-component.adoc[AI Tool component], URI syntax: `ai-tool:toolName`
+
+Please refer to the above link for usage and configuration details.
+
+[id="extensions-ai-tool-maven-coordinates"]
+== Maven coordinates
+
+https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-ai-tool[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"]
+
+Or add the coordinates to your existing project:
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-ai-tool</artifactId>
+</dependency>
+----
+ifeval::[{doc-show-user-guide-link} == true]
+Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications.
+endif::[]
+
+[id="extensions-ai-tool-usage"]
+== Usage
+The `ai-tool` component is a framework-agnostic consumer endpoint that registers Camel routes as LLM-callable tools in a shared `AiToolRegistry`. Framework-specific extensions (such as `camel-quarkus-langchain4j-tools`, `camel-quarkus-openai`) discover and invoke these tools at runtime.
+
+[id="extensions-ai-tool-usage-defining-a-tool"]
+=== Defining a tool
+
+Use the `ai-tool` consumer endpoint to register a route as an AI tool. Tool parameters are declared via URI options.
+
+[source,java]
+----
+public class MyRoutes extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("ai-tool:getWeather?tags=weather"
+ + "&description=Get the current weather for a city"
+ + "¶meter.city=string"
+ + "¶meter.city.description=The city name"
+ + "¶meter.city.required=true"
+ + "¶meter.unit=string"
+ + "¶meter.unit.enum=celsius,fahrenheit")
+ .setBody(simple("Sunny in ${header.city}, temperature in ${header.unit}"));
+ }
+}
+----
+
+Arguments passed by the LLM are set as exchange headers (e.g. `${header.city}`).
+
+[id="extensions-ai-tool-usage-tag-based-grouping"]
+=== Tag-based grouping
+
+Tools can be grouped by tags. AI producer endpoints use tags to discover a subset of available tools.
+
+A tool with no tags is placed in a default pool and is available to all tag queries.
+
+[source,java]
+----
+// Tagged tool — only visible to producers querying the "support" tag
+from("ai-tool:lookupOrder?tags=support&description=Look up order status"
+ + "¶meter.orderId=string¶meter.orderId.required=true")
+ .to("sql:SELECT status FROM orders WHERE id = :#${header.orderId}");
+
+// Untagged tool — available to all producers
+from("ai-tool:greet?description=Greet a user"
+ + "¶meter.name=string¶meter.name.required=true")
+ .setBody(simple("Hello, ${header.name}!"));
+----
+
+[id="extensions-ai-tool-usage-langchain4j-integration"]
+=== LangChain4j integration
+
+When both `camel-quarkus-ai-tool` and a Quarkus LangChain4j extension (`quarkus-langchain4j-ollama`, `quarkus-langchain4j-openai`, etc.) are on the classpath, a `ToolProvider` CDI bean is registered automatically at build time.
+All routes registered via `ai-tool:` endpoints become available to `@RegisterAiService` AI services without explicit wiring.
+
+You must also add `camel-langchain4j-agent` to your project dependencies. This artifact provides the `AiToolSpec` to `ToolSpecification` conversion and is not brought in transitively.
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-langchain4j-agent</artifactId>
+</dependency>
+----
+
+With both dependencies in place, a `@RegisterAiService` interface can call Camel tools directly:
+
+[source,java]
+----
+@RegisterAiService
+public interface WeatherAiService {
+ String chat(@UserMessage String question);
+}
+----
+
+Any `ai-tool:` route is now callable by the LLM behind `WeatherAiService`. No `toolProvider` attribute or manual bean reference is needed.
+
+[id="extensions-ai-tool-usage-tag-filtering-with-camelaitools"]
+==== Tag filtering with `@CamelAiTools`
+
+By default, all registered `ai-tool:` routes are visible to every AI service. When you need different AI services to see different subsets of tools, annotate the service interface with `@CamelAiTools` and specify a tag value that matches the `tags` parameter on your `ai-tool:` routes.
+
+[source,java]
+----
+@RegisterAiService
+@CamelAiTools("weather")
+public interface WeatherAgent {
+ String chat(@UserMessage String question);
+}
+
+@RegisterAiService
+@CamelAiTools("support")
+public interface SupportAgent {
+ String chat(@UserMessage String question);
+}
+----
+
+With this setup:
+
+* `WeatherAgent` sees only tools tagged with `weather` (plus any untagged tools from the default pool).
+* `SupportAgent` sees only tools tagged with `support` (plus any untagged tools from the default pool).
+* A `@RegisterAiService` without `@CamelAiTools` sees all registered tools regardless of tags.
+
+NOTE: The `@CamelAiTools` value must match a tag declared in the `ai-tool:` endpoint URI (e.g. `from("ai-tool:getWeather?tags=weather&...")`). If the annotation value is blank or does not match any route tag, the AI service will only see untagged tools.
+
diff --git a/docs/modules/ROOT/pages/reference/extensions/core.adoc b/docs/modules/ROOT/pages/reference/extensions/core.adoc
index a55cb291d9..66a56d8c56 100644
--- a/docs/modules/ROOT/pages/reference/extensions/core.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/core.adoc
@@ -375,6 +375,26 @@ Prints the `CamelMain` usage statement but allows the application startup to pro
| `fail`, `warn`, `ignore`
| `warn`
+a| [[camel-dataformat-data-format-configs]]`link:#camel-dataformat-data-format-configs[camel.dataformat."data-format-configs"]`
+
+Camel data format configuration.
+
+The format of the configuration is as follows.
+
+[source,properties]
+----
+camel.dataformat.<name>.<property> = value
+----
+
+For example.
+[source,properties]
+----
+camel.dataformat.beanio.stream-name = test-stream
+camel.dataformat.beanio.mapping = test-mapping.xml
+----
+| `Map<String,Map<String,String>>`
+|
+
a| [[quarkus-camel-bootstrap-enabled]]`link:#quarkus-camel-bootstrap-enabled[quarkus.camel.bootstrap.enabled]`
When set to true, the {@link CamelRuntime} will be started automatically.
diff --git a/extensions-support/langchain4j/deployment/pom.xml b/extensions-support/langchain4j/deployment/pom.xml
index 34dc906096..d0e48ad94c 100644
--- a/extensions-support/langchain4j/deployment/pom.xml
+++ b/extensions-support/langchain4j/deployment/pom.xml
@@ -42,6 +42,11 @@
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-support-langchain4j</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-ai-tool</artifactId>
+ <optional>true</optional>
+ </dependency>
</dependencies>
<build>
diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java
new file mode 100644
index 0000000000..7168ec2d43
--- /dev/null
+++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/AiToolPresent.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.support.langchain4j.deployment;
+
+import java.util.function.BooleanSupplier;
+
+public class AiToolPresent implements BooleanSupplier {
+ private static final String AI_TOOL_REGISTRY_CLASS = "org.apache.camel.component.ai.tool.AiToolRegistry";
+ private static final String AI_TOOL_SPEC_CONVERTER_CLASS = "org.apache.camel.component.langchain4j.agent.AiToolSpecToLangChain4j";
+
+ @Override
+ public boolean getAsBoolean() {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ try {
+ cl.loadClass(AI_TOOL_REGISTRY_CLASS);
+ cl.loadClass(AI_TOOL_SPEC_CONVERTER_CLASS);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java
index 72af82eb3f..92133cb549 100644
--- a/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java
+++ b/extensions-support/langchain4j/deployment/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/deployment/SupportQuarkusLangchain4jProcessor.java
@@ -16,13 +16,19 @@
*/
package org.apache.camel.quarkus.component.support.langchain4j.deployment;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import dev.langchain4j.guardrail.Guardrail;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrail;
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
+import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
@@ -34,7 +40,14 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
+import io.quarkus.gizmo.ClassCreator;
+import io.quarkus.gizmo.MethodCreator;
+import io.quarkus.gizmo.MethodDescriptor;
+import io.quarkus.gizmo.ResultHandle;
import jakarta.inject.Singleton;
+import org.apache.camel.quarkus.component.support.langchain4j.AiToolSpecConverter;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolProvider;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiToolsInterceptor;
import org.apache.camel.quarkus.component.support.langchain4j.QuarkusLangchain4jRecorder;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
@@ -53,6 +66,8 @@ class SupportQuarkusLangchain4jProcessor {
public static final DotName REGISTER_AI_SERVICES_DOTNAME = DotName
.createSimple("io.quarkiverse.langchain4j.RegisterAiService");
+ private static final DotName CAMEL_AI_TOOLS_DOTNAME = DotName
+ .createSimple("org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools");
private static final Logger LOG = Logger.getLogger(SupportQuarkusLangchain4jProcessor.class);
@@ -132,10 +147,104 @@ class SupportQuarkusLangchain4jProcessor {
.build());
}
+ @BuildStep(onlyIf = AiToolPresent.class)
+ AdditionalBeanBuildItem registerCamelAiToolProvider() {
+ LOG.info("Camel AI Tool detected - registering CamelAiToolProvider as CDI bean for ToolProvider auto-discovery");
+ return AdditionalBeanBuildItem.unremovableOf(CamelAiToolProvider.class);
+ }
+
+ @BuildStep(onlyIf = AiToolPresent.class)
+ void generateAiToolSpecConverter(BuildProducer<GeneratedBeanBuildItem> generatedBeans,
+ BuildProducer<UnremovableBeanBuildItem> unremovableBeans) {
+
+ String generatedClassName = "org.apache.camel.quarkus.component.support.langchain4j.AiToolSpecConverterImpl";
+
+ try (ClassCreator cc = ClassCreator.builder()
+ .classOutput(new GeneratedBeanGizmoAdaptor(generatedBeans))
+ .className(generatedClassName)
+ .interfaces(AiToolSpecConverter.class)
+ .build()) {
+
+ cc.addAnnotation(Singleton.class);
+
+ String toolSpecClass = "dev.langchain4j.agent.tool.ToolSpecification";
+ String aiToolSpecClass = "org.apache.camel.component.ai.tool.AiToolSpec";
+
+ try (MethodCreator mc = cc.getMethodCreator("toToolSpecification",
+ toolSpecClass, aiToolSpecClass)) {
+
+ ResultHandle spec = mc.getMethodParam(0);
+ ResultHandle result = mc.invokeStaticMethod(
+ MethodDescriptor.ofMethod(
+ "org.apache.camel.component.langchain4j.agent.AiToolSpecToLangChain4j",
+ "toToolSpecification",
+ toolSpecClass,
+ aiToolSpecClass),
+ spec);
+ mc.returnValue(result);
+ }
+ }
+
+ unremovableBeans.produce(beanClassNames(generatedClassName));
+ }
+
+ @BuildStep(onlyIf = AiToolPresent.class)
+ @Record(ExecutionTime.STATIC_INIT)
+ void configureCamelAiToolTags(
+ CombinedIndexBuildItem combinedIndex,
+ BuildProducer<AdditionalBeanBuildItem> additionalBeans,
+ QuarkusLangchain4jRecorder recorder) {
+
+ IndexView index = combinedIndex.getIndex();
+ Map<String, String> tagMap = new HashMap<>();
+ for (AnnotationInstance annotation : index.getAnnotations(CAMEL_AI_TOOLS_DOTNAME)) {
+ if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) {
+ String className = annotation.target().asClass().name().toString();
+ if (annotation.value() == null) {
+ LOG.warnf("@CamelAiTools on %s has no value — skipping", className);
+ continue;
+ }
+ String tagValue = annotation.value().asString();
+ if (tagValue.isBlank()) {
+ LOG.warnf("@CamelAiTools on %s has blank value — skipping", className);
+ continue;
+ }
+ tagMap.put(className, tagValue);
+ LOG.infof("Discovered @CamelAiTools(\"%s\") on %s", tagValue, className);
+ }
+ }
+
+ if (tagMap.isEmpty()) {
+ return;
+ }
+
+ recorder.setCamelAiToolTagMap(tagMap);
+
+ additionalBeans.produce(AdditionalBeanBuildItem.builder()
+ .addBeanClasses(CamelAiToolsInterceptor.class)
+ .setUnremovable()
+ .build());
+ }
+
@BuildStep
- void markAiServicesAsUnremovable(
+ void validateAndRegisterAiServices(
CombinedIndexBuildItem indexBuildItem,
BuildProducer<UnremovableBeanBuildItem> unremovableBeans) {
+
+ if (!new AiToolPresent().getAsBoolean()) {
+ Collection<AnnotationInstance> aiToolsAnnotations = indexBuildItem.getIndex()
+ .getAnnotations(CAMEL_AI_TOOLS_DOTNAME);
+ if (!aiToolsAnnotations.isEmpty()) {
+ LOG.warnf("@CamelAiTools annotations found but camel-langchain4j-agent is not on the classpath. "
+ + "Add camel-langchain4j-agent dependency to enable the Camel AI tool bridge. "
+ + "Affected classes: %s",
+ aiToolsAnnotations.stream()
+ .filter(a -> a.target().kind() == AnnotationTarget.Kind.CLASS)
+ .map(a -> a.target().asClass().name().toString())
+ .collect(Collectors.joining(", ")));
+ }
+ }
+
LOG.debug("Discovering classes annotated with @RegisterAiService to mark implementation beans as unremovable");
for (AnnotationInstance instance : indexBuildItem.getIndex().getAnnotations(REGISTER_AI_SERVICES_DOTNAME)) {
diff --git a/extensions-support/langchain4j/runtime/pom.xml b/extensions-support/langchain4j/runtime/pom.xml
index 85b61f8a6e..9214158d85 100644
--- a/extensions-support/langchain4j/runtime/pom.xml
+++ b/extensions-support/langchain4j/runtime/pom.xml
@@ -50,6 +50,16 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-ai-tool</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-langchain4j-agent</artifactId>
+ <optional>true</optional>
+ </dependency>
</dependencies>
<build>
diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java
similarity index 51%
copy from extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java
copy to extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java
index af6301a6c9..adf443d9fc 100644
--- a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java
+++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/AiToolSpecConverter.java
@@ -16,24 +16,14 @@
*/
package org.apache.camel.quarkus.component.support.langchain4j;
-import java.lang.reflect.InvocationTargetException;
+import dev.langchain4j.agent.tool.ToolSpecification;
+import org.apache.camel.component.ai.tool.AiToolSpec;
-import dev.langchain4j.guardrail.Guardrail;
-import io.quarkus.runtime.RuntimeValue;
-import io.quarkus.runtime.annotations.Recorder;
-import org.jboss.logging.Logger;
-
-@Recorder
-public class QuarkusLangchain4jRecorder {
-
- public RuntimeValue<Guardrail<?, ?>> instantiateGuardrails(Class<Guardrail<?, ?>> guardrailClass) {
- try {
- return new RuntimeValue<>(guardrailClass.getConstructor().newInstance());
- } catch (NoSuchMethodException | InvocationTargetException | InstantiationException
- | IllegalAccessException e) {
- Logger.getLogger(QuarkusLangchain4jRecorder.class).debugf(e,
- "Can not instantiate guardrail of class %s", guardrailClass.getName());
- return null;
- }
- }
+/**
+ * Converts a Camel {@link AiToolSpec} to a langchain4j {@link ToolSpecification}.
+ * The implementation is generated at build time via Gizmo to avoid a compile-time
+ * dependency on {@code camel-langchain4j-agent}.
+ */
+public interface AiToolSpecConverter {
+ ToolSpecification toToolSpecification(AiToolSpec spec);
}
diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java
new file mode 100644
index 0000000000..dbe66ddf67
--- /dev/null
+++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolProvider.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.support.langchain4j;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.service.tool.ToolExecutor;
+import dev.langchain4j.service.tool.ToolProvider;
+import dev.langchain4j.service.tool.ToolProviderRequest;
+import dev.langchain4j.service.tool.ToolProviderResult;
+import jakarta.inject.Inject;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.ai.tool.AiToolExecutor;
+import org.apache.camel.component.ai.tool.AiToolRegistry;
+import org.apache.camel.component.ai.tool.AiToolResult;
+import org.apache.camel.component.ai.tool.AiToolSpec;
+import org.apache.camel.support.DefaultExchange;
+import org.jboss.logging.Logger;
+
+/**
+ * Bridges Camel's {@link AiToolRegistry} to langchain4j's {@link ToolProvider} SPI. When registered as a CDI bean (done
+ * automatically by the deployment processor when both {@code camel-ai-tool} and quarkus-langchain4j are on the
+ * classpath), all Camel routes registered via {@code ai-tool:} endpoints become available to
+ * {@code @RegisterAiService} AI services without explicit configuration.
+ */
+public class CamelAiToolProvider implements ToolProvider {
+
+ private static final Logger LOG = Logger.getLogger(CamelAiToolProvider.class);
+ static final Map<String, String> TAG_MAP = new ConcurrentHashMap<>();
+ // Set by CamelAiToolsInterceptor before each AI service call so provideTools() can filter by the calling service's tag
+ private static final ThreadLocal<String> CURRENT_TAG = new ThreadLocal<>();
+
+ @Inject
+ CamelContext camelContext;
+
+ @Inject
+ ObjectMapper objectMapper;
+
+ @Inject
+ AiToolSpecConverter converter;
+
+ static String getCurrentTag() {
+ return CURRENT_TAG.get();
+ }
+
+ static void setCurrentTag(String tag) {
+ CURRENT_TAG.set(tag);
+ }
+
+ static void clearCurrentTag() {
+ CURRENT_TAG.remove();
+ }
+
+ @Override
+ public ToolProviderResult provideTools(ToolProviderRequest request) {
+ AiToolRegistry registry = AiToolRegistry.getOrCreate(camelContext);
+ String effectiveTag = CURRENT_TAG.get();
+ Set<AiToolSpec> tools = effectiveTag != null ? registry.getToolsByTag(effectiveTag) : registry.getAllTools();
+
+ ToolProviderResult.Builder resultBuilder = ToolProviderResult.builder();
+ for (AiToolSpec spec : tools) {
+ ToolSpecification toolSpec = converter.toToolSpecification(spec);
+ ToolExecutor executor = createExecutor(spec);
+ resultBuilder.add(toolSpec, executor);
+ }
+
+ return resultBuilder.build();
+ }
+
+ private ToolExecutor createExecutor(AiToolSpec spec) {
+ return (ToolExecutionRequest request, Object memoryId) -> {
+ Map<String, Object> arguments = parseArguments(request);
+ if (arguments == null) {
+ return "Invalid arguments: could not parse the provided JSON arguments: " + request.arguments();
+ }
+ Exchange exchange = new DefaultExchange(camelContext);
+ AiToolResult result = AiToolExecutor.execute(spec, arguments, exchange);
+ return toToolResponse(spec.getName(), result);
+ };
+ }
+
+ private Map<String, Object> parseArguments(ToolExecutionRequest request) {
+ String jsonArguments = request.arguments();
+ if (jsonArguments == null || jsonArguments.trim().isEmpty()) {
+ return Map.of();
+ }
+ try {
+ return objectMapper.readValue(jsonArguments, new TypeReference<>() {
+ });
+ } catch (Exception e) {
+ LOG.debugf(e, "Failed to parse tool arguments: %s", jsonArguments);
+ return null;
+ }
+ }
+
+ private String toToolResponse(String toolName, AiToolResult result) {
+ if (result instanceof AiToolResult.Success success) {
+ return success.value();
+ } else if (result instanceof AiToolResult.ArgumentError error) {
+ return "Invalid arguments: " + error.message();
+ } else if (result instanceof AiToolResult.ExecutionError error) {
+ LOG.warnf("Tool '%s' execution failed: %s", toolName, error.message());
+ return "Tool execution failed";
+ }
+ return "Tool execution failed";
+ }
+
+}
diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java
new file mode 100644
index 0000000000..8be0eeacbf
--- /dev/null
+++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiTools.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.support.langchain4j;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import jakarta.enterprise.util.Nonbinding;
+import jakarta.interceptor.InterceptorBinding;
+
+/**
+ * Filters the Camel AI tools exposed to a {@code @RegisterAiService} by tag. When placed on a
+ * {@code @RegisterAiService} interface, only {@code ai-tool:} routes whose {@code tags} parameter includes the
+ * specified value (plus any routes in the default pool) are provided to the AI service.
+ *
+ * <pre>
+ * @RegisterAiService
+ * @CamelAiTools("support")
+ * public interface SupportAgent {
+ * String chat(@UserMessage String message);
+ * }
+ * </pre>
+ */
+@InterceptorBinding
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface CamelAiTools {
+ @Nonbinding
+ String value() default "";
+}
diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java
new file mode 100644
index 0000000000..239ab26976
--- /dev/null
+++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/CamelAiToolsInterceptor.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.support.langchain4j;
+
+import jakarta.annotation.Priority;
+import jakarta.interceptor.AroundInvoke;
+import jakarta.interceptor.Interceptor;
+import jakarta.interceptor.InvocationContext;
+
+/**
+ * CDI interceptor that sets the current Camel AI tool tag on a ThreadLocal before an AI service method executes.
+ * This allows {@link CamelAiToolProvider#provideTools} to filter tools by the tag associated with the calling AI
+ * service,
+ * enabling multiple {@code @RegisterAiService} interfaces with different {@code @CamelAiTools} tags in the same
+ * application.
+ */
+@Interceptor
+@CamelAiTools
+@Priority(Interceptor.Priority.LIBRARY_BEFORE + 100)
+public class CamelAiToolsInterceptor {
+
+ @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();
+ }
+ }
+ }
+
+ private String resolveTag(Class<?> targetClass) {
+ // Walk the full class hierarchy: ArC creates $$QuarkusImpl_Subclass extending $$QuarkusImpl,
+ // and the @CamelAiTools interface is declared on $$QuarkusImpl (not the subclass), so we
+ // must check superclasses and all their interfaces.
+ for (Class<?> clazz = targetClass; clazz != null && clazz != Object.class; clazz = clazz.getSuperclass()) {
+ String tag = CamelAiToolProvider.TAG_MAP.get(clazz.getName());
+ if (tag != null) {
+ return tag;
+ }
+ for (Class<?> iface : clazz.getInterfaces()) {
+ tag = CamelAiToolProvider.TAG_MAP.get(iface.getName());
+ if (tag != null) {
+ return tag;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java
index af6301a6c9..ac23912b1b 100644
--- a/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java
+++ b/extensions-support/langchain4j/runtime/src/main/java/org/apache/camel/quarkus/component/support/langchain4j/QuarkusLangchain4jRecorder.java
@@ -17,6 +17,7 @@
package org.apache.camel.quarkus.component.support.langchain4j;
import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
import dev.langchain4j.guardrail.Guardrail;
import io.quarkus.runtime.RuntimeValue;
@@ -26,6 +27,10 @@ import org.jboss.logging.Logger;
@Recorder
public class QuarkusLangchain4jRecorder {
+ public void setCamelAiToolTagMap(Map<String, String> tagMap) {
+ CamelAiToolProvider.TAG_MAP.putAll(tagMap);
+ }
+
public RuntimeValue<Guardrail<?, ?>> instantiateGuardrails(Class<Guardrail<?, ?>> guardrailClass) {
try {
return new RuntimeValue<>(guardrailClass.getConstructor().newInstance());
diff --git a/extensions/ai-tool/runtime/src/main/doc/usage.adoc b/extensions/ai-tool/runtime/src/main/doc/usage.adoc
new file mode 100644
index 0000000000..36359bf410
--- /dev/null
+++ b/extensions/ai-tool/runtime/src/main/doc/usage.adoc
@@ -0,0 +1,97 @@
+The `ai-tool` component is a framework-agnostic consumer endpoint that registers Camel routes as LLM-callable tools in a shared `AiToolRegistry`. Framework-specific extensions (such as `camel-quarkus-langchain4j-tools`, `camel-quarkus-openai`) discover and invoke these tools at runtime.
+
+=== Defining a tool
+
+Use the `ai-tool` consumer endpoint to register a route as an AI tool. Tool parameters are declared via URI options.
+
+[source,java]
+----
+public class MyRoutes extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("ai-tool:getWeather?tags=weather"
+ + "&description=Get the current weather for a city"
+ + "¶meter.city=string"
+ + "¶meter.city.description=The city name"
+ + "¶meter.city.required=true"
+ + "¶meter.unit=string"
+ + "¶meter.unit.enum=celsius,fahrenheit")
+ .setBody(simple("Sunny in ${header.city}, temperature in ${header.unit}"));
+ }
+}
+----
+
+Arguments passed by the LLM are set as exchange headers (e.g. `${header.city}`).
+
+=== Tag-based grouping
+
+Tools can be grouped by tags. AI producer endpoints use tags to discover a subset of available tools.
+
+A tool with no tags is placed in a default pool and is available to all tag queries.
+
+[source,java]
+----
+// Tagged tool — only visible to producers querying the "support" tag
+from("ai-tool:lookupOrder?tags=support&description=Look up order status"
+ + "¶meter.orderId=string¶meter.orderId.required=true")
+ .to("sql:SELECT status FROM orders WHERE id = :#${header.orderId}");
+
+// Untagged tool — available to all producers
+from("ai-tool:greet?description=Greet a user"
+ + "¶meter.name=string¶meter.name.required=true")
+ .setBody(simple("Hello, ${header.name}!"));
+----
+
+=== LangChain4j integration
+
+When both `camel-quarkus-ai-tool` and a Quarkus LangChain4j extension (`quarkus-langchain4j-ollama`, `quarkus-langchain4j-openai`, etc.) are on the classpath, a `ToolProvider` CDI bean is registered automatically at build time.
+All routes registered via `ai-tool:` endpoints become available to `@RegisterAiService` AI services without explicit wiring.
+
+You must also add `camel-langchain4j-agent` to your project dependencies. This artifact provides the `AiToolSpec` to `ToolSpecification` conversion and is not brought in transitively.
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-langchain4j-agent</artifactId>
+</dependency>
+----
+
+With both dependencies in place, a `@RegisterAiService` interface can call Camel tools directly:
+
+[source,java]
+----
+@RegisterAiService
+public interface WeatherAiService {
+ String chat(@UserMessage String question);
+}
+----
+
+Any `ai-tool:` route is now callable by the LLM behind `WeatherAiService`. No `toolProvider` attribute or manual bean reference is needed.
+
+==== Tag filtering with `@CamelAiTools`
+
+By default, all registered `ai-tool:` routes are visible to every AI service. When you need different AI services to see different subsets of tools, annotate the service interface with `@CamelAiTools` and specify a tag value that matches the `tags` parameter on your `ai-tool:` routes.
+
+[source,java]
+----
+@RegisterAiService
+@CamelAiTools("weather")
+public interface WeatherAgent {
+ String chat(@UserMessage String question);
+}
+
+@RegisterAiService
+@CamelAiTools("support")
+public interface SupportAgent {
+ String chat(@UserMessage String question);
+}
+----
+
+With this setup:
+
+* `WeatherAgent` sees only tools tagged with `weather` (plus any untagged tools from the default pool).
+* `SupportAgent` sees only tools tagged with `support` (plus any untagged tools from the default pool).
+* A `@RegisterAiService` without `@CamelAiTools` sees all registered tools regardless of tags.
+
+NOTE: The `@CamelAiTools` value must match a tag declared in the `ai-tool:` endpoint URI (e.g. `from("ai-tool:getWeather?tags=weather&...")`). If the annotation value is blank or does not match any route tag, the AI service will only see untagged tools.
diff --git a/integration-tests/ai-tool-langchain4j/README.adoc b/integration-tests/ai-tool-langchain4j/README.adoc
new file mode 100644
index 0000000000..40a56d5e24
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/README.adoc
@@ -0,0 +1,15 @@
+== Camel Quarkus AI Tool LangChain4j Integration Tests
+
+By default, the tests use a mock `ChatModel` that simulates tool calling without a real LLM.
+
+=== Running with a real Ollama LLM
+
+To run additional tests against a real Ollama model (qwen3:1.7b) via dev services, activate the `ollama` Maven profile.
+This requires Docker to be running.
+
+[source,shell]
+----
+./mvnw verify -f integration-tests/ai-tool-langchain4j -Pollama
+----
+
+The first run may take longer as the Ollama container image and model are downloaded.
diff --git a/integration-tests/ai-tool-langchain4j/pom.xml b/integration-tests/ai-tool-langchain4j/pom.xml
new file mode 100644
index 0000000000..433e1df189
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/pom.xml
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-build-parent-it</artifactId>
+ <version>3.39.0-SNAPSHOT</version>
+ <relativePath>../../poms/build-parent-it/pom.xml</relativePath>
+ </parent>
+
+ <artifactId>camel-quarkus-integration-test-ai-tool-langchain4j</artifactId>
+ <name>Camel Quarkus :: Integration Tests :: AI Tool LangChain4j</name>
+ <description>Integration tests for Camel AI Tool bridge with Quarkus LangChain4j</description>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>io.quarkiverse.langchain4j</groupId>
+ <artifactId>quarkus-langchain4j-bom</artifactId>
+ <version>${quarkiverse-langchain4j.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-ai-tool</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-bean</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-direct</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-rest</artifactId>
+ </dependency>
+
+ <!-- Camel Quarkus LangChain4j support (provides the AiToolRegistry → ToolProvider bridge) -->
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-support-langchain4j</artifactId>
+ </dependency>
+ <!-- Required at runtime for AiToolSpec → ToolSpecification conversion (optional in support module) -->
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-langchain4j-agent</artifactId>
+ </dependency>
+ <!-- Quarkus LangChain4j -->
+ <dependency>
+ <groupId>io.quarkiverse.langchain4j</groupId>
+ <artifactId>quarkus-langchain4j-ollama</artifactId>
+ </dependency>
+
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.rest-assured</groupId>
+ <artifactId>rest-assured</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <profiles>
+ <profile>
+ <id>native</id>
+ <activation>
+ <property>
+ <name>native</name>
+ </property>
+ </activation>
+ <properties>
+ <quarkus.native.enabled>true</quarkus.native.enabled>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>ollama</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <ollama.test>true</ollama.test>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <ollama.test>true</ollama.test>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>virtualDependencies</id>
+ <activation>
+ <property>
+ <name>!noVirtualDependencies</name>
+ </property>
+ </activation>
+ <dependencies>
+ <!-- The following dependencies guarantee that this module is built after them. You can update them by running `mvn process-resources -Pformat -N` from the source tree root directory -->
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-ai-tool-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-bean-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-direct-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-support-langchain4j-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ </dependencies>
+ </profile>
+ <profile>
+ <id>skip-testcontainers-tests</id>
+ <activation>
+ <property>
+ <name>skip-testcontainers-tests</name>
+ </property>
+ </activation>
+ <properties>
+ <skipTests>true</skipTests>
+ </properties>
+ </profile>
+ </profiles>
+</project>
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java
new file mode 100644
index 0000000000..60fdd44967
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaResource.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiServiceOllama;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiServiceOllamaWrongTag;
+
+@Path("/ai-tool-langchain4j-ollama")
+@ApplicationScoped
+public class AiToolLangchain4jOllamaResource {
+
+ @Inject
+ WeatherAiServiceOllama weatherAiService;
+
+ @Inject
+ WeatherAiServiceOllamaWrongTag weatherAiServiceWrongTag;
+
+ @Path("/chat")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String chat(String message) {
+ return weatherAiService.chat(message);
+ }
+
+ @Path("/chat-wrong-tag")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String chatWrongTag(String message) {
+ return weatherAiServiceWrongTag.chat(message);
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java
new file mode 100644
index 0000000000..82770665ac
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jResource.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.ai.tool.AiToolRegistry;
+import org.apache.camel.component.ai.tool.AiToolSpec;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.AdminAiService;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiService;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service.WeatherAiServiceWrongTag;
+
+@Path("/ai-tool-langchain4j")
+@ApplicationScoped
+public class AiToolLangchain4jResource {
+
+ @Inject
+ CamelContext camelContext;
+
+ @Inject
+ WeatherAiService weatherAiService;
+
+ @Inject
+ AdminAiService adminAiService;
+
+ @Inject
+ WeatherAiServiceWrongTag weatherAiServiceWrongTag;
+
+ @Path("/weather/chat")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String weatherChat(String message) {
+ return weatherAiService.chat(message);
+ }
+
+ @Path("/admin/chat")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String adminChat(String message) {
+ return adminAiService.chat(message);
+ }
+
+ @Path("/weather-wrong-tag/chat")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String weatherChatWrongTag(String message) {
+ return weatherAiServiceWrongTag.chat(message);
+ }
+
+ @Path("/tools/{tag}")
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ public String listToolsByTag(@PathParam("tag") String tag) {
+ AiToolRegistry registry = AiToolRegistry.getOrCreate(camelContext);
+ Set<AiToolSpec> tools = registry.getToolsByTag(tag);
+ return tools.stream()
+ .map(AiToolSpec::getName)
+ .sorted()
+ .collect(Collectors.joining(","));
+ }
+
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java
new file mode 100644
index 0000000000..8f8b45acf6
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jRoutes.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import org.apache.camel.builder.RouteBuilder;
+
+public class AiToolLangchain4jRoutes extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("ai-tool:getWeather?"
+ + "tags=weatherTag"
+ + "&description=Get the current weather for a city"
+ + "¶meter.city=string"
+ + "¶meter.city.required=true"
+ + "¶meter.city.description=The city name")
+ .setBody(simple("Sunny in ${header.city}, 1111 celsius"));
+
+ from("ai-tool:getNews?"
+ + "tags=adminTag"
+ + "&description=Get the latest news about a topic"
+ + "¶meter.topic=string"
+ + "¶meter.topic.required=true"
+ + "¶meter.topic.description=The news topic")
+ .setBody(simple("Latest news about ${header.topic}"));
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java
new file mode 100644
index 0000000000..a4be0986b9
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AbstractToolCallingChatModel.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.response.ChatResponse;
+
+/**
+ * Simulates the two-turn tool-calling protocol used by langchain4j:
+ *
+ * <ol>
+ * <li><b>Turn 1</b> — langchain4j sends the user message and available tool specifications to the model.
+ * The model responds with a {@link ToolExecutionRequest} ("I want to call tool X with args Y").
+ * No tool is executed yet.</li>
+ * <li><b>Tool execution</b> — langchain4j sees the request, invokes the matching {@code ToolExecutor}
+ * (in our case the Camel {@code ai-tool:} route), and captures the result.</li>
+ * <li><b>Turn 2</b> — langchain4j calls the model again, appending a {@link ToolExecutionResultMessage}
+ * with the tool's output. The model returns a final text answer.</li>
+ * </ol>
+ *
+ * Subclasses only define which tool to call and how to format the final response.
+ */
+public abstract class AbstractToolCallingChatModel implements Supplier<ChatModel> {
+
+ // Correlation ID for matching ToolExecutionResultMessage back to the request — required by the
+ // builder but the actual value is irrelevant in tests with a single tool call per turn.
+ protected abstract String toolCallId();
+
+ protected abstract String toolName();
+
+ protected abstract String toolArguments();
+
+ protected abstract String formatResponse(String toolResult);
+
+ protected abstract String getNameOfService();
+
+ @Override
+ public ChatModel get() {
+ return new ChatModel() {
+ @Override
+ public ChatResponse doChat(ChatRequest chatRequest) {
+ List<ChatMessage> messages = chatRequest.messages();
+
+ boolean hasToolResult = messages.stream()
+ .anyMatch(m -> m instanceof ToolExecutionResultMessage);
+
+ if (!hasToolResult) {
+ ToolExecutionRequest toolRequest = ToolExecutionRequest.builder()
+ .id(toolCallId())
+ .name(toolName())
+ .arguments(toolArguments())
+ .build();
+ return ChatResponse.builder()
+ .aiMessage(AiMessage.from(toolRequest))
+ .build();
+ }
+
+ // #2 tool's response already present
+ String toolResult = messages.stream()
+ .filter(m -> m instanceof ToolExecutionResultMessage)
+ .map(m -> ((ToolExecutionResultMessage) m).text())
+ .findFirst()
+ .orElse("unknown");
+
+ return ChatResponse.builder()
+ .aiMessage(new AiMessage(formatResponse(toolResult)))
+ .build();
+ }
+ };
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java
new file mode 100644
index 0000000000..2bff6c1e12
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/AdminToolCallingChatModel.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model;
+
+public class AdminToolCallingChatModel extends AbstractToolCallingChatModel {
+
+ @Override
+ protected String getNameOfService() {
+ return "adminTag";
+ }
+
+ @Override
+ protected String toolCallId() {
+ return "call_admin_1";
+ }
+
+ @Override
+ protected String toolName() {
+ return "getNews";
+ }
+
+ @Override
+ protected String toolArguments() {
+ return "{\"topic\":\"camel\"}";
+ }
+
+ @Override
+ protected String formatResponse(String toolResult) {
+ return "News result: " + toolResult;
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java
new file mode 100644
index 0000000000..4a52902d80
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WeatherToolCallingChatModel.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model;
+
+public class WeatherToolCallingChatModel extends AbstractToolCallingChatModel {
+
+ @Override
+ protected String getNameOfService() {
+ return "weatherTag";
+ }
+
+ @Override
+ protected String toolCallId() {
+ return "call_1";
+ }
+
+ @Override
+ protected String toolName() {
+ return "getWeather";
+ }
+
+ @Override
+ protected String toolArguments() {
+ return "{\"city\":\"Prague\"}";
+ }
+
+ @Override
+ protected String formatResponse(String toolResult) {
+ return toolResult;
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java
new file mode 100644
index 0000000000..a4c1803a46
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/model/WrongTagToolCallingChatModel.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.model.chat.ChatModel;
+import dev.langchain4j.model.chat.request.ChatRequest;
+import dev.langchain4j.model.chat.response.ChatResponse;
+
+/**
+ * Mock model for testing tag isolation. Calls {@code getWeather} only if it appears in the
+ * available tool specifications — when the interceptor works correctly and this service is
+ * tagged with {@code adminTag}, the weather tool should NOT be visible.
+ */
+public class WrongTagToolCallingChatModel implements Supplier<ChatModel> {
+
+ @Override
+ public ChatModel get() {
+ return new ChatModel() {
+ @Override
+ public ChatResponse doChat(ChatRequest chatRequest) {
+ List<ChatMessage> messages = chatRequest.messages();
+ boolean hasToolResult = messages.stream()
+ .anyMatch(m -> m instanceof ToolExecutionResultMessage);
+
+ if (!hasToolResult) {
+ boolean weatherToolAvailable = chatRequest.toolSpecifications() != null
+ && chatRequest.toolSpecifications().stream()
+ .anyMatch(ts -> "getWeather".equals(ts.name()));
+
+ if (weatherToolAvailable) {
+ ToolExecutionRequest request = ToolExecutionRequest.builder()
+ .id("call_wrong_tag")
+ .name("getWeather")
+ .arguments("{\"city\":\"Prague\"}")
+ .build();
+ return ChatResponse.builder()
+ .aiMessage(AiMessage.from(request))
+ .build();
+ }
+
+ return ChatResponse.builder()
+ .aiMessage(new AiMessage("No weather tool available"))
+ .build();
+ }
+
+ String toolResult = messages.stream()
+ .filter(m -> m instanceof ToolExecutionResultMessage)
+ .map(m -> ((ToolExecutionResultMessage) m).text())
+ .findFirst()
+ .orElse("unknown");
+ return ChatResponse.builder()
+ .aiMessage(new AiMessage(toolResult))
+ .build();
+ }
+ };
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java
new file mode 100644
index 0000000000..b54e0d4d12
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/AdminAiService.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service;
+
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model.AdminToolCallingChatModel;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools;
+
+@ApplicationScoped
+@RegisterAiService(chatLanguageModelSupplier = AdminToolCallingChatModel.class)
+@CamelAiTools("adminTag")
+public interface AdminAiService {
+
+ String chat(@UserMessage String message);
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java
new file mode 100644
index 0000000000..9a1e8501a1
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiService.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service;
+
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model.WeatherToolCallingChatModel;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools;
+
+@ApplicationScoped
+@RegisterAiService(chatLanguageModelSupplier = WeatherToolCallingChatModel.class)
+@CamelAiTools("weatherTag")
+public interface WeatherAiService {
+
+ String chat(@UserMessage String message);
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java
new file mode 100644
index 0000000000..76829813cb
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllama.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service;
+
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools;
+
+@ApplicationScoped
+@RegisterAiService
+@CamelAiTools("weatherTag")
+public interface WeatherAiServiceOllama {
+
+ String chat(@UserMessage String message);
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java
new file mode 100644
index 0000000000..1545589a33
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceOllamaWrongTag.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service;
+
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools;
+
+@ApplicationScoped
+@RegisterAiService
+@CamelAiTools("adminTag")
+public interface WeatherAiServiceOllamaWrongTag {
+
+ String chat(@UserMessage String message);
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java
new file mode 100644
index 0000000000..7e4a78d7b6
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/service/WeatherAiServiceWrongTag.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it.service;
+
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.camel.quarkus.component.ai.tool.langchain4j.it.model.WrongTagToolCallingChatModel;
+import org.apache.camel.quarkus.component.support.langchain4j.CamelAiTools;
+
+@ApplicationScoped
+@RegisterAiService(chatLanguageModelSupplier = WrongTagToolCallingChatModel.class)
+@CamelAiTools("adminTag")
+public interface WeatherAiServiceWrongTag {
+
+ String chat(@UserMessage String message);
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/main/resources/application.properties b/integration-tests/ai-tool-langchain4j/src/main/resources/application.properties
new file mode 100644
index 0000000000..a58bbb874a
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/main/resources/application.properties
@@ -0,0 +1,18 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements. See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License. You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+quarkus.devservices.enabled=false
+quarkus.langchain4j.ollama.chat-model.model-id=qwen3:1.7b
diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java
new file mode 100644
index 0000000000..40dd473753
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jIT.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+class AiToolLangchain4jIT extends AiToolLangchain4jTest {
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java
new file mode 100644
index 0000000000..b263b5abc2
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaIT.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+@QuarkusIntegrationTest
+@EnabledIfSystemProperty(named = "ollama.test", matches = "true")
+class AiToolLangchain4jOllamaIT extends AiToolLangchain4jOllamaTest {
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java
new file mode 100644
index 0000000000..d4b5fef08d
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jOllamaTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import java.util.Map;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.TestProfile;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+@QuarkusTest
+@TestProfile(AiToolLangchain4jOllamaTest.OllamaProfile.class)
+@EnabledIfSystemProperty(named = "ollama.test", matches = "true")
+class AiToolLangchain4jOllamaTest {
+
+ @Test
+ void weatherFromToolTest() {
+ RestAssured.given()
+ .contentType("text/plain")
+ .body("What is the weather in Prague?")
+ .post("/ai-tool-langchain4j-ollama/chat")
+ .then()
+ .statusCode(200)
+ .body(containsString("1111"));
+ }
+
+ // Verifies that the @CamelAiTools interceptor filters tools by tag: a service tagged "adminTag"
+ // should not see the getWeather tool (tagged "weatherTag"), so the real LLM won't call it
+ // and "1111" won't appear in the response. Fails if the interceptor is broken and all tools are visible.
+ @Test
+ void weatherWithWrongTag() {
+ RestAssured.given()
+ .contentType("text/plain")
+ .body("What is the weather in Prague?")
+ .post("/ai-tool-langchain4j-ollama/chat-wrong-tag")
+ .then()
+ .statusCode(200)
+ .body(not(containsString("1111")));
+ }
+
+ public static class OllamaProfile implements QuarkusTestProfile {
+ @Override
+ public Map<String, String> getConfigOverrides() {
+ return Map.of(
+ "quarkus.devservices.enabled", "true");
+ }
+ }
+}
diff --git a/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java
new file mode 100644
index 0000000000..2b7cf401a6
--- /dev/null
+++ b/integration-tests/ai-tool-langchain4j/src/test/java/org/apache/camel/quarkus/component/ai/tool/langchain4j/it/AiToolLangchain4jTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.ai.tool.langchain4j.it;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+@QuarkusTest
+class AiToolLangchain4jTest {
+
+ @Test
+ void weatherServiceTest() {
+ RestAssured.given()
+ .contentType("text/plain")
+ .body("What is the weather in Prague?")
+ .post("/ai-tool-langchain4j/weather/chat")
+ .then()
+ .statusCode(200)
+ .body(containsString("1111"));
+ }
+
+ @Test
+ void weatherServiceTestWithWrongTag() {
+ RestAssured.given()
+ .contentType("text/plain")
+ .body("What is the weather in Prague?")
+ .post("/ai-tool-langchain4j/admin/chat")
+ .then()
+ .statusCode(200)
+ .body(containsString("Latest news about camel"));
+ }
+
+ @Test
+ void aiToolRegistryToolsByWeatherTagTest() {
+ RestAssured.given()
+ .get("/ai-tool-langchain4j/tools/weatherTag")
+ .then()
+ .statusCode(200)
+ .body(containsString("getWeather"))
+ .body(not(containsString("getNews")));
+ }
+
+ @Test
+ void aiToolRegistryToolsByAdminTagTest() {
+ RestAssured.given()
+ .get("/ai-tool-langchain4j/tools/adminTag")
+ .then()
+ .statusCode(200)
+ .body(containsString("getNews"))
+ .body(not(containsString("getWeather")));
+ }
+
+ @Test
+ void adminServiceTest() {
+ RestAssured.given()
+ .contentType("text/plain")
+ .body("What are the latest news about camel?")
+ .post("/ai-tool-langchain4j/admin/chat")
+ .then()
+ .statusCode(200)
+ .body(containsString("camel"))
+ .body(containsString("news"));
+ }
+
+ // Verifies that the @CamelAiTools interceptor filters tools by tag: a service tagged "adminTag"
+ // should not see the getWeather tool (tagged "weatherTag"), so the mock model won't call it
+ // and "1111" won't appear in the response. Fails if the interceptor is broken and all tools are visible.
+ @Test
+ void weatherWithWrongTagTest() {
+ RestAssured.given()
+ .contentType("text/plain")
+ .body("What is the weather in Prague?")
+ .post("/ai-tool-langchain4j/weather-wrong-tag/chat")
+ .then()
+ .statusCode(200)
+ .body(not(containsString("1111")));
+ }
+
+}
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index b29781f585..ce58500c83 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -51,6 +51,8 @@
<module>a2a</module>
<module>activemq</module>
<module>activemq6</module>
+ <module>ai-tool</module>
+ <module>ai-tool-langchain4j</module>
<module>amqp</module>
<module>arangodb</module>
<module>as2</module>
diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml
index 6eecf4224a..97b679b994 100644
--- a/tooling/scripts/test-categories.yaml
+++ b/tooling/scripts/test-categories.yaml
@@ -231,6 +231,8 @@ group-11:
- xml-grouped
- xslt-saxon
group-12:
+ - ai-tool
+ - ai-tool-langchain4j
- aws2-grouped
- csimple
- github2