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

gnodet pushed a commit to branch fix/CAMEL-23950-complete-agent-config
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 768d8c7419bcb0730dac551ad09eb92e45800ac9
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Jul 31 11:32:32 2026 +0200

    CAMEL-23950: complete AgentConfiguration coverage of AiServices builder
    
    Add the following configuration options to AgentConfiguration, wired in
    AbstractAgent.configureBuilder() with null guards:
    
    - inputGuardrails / outputGuardrails (List<InputGuardrail> / 
List<OutputGuardrail>):
      pre-instantiated guardrail instances for DI-friendly usage 
(Spring/Quarkus CDI),
      complementing the existing class-based guardrail support
    - inputGuardrailsConfig / outputGuardrailsConfig: guardrail configuration 
objects
      controlling retry policies and other guardrail behavior
    - beforeToolExecution / afterToolExecution (Consumer<BeforeToolExecution> /
      Consumer<ToolExecution>): hooks for per-tool-call logging, metrics, and 
tracing
    
    Deliberately not added:
    - maxSequentialToolsInvocations: deprecated alias for 
maxToolCallingRoundTrips
      (already exposed) since LangChain4j 1.15.0
    - immediateReturnToolNames: no standalone AiServices method; already 
available
      via ToolProviderResult in the ToolProvider path
    
    Also improved aiServicesCustomizer Javadoc to document the 
chatRequestTransformer
    conflict with responseFormat/structured output, and listed builder options
    deliberately left for the escape hatch (toolSearchStrategy,
    storeRetrievedContentInChatMemory, AiServiceListener).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../langchain4j/agent/api/AbstractAgent.java       |  26 +++
 .../langchain4j/agent/api/AgentConfiguration.java  | 197 ++++++++++++++++++++-
 .../agent/api/AgentConfigurationTest.java          | 134 ++++++++++++++
 3 files changed, 355 insertions(+), 2 deletions(-)

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 9c0689f8f51a..e4dfad5a23fc 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
@@ -163,6 +163,24 @@ public abstract class AbstractAgent<S> implements Agent {
             builder.outputGuardrailClasses((List) 
configuration.getOutputGuardrailClasses());
         }
 
+        // Input Guardrail instances (pre-instantiated, DI-friendly)
+        if (configuration.getInputGuardrails() != null && 
!configuration.getInputGuardrails().isEmpty()) {
+            builder.inputGuardrails(configuration.getInputGuardrails());
+        }
+
+        // Output Guardrail instances (pre-instantiated, DI-friendly)
+        if (configuration.getOutputGuardrails() != null && 
!configuration.getOutputGuardrails().isEmpty()) {
+            builder.outputGuardrails(configuration.getOutputGuardrails());
+        }
+
+        // Guardrail configuration (retry policies, etc.)
+        if (configuration.getInputGuardrailsConfig() != null) {
+            
builder.inputGuardrailsConfig(configuration.getInputGuardrailsConfig());
+        }
+        if (configuration.getOutputGuardrailsConfig() != null) {
+            
builder.outputGuardrailsConfig(configuration.getOutputGuardrailsConfig());
+        }
+
         // Response Format (structured output): set once at startup via 
setResponseFormat(), used here per request
         if (responseFormat != null) {
             builder.chatRequestTransformer(chatRequest -> 
chatRequest.toBuilder().responseFormat(responseFormat).build());
@@ -185,6 +203,14 @@ public abstract class AbstractAgent<S> implements Agent {
             
builder.compensateOnToolErrors(configuration.getCompensateOnToolErrors());
         }
 
+        // Tool execution hooks
+        if (configuration.getBeforeToolExecution() != null) {
+            
builder.beforeToolExecution(configuration.getBeforeToolExecution());
+        }
+        if (configuration.getAfterToolExecution() != null) {
+            builder.afterToolExecution(configuration.getAfterToolExecution());
+        }
+
         // 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 c813d7454ae5..9a2af7e0da91 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
@@ -28,12 +28,18 @@ 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.guardrail.InputGuardrail;
+import dev.langchain4j.guardrail.OutputGuardrail;
+import dev.langchain4j.guardrail.config.InputGuardrailsConfig;
+import dev.langchain4j.guardrail.config.OutputGuardrailsConfig;
 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.BeforeToolExecution;
 import dev.langchain4j.service.tool.ToolArgumentsErrorHandler;
+import dev.langchain4j.service.tool.ToolExecution;
 import dev.langchain4j.service.tool.ToolExecutionErrorHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,11 +60,15 @@ import org.slf4j.LoggerFactory;
  * <li><strong>Chat Model:</strong> The underlying LLM for processing 
conversations</li>
  * <li><strong>Memory Provider:</strong> For maintaining conversation history 
in stateful agents</li>
  * <li><strong>Retrieval Augmentor:</strong> For RAG (Retrieval-Augmented 
Generation) capabilities</li>
- * <li><strong>Input Guardrails:</strong> Security filters applied to incoming 
messages</li>
- * <li><strong>Output Guardrails:</strong> Security filters applied to agent 
responses</li>
+ * <li><strong>Input Guardrails:</strong> Security filters applied to incoming 
messages (class-based or
+ * instance-based)</li>
+ * <li><strong>Output Guardrails:</strong> Security filters applied to agent 
responses (class-based or
+ * instance-based)</li>
+ * <li><strong>Guardrail Configuration:</strong> Retry policies and other 
guardrail behavior settings</li>
  * <li><strong>Custom Tools:</strong> Custom LangChain4j tools with @Tool 
annotations</li>
  * <li><strong>MCP Clients:</strong> Model Context Protocol clients for 
external tool integration</li>
  * <li><strong>MCP Tool Filters:</strong> Filters for controlling which MCP 
tools are available</li>
+ * <li><strong>Tool Execution Hooks:</strong> Callbacks invoked before and 
after each tool execution</li>
  * </ul>
  *
  * @since 4.9.0
@@ -79,6 +89,12 @@ public class AgentConfiguration {
     private ToolExecutionErrorHandler toolExecutionErrorHandler;
     private ToolArgumentsErrorHandler toolArgumentsErrorHandler;
     private Boolean compensateOnToolErrors;
+    private List<InputGuardrail> inputGuardrails;
+    private List<OutputGuardrail> outputGuardrails;
+    private InputGuardrailsConfig inputGuardrailsConfig;
+    private OutputGuardrailsConfig outputGuardrailsConfig;
+    private Consumer<BeforeToolExecution> beforeToolExecution;
+    private Consumer<ToolExecution> afterToolExecution;
     private Consumer<AiServices<?>> aiServicesCustomizer;
 
     /**
@@ -463,6 +479,165 @@ public class AgentConfiguration {
         return this;
     }
 
+    /**
+     * Gets the configured input guardrail instances for security filtering.
+     *
+     * <p>
+     * Unlike {@link #getInputGuardrailClasses()}, which returns classes to be 
instantiated by LangChain4j via no-arg
+     * constructor, this returns pre-instantiated guardrail objects that can 
carry configuration, injected
+     * collaborators, or a {@code CamelContext} reference.
+     * </p>
+     *
+     * @return the list of input guardrail instances, or {@code null} if not 
configured
+     */
+    public List<InputGuardrail> getInputGuardrails() {
+        return inputGuardrails;
+    }
+
+    /**
+     * Sets pre-instantiated input guardrail instances for security filtering 
of incoming messages.
+     *
+     * <p>
+     * Use this method when guardrails need configuration (thresholds, 
wordlists, vault-sourced patterns) or injected
+     * collaborators from a DI container (Spring, Quarkus CDI). Both guardrail 
classes and instances can be set; they
+     * are additive on the AiServices builder.
+     * </p>
+     *
+     * @param  inputGuardrails list of guardrail instances to apply to user 
inputs
+     * @return                 this configuration instance for method chaining
+     * @see                    #withInputGuardrailClasses(List)
+     */
+    public AgentConfiguration withInputGuardrails(List<InputGuardrail> 
inputGuardrails) {
+        this.inputGuardrails = inputGuardrails;
+        return this;
+    }
+
+    /**
+     * Gets the configured output guardrail instances for security filtering.
+     *
+     * <p>
+     * Unlike {@link #getOutputGuardrailClasses()}, which returns classes to 
be instantiated by LangChain4j via no-arg
+     * constructor, this returns pre-instantiated guardrail objects that can 
carry configuration, injected
+     * collaborators, or a {@code CamelContext} reference.
+     * </p>
+     *
+     * @return the list of output guardrail instances, or {@code null} if not 
configured
+     */
+    public List<OutputGuardrail> getOutputGuardrails() {
+        return outputGuardrails;
+    }
+
+    /**
+     * Sets pre-instantiated output guardrail instances for security filtering 
of agent responses.
+     *
+     * <p>
+     * Use this method when guardrails need configuration or injected 
collaborators from a DI container. Both guardrail
+     * classes and instances can be set; they are additive on the AiServices 
builder.
+     * </p>
+     *
+     * @param  outputGuardrails list of guardrail instances to apply to agent 
outputs
+     * @return                  this configuration instance for method chaining
+     * @see                     #withOutputGuardrailClasses(List)
+     */
+    public AgentConfiguration withOutputGuardrails(List<OutputGuardrail> 
outputGuardrails) {
+        this.outputGuardrails = outputGuardrails;
+        return this;
+    }
+
+    /**
+     * Gets the configuration for input guardrails (e.g., max retries).
+     *
+     * @return the input guardrails configuration, or {@code null} if not 
configured
+     */
+    public InputGuardrailsConfig getInputGuardrailsConfig() {
+        return inputGuardrailsConfig;
+    }
+
+    /**
+     * Sets the configuration for input guardrails. This controls behavior 
such as retry policies when an input
+     * guardrail rejects a message.
+     *
+     * @param  inputGuardrailsConfig the configuration for input guardrails
+     * @return                       this configuration instance for method 
chaining
+     */
+    public AgentConfiguration withInputGuardrailsConfig(InputGuardrailsConfig 
inputGuardrailsConfig) {
+        this.inputGuardrailsConfig = inputGuardrailsConfig;
+        return this;
+    }
+
+    /**
+     * Gets the configuration for output guardrails (e.g., max retries).
+     *
+     * @return the output guardrails configuration, or {@code null} if not 
configured
+     */
+    public OutputGuardrailsConfig getOutputGuardrailsConfig() {
+        return outputGuardrailsConfig;
+    }
+
+    /**
+     * Sets the configuration for output guardrails. This controls behavior 
such as retry policies when an output
+     * guardrail rejects a response.
+     *
+     * @param  outputGuardrailsConfig the configuration for output guardrails
+     * @return                        this configuration instance for method 
chaining
+     */
+    public AgentConfiguration 
withOutputGuardrailsConfig(OutputGuardrailsConfig outputGuardrailsConfig) {
+        this.outputGuardrailsConfig = outputGuardrailsConfig;
+        return this;
+    }
+
+    /**
+     * Gets the callback invoked before each tool execution.
+     *
+     * @return the before-tool-execution consumer, or {@code null} if not 
configured
+     */
+    public Consumer<BeforeToolExecution> getBeforeToolExecution() {
+        return beforeToolExecution;
+    }
+
+    /**
+     * Sets a callback that is invoked before each tool execution. This is the 
natural hook for per-tool-call logging,
+     * Camel events, Micrometer metrics and OpenTelemetry spans.
+     *
+     * <p>
+     * <strong>Note:</strong> The {@link BeforeToolExecution} type is marked 
{@code @Experimental} in LangChain4j — its
+     * API may change in future versions.
+     * </p>
+     *
+     * @param  beforeToolExecution the consumer to invoke before each tool 
execution
+     * @return                     this configuration instance for method 
chaining
+     */
+    public AgentConfiguration 
withBeforeToolExecution(Consumer<BeforeToolExecution> beforeToolExecution) {
+        this.beforeToolExecution = beforeToolExecution;
+        return this;
+    }
+
+    /**
+     * Gets the callback invoked after each tool execution.
+     *
+     * @return the after-tool-execution consumer, or {@code null} if not 
configured
+     */
+    public Consumer<ToolExecution> getAfterToolExecution() {
+        return afterToolExecution;
+    }
+
+    /**
+     * Sets a callback that is invoked after each tool execution. This is the 
natural hook for per-tool-call logging,
+     * Camel events, Micrometer metrics and OpenTelemetry spans.
+     *
+     * <p>
+     * <strong>Note:</strong> The {@link ToolExecution} type is marked {@code 
@Experimental} in LangChain4j — its API
+     * may change in future versions.
+     * </p>
+     *
+     * @param  afterToolExecution the consumer to invoke after each tool 
execution
+     * @return                    this configuration instance for method 
chaining
+     */
+    public AgentConfiguration withAfterToolExecution(Consumer<ToolExecution> 
afterToolExecution) {
+        this.afterToolExecution = afterToolExecution;
+        return this;
+    }
+
     /**
      * Gets the custom AiServices builder customizer.
      *
@@ -477,6 +652,24 @@ public class AgentConfiguration {
      * 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.
      *
+     * <p>
+     * <strong>Warning:</strong> A customizer that calls {@code 
chatRequestTransformer(...)} will silently replace the
+     * transformer installed by {@code configureBuilder()} for {@code 
responseFormat}, breaking
+     * {@code jsonSchema}/{@code outputClass} structured output. If you need 
to set both a custom transformer and
+     * structured output, compose both transformers into a single {@code 
UnaryOperator<ChatRequest>} and set it via the
+     * customizer.
+     * </p>
+     *
+     * <p>
+     * The following AiServices builder options are deliberately not exposed 
as first-class fields and should be
+     * configured via this escape hatch when needed:
+     * </p>
+     * <ul>
+     * <li>{@code toolSearchStrategy} — overlaps with the component's own 
tool-search mechanism</li>
+     * <li>{@code storeRetrievedContentInChatMemory} — niche RAG-memory 
flag</li>
+     * <li>{@code AiServiceListener} — users can attach {@code 
ChatModelListener}s to the ChatModel bean directly</li>
+     * </ul>
+     *
      * @param  aiServicesCustomizer the customizer to apply to the AiServices 
builder
      * @return                      this configuration instance for method 
chaining
      */
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 297ac1ccc71e..d96e954b7ac5 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
@@ -19,11 +19,22 @@ package org.apache.camel.component.langchain4j.agent.api;
 import java.io.Serializable;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 import dev.langchain4j.agent.tool.ToolExecutionRequest;
 import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.guardrail.InputGuardrail;
+import dev.langchain4j.guardrail.InputGuardrailRequest;
+import dev.langchain4j.guardrail.InputGuardrailResult;
+import dev.langchain4j.guardrail.OutputGuardrail;
+import dev.langchain4j.guardrail.OutputGuardrailRequest;
+import dev.langchain4j.guardrail.OutputGuardrailResult;
+import dev.langchain4j.guardrail.config.InputGuardrailsConfig;
+import dev.langchain4j.guardrail.config.OutputGuardrailsConfig;
+import dev.langchain4j.service.tool.BeforeToolExecution;
 import dev.langchain4j.service.tool.ToolArgumentsErrorHandler;
+import dev.langchain4j.service.tool.ToolExecution;
 import dev.langchain4j.service.tool.ToolExecutionErrorHandler;
 import org.junit.jupiter.api.Test;
 
@@ -331,16 +342,133 @@ public class AgentConfigurationTest {
         assertNotNull(config.getAiServicesCustomizer());
     }
 
+    // Tests for guardrail instances
+
+    @Test
+    public void testInputGuardrails() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getInputGuardrails());
+
+        InputGuardrail guardrail = new InputGuardrail() {
+            @Override
+            public InputGuardrailResult validate(InputGuardrailRequest 
request) {
+                return InputGuardrailResult.success();
+            }
+        };
+        List<InputGuardrail> guardrails = List.of(guardrail);
+        AgentConfiguration result = config.withInputGuardrails(guardrails);
+
+        assertSame(config, result);
+        assertSame(guardrails, config.getInputGuardrails());
+        assertEquals(1, config.getInputGuardrails().size());
+    }
+
+    @Test
+    public void testOutputGuardrails() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getOutputGuardrails());
+
+        OutputGuardrail guardrail = new OutputGuardrail() {
+            @Override
+            public OutputGuardrailResult validate(OutputGuardrailRequest 
request) {
+                return OutputGuardrailResult.success();
+            }
+        };
+        List<OutputGuardrail> guardrails = List.of(guardrail);
+        AgentConfiguration result = config.withOutputGuardrails(guardrails);
+
+        assertSame(config, result);
+        assertSame(guardrails, config.getOutputGuardrails());
+        assertEquals(1, config.getOutputGuardrails().size());
+    }
+
+    @Test
+    public void testInputGuardrailsConfig() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getInputGuardrailsConfig());
+
+        InputGuardrailsConfig guardrailsConfig = 
InputGuardrailsConfig.builder().build();
+        AgentConfiguration result = 
config.withInputGuardrailsConfig(guardrailsConfig);
+
+        assertSame(config, result);
+        assertSame(guardrailsConfig, config.getInputGuardrailsConfig());
+    }
+
+    @Test
+    public void testOutputGuardrailsConfig() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getOutputGuardrailsConfig());
+
+        OutputGuardrailsConfig guardrailsConfig = 
OutputGuardrailsConfig.builder().build();
+        AgentConfiguration result = 
config.withOutputGuardrailsConfig(guardrailsConfig);
+
+        assertSame(config, result);
+        assertSame(guardrailsConfig, config.getOutputGuardrailsConfig());
+    }
+
+    // Tests for tool execution hooks
+
+    @Test
+    public void testBeforeToolExecution() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getBeforeToolExecution());
+
+        Consumer<BeforeToolExecution> hook = beforeExec -> {
+        };
+        AgentConfiguration result = config.withBeforeToolExecution(hook);
+
+        assertSame(config, result);
+        assertSame(hook, config.getBeforeToolExecution());
+    }
+
+    @Test
+    public void testAfterToolExecution() {
+        AgentConfiguration config = new AgentConfiguration();
+        assertNull(config.getAfterToolExecution());
+
+        Consumer<ToolExecution> hook = afterExec -> {
+        };
+        AgentConfiguration result = config.withAfterToolExecution(hook);
+
+        assertSame(config, result);
+        assertSame(hook, config.getAfterToolExecution());
+    }
+
     @Test
     public void testFluentChaining() {
         ToolExecutionErrorHandler execHandler = (error, context) -> null;
         ToolArgumentsErrorHandler argsHandler = (error, context) -> null;
+        Consumer<BeforeToolExecution> beforeHook = beforeExec -> {
+        };
+        Consumer<ToolExecution> afterHook = afterExec -> {
+        };
+        InputGuardrailsConfig inConfig = 
InputGuardrailsConfig.builder().build();
+        OutputGuardrailsConfig outConfig = 
OutputGuardrailsConfig.builder().build();
+
+        InputGuardrail inputGuardrail = new InputGuardrail() {
+            @Override
+            public InputGuardrailResult validate(InputGuardrailRequest 
request) {
+                return InputGuardrailResult.success();
+            }
+        };
+        OutputGuardrail outputGuardrail = new OutputGuardrail() {
+            @Override
+            public OutputGuardrailResult validate(OutputGuardrailRequest 
request) {
+                return OutputGuardrailResult.success();
+            }
+        };
 
         AgentConfiguration config = new AgentConfiguration()
                 .withMaxToolCallingRoundTrips(5)
                 .withToolExecutionErrorHandler(execHandler)
                 .withToolArgumentsErrorHandler(argsHandler)
                 .withCompensateOnToolErrors(true)
+                .withInputGuardrails(List.of(inputGuardrail))
+                .withOutputGuardrails(List.of(outputGuardrail))
+                .withInputGuardrailsConfig(inConfig)
+                .withOutputGuardrailsConfig(outConfig)
+                .withBeforeToolExecution(beforeHook)
+                .withAfterToolExecution(afterHook)
                 .withAiServicesCustomizer(builder -> {
                 });
 
@@ -348,6 +476,12 @@ public class AgentConfigurationTest {
         assertSame(execHandler, config.getToolExecutionErrorHandler());
         assertSame(argsHandler, config.getToolArgumentsErrorHandler());
         assertTrue(config.getCompensateOnToolErrors());
+        assertEquals(1, config.getInputGuardrails().size());
+        assertEquals(1, config.getOutputGuardrails().size());
+        assertSame(inConfig, config.getInputGuardrailsConfig());
+        assertSame(outConfig, config.getOutputGuardrailsConfig());
+        assertSame(beforeHook, config.getBeforeToolExecution());
+        assertSame(afterHook, config.getAfterToolExecution());
         assertNotNull(config.getAiServicesCustomizer());
     }
 }

Reply via email to