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 0cccc29792b8f..bc837d6c2c42a 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 @@ protected void configureBuilder(AiServices builder, ToolProvider toolProvider 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 65ea85617afb2..c813d7454ae53 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.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 customTools; // Custom LangChain4j tools private List mcpClients; // MCP clients for external tool integration private BiPredicate mcpToolProviderFilter; // Filter for MCP tools + private int maxToolCallingRoundTrips; // Max number of tool-calling round trips (0 = not set) + private Function hallucinatedToolNameStrategy; + private ToolExecutionErrorHandler toolExecutionErrorHandler; + private ToolArgumentsErrorHandler toolArgumentsErrorHandler; + private Boolean compensateOnToolErrors; + private Consumer> aiServicesCustomizer; /** * Gets the configured chat model. @@ -346,6 +359,132 @@ public AgentConfiguration withMcpToolProviderFilter(BiPredicate 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 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> 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> 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 12a682d07ad03..297ac1ccc71ea 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,19 @@ import java.io.Serializable; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +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 +257,97 @@ public void testWithOutputGuardrailClassesArray_WithInvalidClasses() { 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()); + + Function 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()); + } }