This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch 
feature/CAMEL-23928-agent-aiservices-customizer
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 52993646c4573da838a908ad89edc3aef4332a25
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jul 7 09:29:07 2026 +0200

    CAMEL-23928: camel-langchain4j-agent - Add AiServices builder customizer 
and tool-calling options to AgentConfiguration
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../langchain4j/agent/api/AbstractAgent.java       |  22 ++++
 .../langchain4j/agent/api/AgentConfiguration.java  | 139 +++++++++++++++++++++
 .../agent/api/AgentConfigurationTest.java          | 100 +++++++++++++++
 3 files changed, 261 insertions(+)

diff --git 
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
 
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
index 0cccc29792b8..bc837d6c2c42 100644
--- 
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
+++ 
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
@@ -150,5 +150,27 @@ public abstract class AbstractAgent<S> implements Agent {
         if (responseFormat != null) {
             builder.chatRequestTransformer(chatRequest -> 
chatRequest.toBuilder().responseFormat(responseFormat).build());
         }
+
+        // Tool calling options
+        if (configuration.getMaxToolCallingRoundTrips() > 0) {
+            
builder.maxToolCallingRoundTrips(configuration.getMaxToolCallingRoundTrips());
+        }
+        if (configuration.getHallucinatedToolNameStrategy() != null) {
+            
builder.hallucinatedToolNameStrategy(configuration.getHallucinatedToolNameStrategy());
+        }
+        if (configuration.getToolExecutionErrorHandler() != null) {
+            
builder.toolExecutionErrorHandler(configuration.getToolExecutionErrorHandler());
+        }
+        if (configuration.getToolArgumentsErrorHandler() != null) {
+            
builder.toolArgumentsErrorHandler(configuration.getToolArgumentsErrorHandler());
+        }
+        if (configuration.getCompensateOnToolErrors() != null) {
+            
builder.compensateOnToolErrors(configuration.getCompensateOnToolErrors());
+        }
+
+        // Custom AiServices builder customizer (escape hatch for any builder 
option not directly exposed)
+        if (configuration.getAiServicesCustomizer() != null) {
+            configuration.getAiServicesCustomizer().accept(builder);
+        }
     }
 }
diff --git 
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java
 
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java
index 65ea85617afb..c813d7454ae5 100644
--- 
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java
+++ 
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java
@@ -21,13 +21,20 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
 import dev.langchain4j.agent.tool.ToolSpecification;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
 import dev.langchain4j.mcp.client.McpClient;
 import dev.langchain4j.memory.chat.ChatMemoryProvider;
 import dev.langchain4j.model.chat.ChatModel;
 import dev.langchain4j.rag.RetrievalAugmentor;
+import dev.langchain4j.service.AiServices;
+import dev.langchain4j.service.tool.ToolArgumentsErrorHandler;
+import dev.langchain4j.service.tool.ToolExecutionErrorHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -67,6 +74,12 @@ public class AgentConfiguration {
     private List<Object> customTools; // Custom LangChain4j tools
     private List<McpClient> mcpClients; // MCP clients for external tool 
integration
     private BiPredicate<McpClient, ToolSpecification> mcpToolProviderFilter; 
// Filter for MCP tools
+    private int maxToolCallingRoundTrips; // Max number of tool-calling round 
trips (0 = not set)
+    private Function<ToolExecutionRequest, ToolExecutionResultMessage> 
hallucinatedToolNameStrategy;
+    private ToolExecutionErrorHandler toolExecutionErrorHandler;
+    private ToolArgumentsErrorHandler toolArgumentsErrorHandler;
+    private Boolean compensateOnToolErrors;
+    private Consumer<AiServices<?>> aiServicesCustomizer;
 
     /**
      * Gets the configured chat model.
@@ -346,6 +359,132 @@ public class AgentConfiguration {
         return this;
     }
 
+    /**
+     * Gets the maximum number of tool-calling round trips allowed.
+     *
+     * @return the maximum round trips, or 0 if not configured
+     */
+    public int getMaxToolCallingRoundTrips() {
+        return maxToolCallingRoundTrips;
+    }
+
+    /**
+     * Sets the maximum number of tool-calling round trips the LLM can make 
per request. This limits how many times the
+     * model can invoke tools before a final response must be produced.
+     *
+     * @param  maxToolCallingRoundTrips the maximum number of round trips 
(must be positive)
+     * @return                          this configuration instance for method 
chaining
+     */
+    public AgentConfiguration withMaxToolCallingRoundTrips(int 
maxToolCallingRoundTrips) {
+        this.maxToolCallingRoundTrips = maxToolCallingRoundTrips;
+        return this;
+    }
+
+    /**
+     * Gets the strategy for handling hallucinated (non-existent) tool names.
+     *
+     * @return the hallucinated tool name strategy, or {@code null} if not 
configured
+     */
+    public Function<ToolExecutionRequest, ToolExecutionResultMessage> 
getHallucinatedToolNameStrategy() {
+        return hallucinatedToolNameStrategy;
+    }
+
+    /**
+     * Sets the strategy for handling cases where the LLM hallucinates a tool 
name that does not exist. The function
+     * receives the invalid tool execution request and returns a result 
message to send back to the LLM.
+     *
+     * @param  hallucinatedToolNameStrategy the strategy function for handling 
hallucinated tool names
+     * @return                              this configuration instance for 
method chaining
+     */
+    public AgentConfiguration withHallucinatedToolNameStrategy(
+            Function<ToolExecutionRequest, ToolExecutionResultMessage> 
hallucinatedToolNameStrategy) {
+        this.hallucinatedToolNameStrategy = hallucinatedToolNameStrategy;
+        return this;
+    }
+
+    /**
+     * Gets the error handler for tool execution failures.
+     *
+     * @return the tool execution error handler, or {@code null} if not 
configured
+     */
+    public ToolExecutionErrorHandler getToolExecutionErrorHandler() {
+        return toolExecutionErrorHandler;
+    }
+
+    /**
+     * Sets the error handler invoked when a tool execution throws an 
exception.
+     *
+     * @param  toolExecutionErrorHandler the handler for tool execution errors
+     * @return                           this configuration instance for 
method chaining
+     */
+    public AgentConfiguration 
withToolExecutionErrorHandler(ToolExecutionErrorHandler 
toolExecutionErrorHandler) {
+        this.toolExecutionErrorHandler = toolExecutionErrorHandler;
+        return this;
+    }
+
+    /**
+     * Gets the error handler for tool argument parsing failures.
+     *
+     * @return the tool arguments error handler, or {@code null} if not 
configured
+     */
+    public ToolArgumentsErrorHandler getToolArgumentsErrorHandler() {
+        return toolArgumentsErrorHandler;
+    }
+
+    /**
+     * Sets the error handler invoked when tool arguments cannot be parsed or 
validated.
+     *
+     * @param  toolArgumentsErrorHandler the handler for tool argument errors
+     * @return                           this configuration instance for 
method chaining
+     */
+    public AgentConfiguration 
withToolArgumentsErrorHandler(ToolArgumentsErrorHandler 
toolArgumentsErrorHandler) {
+        this.toolArgumentsErrorHandler = toolArgumentsErrorHandler;
+        return this;
+    }
+
+    /**
+     * Gets whether tool error compensation is enabled.
+     *
+     * @return {@code true} if enabled, {@code false} if disabled, or {@code 
null} if not configured
+     */
+    public Boolean getCompensateOnToolErrors() {
+        return compensateOnToolErrors;
+    }
+
+    /**
+     * Sets whether the agent should attempt to compensate when tool execution 
errors occur by sending the error back to
+     * the LLM for recovery.
+     *
+     * @param  compensateOnToolErrors {@code true} to enable compensation, 
{@code false} to disable
+     * @return                        this configuration instance for method 
chaining
+     */
+    public AgentConfiguration withCompensateOnToolErrors(Boolean 
compensateOnToolErrors) {
+        this.compensateOnToolErrors = compensateOnToolErrors;
+        return this;
+    }
+
+    /**
+     * Gets the custom AiServices builder customizer.
+     *
+     * @return the customizer, or {@code null} if not configured
+     */
+    public Consumer<AiServices<?>> getAiServicesCustomizer() {
+        return aiServicesCustomizer;
+    }
+
+    /**
+     * Sets a customizer callback that is invoked on the LangChain4j {@link 
AiServices} builder after all standard
+     * configuration has been applied but before {@code build()} is called. 
This provides an escape hatch for
+     * configuring any AiServices builder option that is not directly exposed 
on this configuration class.
+     *
+     * @param  aiServicesCustomizer the customizer to apply to the AiServices 
builder
+     * @return                      this configuration instance for method 
chaining
+     */
+    public AgentConfiguration withAiServicesCustomizer(Consumer<AiServices<?>> 
aiServicesCustomizer) {
+        this.aiServicesCustomizer = aiServicesCustomizer;
+        return this;
+    }
+
     /**
      * Loads a guardrail class by its fully qualified name using reflection.
      *
diff --git 
a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java
 
b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java
index 12a682d07ad0..6825b3720f8c 100644
--- 
a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java
+++ 
b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java
@@ -18,11 +18,18 @@ package org.apache.camel.component.langchain4j.agent.api;
 
 import java.io.Serializable;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
+import dev.langchain4j.agent.tool.ToolExecutionRequest;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.service.tool.ToolArgumentsErrorHandler;
+import dev.langchain4j.service.tool.ToolExecutionErrorHandler;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class AgentConfigurationTest {
@@ -249,4 +256,97 @@ public class AgentConfigurationTest {
         assertNotNull(config.getOutputGuardrailClasses());
         assertTrue(config.getOutputGuardrailClasses().isEmpty());
     }
+
+    // Tests for tool-calling options
+
+    @Test
+    public void testMaxToolCallingRoundTrips() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertEquals(0, config.getMaxToolCallingRoundTrips());
+
+        AgentConfiguration result = config.withMaxToolCallingRoundTrips(10);
+
+        assertSame(config, result);
+        assertEquals(10, config.getMaxToolCallingRoundTrips());
+    }
+
+    @Test
+    public void testHallucinatedToolNameStrategy() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getHallucinatedToolNameStrategy());
+
+        java.util.function.Function<ToolExecutionRequest, 
ToolExecutionResultMessage> strategy
+                = request -> ToolExecutionResultMessage.from(request, "Unknown 
tool: " + request.name());
+        AgentConfiguration result = 
config.withHallucinatedToolNameStrategy(strategy);
+
+        assertSame(config, result);
+        assertSame(strategy, config.getHallucinatedToolNameStrategy());
+    }
+
+    @Test
+    public void testToolExecutionErrorHandler() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getToolExecutionErrorHandler());
+
+        ToolExecutionErrorHandler handler = (error, context) -> null;
+        AgentConfiguration result = 
config.withToolExecutionErrorHandler(handler);
+
+        assertSame(config, result);
+        assertSame(handler, config.getToolExecutionErrorHandler());
+    }
+
+    @Test
+    public void testToolArgumentsErrorHandler() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getToolArgumentsErrorHandler());
+
+        ToolArgumentsErrorHandler handler = (error, context) -> null;
+        AgentConfiguration result = 
config.withToolArgumentsErrorHandler(handler);
+
+        assertSame(config, result);
+        assertSame(handler, config.getToolArgumentsErrorHandler());
+    }
+
+    @Test
+    public void testCompensateOnToolErrors() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getCompensateOnToolErrors());
+
+        AgentConfiguration result = config.withCompensateOnToolErrors(true);
+
+        assertSame(config, result);
+        assertTrue(config.getCompensateOnToolErrors());
+    }
+
+    @Test
+    public void testAiServicesCustomizer() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getAiServicesCustomizer());
+
+        AtomicBoolean invoked = new AtomicBoolean(false);
+        AgentConfiguration result = config.withAiServicesCustomizer(builder -> 
invoked.set(true));
+
+        assertSame(config, result);
+        assertNotNull(config.getAiServicesCustomizer());
+    }
+
+    @Test
+    public void testFluentChaining() {
+        ToolExecutionErrorHandler execHandler = (error, context) -> null;
+        ToolArgumentsErrorHandler argsHandler = (error, context) -> null;
+
+        AgentConfiguration config = new AgentConfiguration()
+                .withMaxToolCallingRoundTrips(5)
+                .withToolExecutionErrorHandler(execHandler)
+                .withToolArgumentsErrorHandler(argsHandler)
+                .withCompensateOnToolErrors(true)
+                .withAiServicesCustomizer(builder -> {
+                });
+
+        assertEquals(5, config.getMaxToolCallingRoundTrips());
+        assertSame(execHandler, config.getToolExecutionErrorHandler());
+        assertSame(argsHandler, config.getToolArgumentsErrorHandler());
+        assertTrue(config.getCompensateOnToolErrors());
+        assertNotNull(config.getAiServicesCustomizer());
+    }
 }

Reply via email to