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()); + } }
