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
The following commit(s) were added to refs/heads/main by this push:
new 3bd2c49c41 Fixes #8993. Scope the ai-tool bridge away from Camel agents
3bd2c49c41 is described below
commit 3bd2c49c41ec0676542bcd96398b95c241673455
Author: Jiří Ondrušek <[email protected]>
AuthorDate: Wed Aug 12 17:53:29 2026 +0200
Fixes #8993. Scope the ai-tool bridge away from Camel agents
Fixes #8992.
Also repairs the langchain4j-agent-ql4j stubs after #8985's ai-tool
migration (the module reuses the agent module's sources via copy-tests
while keeping its own WireMock recordings).
CamelAiToolProvider now ignores requests originating from Camel
langchain4j-agent endpoints (recognized by the agent-api service
interfaces). Camel agents select registry tools deliberately through
their endpoint's tags parameter; serving them from the auto-discovered
CDI provider as well attached every registered ai-tool to every agent -
breaking agents backed by models without tool support, duplicating the
tags-selected tools in each request (quarkus-langchain4j appends provider
specs without name dedup), and letting tools leak into unrelated
conversations (the memory test answered from the userDb tool instead of
chat memory). @RegisterAiService services and user-built AiServices keep
the all-tools default; the ai-tool usage doc now states the rule.
Stub updates (3 recordings): the tool rename from the ai-tool migration
(QueryUserDatabaseByUserID -> userDb, mirroring #8985) and the userDb
tool in the custom AiService recording - the one consumer where the
all-tools default applies by design.
The custom AiService agent's model is now configurable
(agent.tests.custom-service-model-id, default orca-mini): in the ql4j
flavor it receives the registered tools, and orca-mini rejects requests
carrying them, so the module overrides it with granite4:3b - keeping the
module runnable and re-recordable against a real Ollama.
Verified: langchain4j-agent-ql4j 16/16, langchain4j-agent 15/15,
ai-tool-langchain4j 8/8 (tag filtering unaffected), rag-bridge-ql4j
11/11; a real-Ollama run of the ql4j module goes from 3/16 to 16/16.
Co-authored-by: Claude Fable 5 <[email protected]>
---
.../ROOT/pages/reference/extensions/ai-tool.adoc | 10 +++++++++
.../support/langchain4j/CamelAiToolProvider.java | 25 ++++++++++++++++++++++
extensions/ai-tool/runtime/src/main/doc/usage.adoc | 9 ++++++++
.../src/main/resources/application.properties | 4 ++++
..._chat-a339a757-7f86-466a-a67d-3f519751fdd1.json | 2 +-
..._chat-df766850-0561-4506-bd7c-55eacf12dbf4.json | 2 +-
..._chat-e45a0476-1655-4602-b20d-30332bd754bf.json | 4 ++--
.../langchain4j/agent/it/AgentProducers.java | 15 ++++++++++---
8 files changed, 64 insertions(+), 7 deletions(-)
diff --git a/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc
b/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc
index 16920abb9a..fc3d32ee34 100644
--- a/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/ai-tool.adoc
@@ -92,3 +92,13 @@ from("ai-tool:greet?description=Greet a user"
.setBody(simple("Hello, ${header.name}!"));
----
+[id="extensions-ai-tool-usage-who-receives-registered-tools"]
+=== Who receives registered tools
+
+Camel agents (`langchain4j-agent` endpoints) receive registry tools only
through their
+endpoint's `tags` parameter — an agent without `tags` receives no registry
tools (tools from
+the agent's other sources, such as MCP clients or custom tool objects, are
unaffected).
+Quarkus LangChain4j AI services (`@RegisterAiService` interfaces and
programmatically built
+`AiServices`) receive every registered tool by default; restrict a service to
a subset with
+the `@CamelAiTools` annotation.
+
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
index dbe66ddf67..2c8c093f69 100644
---
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
@@ -50,6 +50,9 @@ public class CamelAiToolProvider implements ToolProvider {
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<>();
+ private static final Set<String> CAMEL_AGENT_SERVICE_INTERFACES = Set.of(
+
"org.apache.camel.component.langchain4j.agent.api.AiAgentWithMemoryService",
+
"org.apache.camel.component.langchain4j.agent.api.AiAgentWithoutMemoryService");
@Inject
CamelContext camelContext;
@@ -74,6 +77,14 @@ public class CamelAiToolProvider implements ToolProvider {
@Override
public ToolProviderResult provideTools(ToolProviderRequest request) {
+ if (isCamelAgentRequest(request)) {
+ // Camel agents select registry tools through their endpoint's
tags parameter — the
+ // langchain4j-agent producer builds its own provider from the
AiToolRegistry. Serving
+ // them here too would attach every registered tool to every agent
(breaking agents
+ // backed by models without tool support) and duplicate the
tags-selected tools.
+ return ToolProviderResult.builder().build();
+ }
+
AiToolRegistry registry = AiToolRegistry.getOrCreate(camelContext);
String effectiveTag = CURRENT_TAG.get();
Set<AiToolSpec> tools = effectiveTag != null ?
registry.getToolsByTag(effectiveTag) : registry.getAllTools();
@@ -88,6 +99,20 @@ public class CamelAiToolProvider implements ToolProvider {
return resultBuilder.build();
}
+ /**
+ * Whether the request originates from a Camel {@code langchain4j-agent}
endpoint: the agent
+ * component builds its AI services from exactly these {@code
camel-langchain4j-agent-api}
+ * interfaces. Name constants rather than class literals on purpose — the
agent artifact is
+ * an optional dependency, absent in applications that bridge tools to AI
services only.
+ * When a Camel release adds a new agent service interface, it must be
added here.
+ */
+ static boolean isCamelAgentRequest(ToolProviderRequest request) {
+ if (request.invocationContext() == null) {
+ return false;
+ }
+ return
CAMEL_AGENT_SERVICE_INTERFACES.contains(request.invocationContext().interfaceName());
+ }
+
private ToolExecutor createExecutor(AiToolSpec spec) {
return (ToolExecutionRequest request, Object memoryId) -> {
Map<String, Object> arguments = parseArguments(request);
diff --git a/extensions/ai-tool/runtime/src/main/doc/usage.adoc
b/extensions/ai-tool/runtime/src/main/doc/usage.adoc
index 677f114f7c..555525dda4 100644
--- a/extensions/ai-tool/runtime/src/main/doc/usage.adoc
+++ b/extensions/ai-tool/runtime/src/main/doc/usage.adoc
@@ -41,3 +41,12 @@ from("ai-tool:greet?description=Greet a user"
+ "¶meter.name=string¶meter.name.required=true")
.setBody(simple("Hello, ${header.name}!"));
----
+
+=== Who receives registered tools
+
+Camel agents (`langchain4j-agent` endpoints) receive registry tools only
through their
+endpoint's `tags` parameter — an agent without `tags` receives no registry
tools (tools from
+the agent's other sources, such as MCP clients or custom tool objects, are
unaffected).
+Quarkus LangChain4j AI services (`@RegisterAiService` interfaces and
programmatically built
+`AiServices`) receive every registered tool by default; restrict a service to
a subset with
+the `@CamelAiTools` annotation.
diff --git
a/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
b/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
index a58de4291a..057f03e68a 100644
---
a/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
+++
b/integration-tests/langchain4j-agent-ql4j/src/main/resources/application.properties
@@ -19,3 +19,7 @@ quarkus.http.test-timeout=120S
quarkus.devservices.enabled=false
quarkus.langchain4j.devservices.preload=false
+
+# The custom AiService receives registered ai-tools (all-tools default for
user-built
+# services), so it needs a tool-capable model; orca-mini rejects requests
carrying tools
+agent.tests.custom-service-model-id=granite4:3b
diff --git
a/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-a339a757-7f86-466a-a67d-3f519751fdd1.json
b/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-a339a757-7f86-466a-a67d-3f519751fdd1.json
index 7731dad51f..9994585204 100644
---
a/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-a339a757-7f86-466a-a67d-3f519751fdd1.json
+++
b/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-a339a757-7f86-466a-a67d-3f519751fdd1.json
@@ -5,7 +5,7 @@
"url" : "/api/chat",
"method" : "POST",
"bodyPatterns" : [ {
- "equalToJson" : "{\n \"model\" : \"granite4:3b\",\n \"messages\" : [
{\n \"role\" : \"user\",\n \"content\" : \"What is the name of user ID
123? Do NOT respond with any markdown formatting. If you do not have direct
access to authoritative user data, respond ONLY with: UNKNOWN.\"\n }, {\n
\"role\" : \"assistant\",\n \"tool_calls\" : [ {\n \"id\" :
\"call_8es60ip7\",\n \"function\" : {\n \"name\" :
\"QueryUserDatabaseByUserID\",\n \"arguments\" [...]
+ "equalToJson" : "{\n \"model\" : \"granite4:3b\",\n \"messages\" : [
{\n \"role\" : \"user\",\n \"content\" : \"What is the name of user ID
123? Do NOT respond with any markdown formatting. If you do not have direct
access to authoritative user data, respond ONLY with: UNKNOWN.\"\n }, {\n
\"role\" : \"assistant\",\n \"tool_calls\" : [ {\n \"id\" :
\"call_8es60ip7\",\n \"function\" : {\n \"name\" : \"userDb\",\n
\"arguments\" : {\n \"u [...]
"ignoreArrayOrder" : true,
"ignoreExtraElements" : false
} ]
diff --git
a/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-df766850-0561-4506-bd7c-55eacf12dbf4.json
b/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-df766850-0561-4506-bd7c-55eacf12dbf4.json
index 358e28c86c..786a90fe37 100644
---
a/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-df766850-0561-4506-bd7c-55eacf12dbf4.json
+++
b/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-df766850-0561-4506-bd7c-55eacf12dbf4.json
@@ -5,7 +5,7 @@
"url" : "/api/chat",
"method" : "POST",
"bodyPatterns" : [ {
- "equalToJson" : "{\n \"model\" : \"orca-mini\",\n \"messages\" : [ {\n
\"role\" : \"user\",\n \"content\" : \"Return an example JSON object
about a person named John Doe with the fields name and description\\nYou must
answer strictly in the following JSON format: {\\n\\\"name\\\": (type:
string),\\n\\\"description\\\": (type: string)\\n}\"\n } ],\n \"options\" :
{\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" :
false,\n \"tools\" : [ ]\n}",
+ "equalToJson" : "{\n \"model\" : \"granite4:3b\",\n \"messages\" : [
{\n \"role\" : \"user\",\n \"content\" : \"Return an example JSON object
about a person named John Doe with the fields name and description\\nYou must
answer strictly in the following JSON format: {\\n\\\"name\\\": (type:
string),\\n\\\"description\\\": (type: string)\\n}\"\n } ],\n \"options\" :
{\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n \"stream\" :
false,\n \"tools\" : [ {\n \"type [...]
"ignoreArrayOrder" : true,
"ignoreExtraElements" : false
} ]
diff --git
a/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-e45a0476-1655-4602-b20d-30332bd754bf.json
b/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-e45a0476-1655-4602-b20d-30332bd754bf.json
index 3f24ce9b46..c8fc8d15d2 100644
---
a/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-e45a0476-1655-4602-b20d-30332bd754bf.json
+++
b/integration-tests/langchain4j-agent-ql4j/src/test/resources/mappings/agent/api_chat-e45a0476-1655-4602-b20d-30332bd754bf.json
@@ -5,14 +5,14 @@
"url" : "/api/chat",
"method" : "POST",
"bodyPatterns" : [ {
- "equalToJson" : "{\n \"model\" : \"granite4:3b\",\n \"messages\" : [
{\n \"role\" : \"user\",\n \"content\" : \"What is the name of user ID
123? Do NOT respond with any markdown formatting. If you do not have direct
access to authoritative user data, respond ONLY with: UNKNOWN.\"\n } ],\n
\"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n
\"stream\" : false,\n \"tools\" : [ {\n \"type\" : \"function\",\n
\"function\" : {\n \"name\" : \"Qu [...]
+ "equalToJson" : "{\n \"model\" : \"granite4:3b\",\n \"messages\" : [
{\n \"role\" : \"user\",\n \"content\" : \"What is the name of user ID
123? Do NOT respond with any markdown formatting. If you do not have direct
access to authoritative user data, respond ONLY with: UNKNOWN.\"\n } ],\n
\"options\" : {\n \"temperature\" : 0.3,\n \"stop\" : [ ]\n },\n
\"stream\" : false,\n \"tools\" : [ {\n \"type\" : \"function\",\n
\"function\" : {\n \"name\" : \"us [...]
"ignoreArrayOrder" : true,
"ignoreExtraElements" : false
} ]
},
"response" : {
"status" : 200,
- "body" :
"{\"model\":\"granite4:3b\",\"created_at\":\"2026-04-23T07:06:12.628589242Z\",\"message\":{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[{\"id\":\"call_8es60ip7\",\"function\":{\"index\":0,\"name\":\"QueryUserDatabaseByUserID\",\"arguments\":{\"userId\":123}}}]},\"done\":true,\"done_reason\":\"stop\",\"total_duration\":4963190121,\"load_duration\":1455484935,\"prompt_eval_count\":200,\"prompt_eval_duration\":2349253791,\"eval_count\":23,\"eval_duration\":1151365523}",
+ "body" :
"{\"model\":\"granite4:3b\",\"created_at\":\"2026-04-23T07:06:12.628589242Z\",\"message\":{\"role\":\"assistant\",\"content\":\"\",\"tool_calls\":[{\"id\":\"call_8es60ip7\",\"function\":{\"index\":0,\"name\":\"userDb\",\"arguments\":{\"userId\":123}}}]},\"done\":true,\"done_reason\":\"stop\",\"total_duration\":4963190121,\"load_duration\":1455484935,\"prompt_eval_count\":200,\"prompt_eval_duration\":2349253791,\"eval_count\":23,\"eval_duration\":1151365523}",
"headers" : {
"Date" : "Thu, 23 Apr 2026 07:06:12 GMT",
"Content-Type" : "application/json; charset=utf-8"
diff --git
a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java
b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java
index f38e653efc..b0b54d04e2 100644
---
a/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java
+++
b/integration-tests/langchain4j-agent/src/main/java/org/apache/camel/quarkus/component/langchain4j/agent/it/AgentProducers.java
@@ -208,11 +208,20 @@ public class AgentProducers {
return new AgentWithoutMemory(new
AgentConfiguration().withChatModel(chatModel));
}
+ // Configurable: in the langchain4j-agent-ql4j flavor this custom
AiService receives the
+ // registered ai-tools (the all-tools default for user-built services), so
there it must be
+ // backed by a tool-capable model — orca-mini rejects requests carrying
tools.
+ @ConfigProperty(name = "agent.tests.custom-service-model-id", defaultValue
= "orca-mini")
+ String customServiceModelId;
+
@Produces
@Identifier("agentWithCustomService")
- public Agent agentCustom(
- @Identifier("ollamaOrcaMiniModel") ChatModel chatModel,
- ObjectMapper objectMapper) {
+ public Agent agentCustom(ObjectMapper objectMapper) {
+ ChatModel chatModel = OllamaChatModel.builder()
+ .baseUrl(baseUrl)
+ .modelName(customServiceModelId)
+ .temperature(0.3)
+ .build();
return new TestPojoAiAgent(new AgentConfiguration()
.withChatModel(chatModel), objectMapper);
}