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"
         + "&parameter.name=string&parameter.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);
     }

Reply via email to