Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"CamelLangChain4jAgentRequestModel": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The request model name.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#REQUEST_MODEL" },
"CamelLangChain4jAgentResponseModel": { "index": 11, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response model name. Not set by the agent producer when langchain4j Result does not expose it.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#RESPONSE_MODEL" },
"CamelLangChain4jAgentSources": { "index": 12, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.rag.content.Content>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "RAG sources retrieved during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#SOURCES" },
"CamelLangChain4jAgentToolExecutions": { "index": 13, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.service.tool.ToolExecution>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tool executions performed during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOOL_EXECUTIONS" }
"CamelLangChain4jAgentToolExecutions": { "index": 13, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List<dev.langchain4j.service.tool.ToolExecution>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tool executions performed during agent invocation.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#TOOL_EXECUTIONS" },
"CamelLangChain4jAgentModerationFlagged": { "index": 14, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Set to true when user input is rejected by the configured moderation model; not set on success.", "constantName": "org.apache.camel.component.langchain4j.agent.api.Headers#MODERATION_FLAGGED" }
},
"properties": {
"agentId": { "index": 0, "kind": "path", "displayName": "Agent Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Agent id" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6842,6 +6842,16 @@
"default": false
}
},
{
"name": "kamelets",
"in": "query",
"description": "Whether to include routes created by Kamelets. These are hidden by default, as they are an implementation detail of the Kamelet",
"required": false,
"schema": {
"type": "boolean",
"default": false
}
},
{
"name": "metric",
"in": "query",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,24 @@
"defaultValue": false,
"description": "Whether to include external systems (databases, messaging brokers, etc.) as nodes"
},
"metric": {
"kamelets": {
"index": 1,
"kind": "option",
"displayName": "Kamelets",
"group": "query",
"label": "query",
"required": false,
"type": "boolean",
"javaType": "java.lang.Boolean",
"deprecated": false,
"autowired": false,
"secret": false,
"defaultValue": false,
"description": "Whether to include routes created by Kamelets. These are hidden by default, as they are an implementation detail of the Kamelet"
},
"metric": {
"index": 2,
"kind": "option",
"displayName": "Metric",
"group": "query",
"label": "query",
Expand All @@ -44,7 +59,7 @@
"description": "Whether to include live metrics (message counts) on nodes and edges"
},
"routes": {
"index": 2,
"index": 3,
"kind": "option",
"displayName": "Routes",
"group": "query",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Agents are configured using the `AgentConfiguration` class which provides a flue
* Chat Memory Provider (for memory-enabled agents)
* Retrieval Augmentor (for RAG functionality)
* Input and Output Guardrails
* Content moderation (`withModerationModel`)
* Concurrent tool execution (`withExecuteToolsConcurrently`) for parallel Camel route tools and MCP tools within one LLM round trip
* Tool-calling control: round-trip limits, hallucinated tool handling, and error compensation
* AiServices builder customizer for advanced LangChain4j options
Expand All @@ -178,6 +179,28 @@ Agents are configured using the `AgentConfiguration` class which provides a flue
| `withExecuteToolsConcurrently()` / `withExecuteToolsConcurrently(Executor)` | Runs multiple tool calls from one LLM turn in parallel
|===

==== Content moderation

When `AgentConfiguration.withModerationModel(ModerationModel)` is set, Camel pre-moderates the user message before invoking the chat model. Flagged input raises `ModerationException` before tools run or memory is updated. The producer sets the `CamelLangChain4jAgentModerationFlagged` header to `true` on failure so routes can return a controlled refusal. See xref:others:langchain4j-agent-guardrails.adoc#_content_moderation[Content Moderation] for route examples. The flagged text is available via `${exception.moderation}` when handling `ModerationException`.

._Java-only: public support chatbot with moderation_
[source,java]
----
AgentConfiguration configuration = new AgentConfiguration()
.withChatModel(chatModel)
.withModerationModel(openAiModerationModel);

context.getRegistry().bind("supportAgentConfig", configuration);

onException(ModerationException.class)
.handled(true)
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(422))
.setBody(constant("Sorry, your message violates our usage policy."));

from("platform-http:/support/chat")
.to("langchain4j-agent:support?agentConfiguration=#supportAgentConfig&tags=orders,billing");
----

._Java-only: recover from a hallucinated tool name_
[source,java]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,12 +610,44 @@ public class AgentConfig {
}
----

=== Content Moderation

Configure a LangChain4j `ModerationModel` on `AgentConfiguration.withModerationModel(...)`. Camel pre-moderates the user message before the chat model is invoked, so flagged input raises `ModerationException` before tools run or conversation memory is updated. The producer sets the `CamelLangChain4jAgentModerationFlagged` header to `true` when moderation flags the input so routes can return a controlled refusal. Provider infrastructure failures are rethrown without setting the flagged header.

Moderation runs before LangChain4j input guardrails, which execute inside the AiServices call. Moderation complements guardrails: guardrails are deterministic Java rules you implement; moderation delegates policy checks to a provider API (OpenAI Moderation, Mistral moderation, etc.). Only the user message text is moderated; system prompts configured by the route author are not sent to the moderation model.

Custom `Agent` implementations should call `ModerationSupport.moderateUserMessage(...)` when they bypass `AgentWithMemory` / `AgentWithoutMemory`. Moderation failures are policy rejections (`GenAiErrorCategory.VALIDATION`), not malformed requests to retry blindly.

._Java-only: public support chatbot with moderation and controlled refusal_
[source,java]
----
AgentConfiguration configuration = new AgentConfiguration()
.withChatModel(chatModel)
.withModerationModel(openAiModerationModel);

context.getRegistry().bind("supportAgentConfig", configuration);

onException(ModerationException.class)
.handled(true)
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(422))
.setBody(constant("Sorry, your message violates our usage policy."));

from("platform-http:/support/chat")
.to("langchain4j-agent:support?agentConfiguration=#supportAgentConfig&tags=orders,billing");
----

In Spring Boot, inject an auto-configured `ModerationModel` bean from the LangChain4j starter alongside `ChatModel`.

=== Guardrail Behavior Summary

[cols="1,1,2"]
|===
|Guardrail |When Triggered |Result

|`Content moderation`
|User message flagged by moderation model
|Blocks the request before input guardrails and chat model

|`InputLengthGuardrail`
|Input too short/long
|Blocks the request
Expand Down
6 changes: 6 additions & 0 deletions components/camel-ai/camel-ai-observability-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@
<version>${langchain4j-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j-version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.camel</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public final class GenAiErrorSupport {
Map.entry("dev.langchain4j.exception.InvalidRequestException", GenAiErrorCategory.VALIDATION),
Map.entry("dev.langchain4j.exception.ContentFilteredException", GenAiErrorCategory.VALIDATION),
Map.entry("dev.langchain4j.exception.ModelNotFoundException", GenAiErrorCategory.VALIDATION),
Map.entry("dev.langchain4j.exception.ToolArgumentsException", GenAiErrorCategory.VALIDATION));
Map.entry("dev.langchain4j.exception.ToolArgumentsException", GenAiErrorCategory.VALIDATION),
Map.entry("dev.langchain4j.service.ModerationException", GenAiErrorCategory.VALIDATION));

private static final Map<String, GenAiErrorCategory> OPENAI_EXCEPTION_CATEGORIES = Map.ofEntries(
Map.entry("com.openai.errors.RateLimitException", GenAiErrorCategory.RATE_LIMIT),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import dev.langchain4j.exception.InternalServerException;
import dev.langchain4j.exception.InvalidRequestException;
import dev.langchain4j.exception.RateLimitException;
import dev.langchain4j.model.moderation.Moderation;
import dev.langchain4j.service.ModerationException;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -98,6 +100,29 @@ void shouldApplyCategoryPropertyToExchange() {
assertThat(exchange.getProperty(GenAiErrorProperties.RETRY_AFTER_MILLIS)).isNull();
}

@Test
void shouldClassifyModerationExceptionAsValidation() {
Moderation moderation = Moderation.flagged("bad");
assertThat(GenAiErrorSupport.classify(new ModerationException("flagged", moderation)))
.isEqualTo(GenAiErrorCategory.VALIDATION);
}

@Test
void shouldClassifyWrappedModerationExceptionAsValidation() {
Moderation moderation = Moderation.flagged("bad");
RuntimeException wrapped = new RuntimeException("outer", new ModerationException("flagged", moderation));
assertThat(GenAiErrorSupport.classify(wrapped)).isEqualTo(GenAiErrorCategory.VALIDATION);
}

@Test
void shouldApplyModerationExceptionCategoryToExchange() {
DefaultExchange exchange = new DefaultExchange(context);
GenAiErrorSupport.apply(exchange, new ModerationException("flagged", Moderation.flagged("bad")));

assertThat(exchange.getProperty(GenAiErrorProperties.ERROR_CATEGORY, String.class))
.isEqualTo(GenAiErrorCategory.VALIDATION.name());
}

@Test
void shouldReturnUnknownForUnrecognizedException() {
assertThat(GenAiErrorSupport.classify(new IllegalStateException("boom")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ public void setResponseFormat(ResponseFormat responseFormat) {
* @param builder the AI services builder to configure
* @param toolProvider the Apache Camel tool provider (may be null)
*/
@SuppressWarnings("unchecked")
protected void configureBuilder(AiServices<S> builder, ToolProvider toolProvider) {
// Collect all tool providers to compose them into a single provider
List<ToolProvider> toolProviders = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.moderation.ModerationModel;
import dev.langchain4j.rag.RetrievalAugmentor;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolArgumentsErrorHandler;
Expand All @@ -58,6 +59,7 @@
* <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>Moderation Model:</strong> Pre-moderation of user message text before the chat model is invoked</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>
Expand All @@ -69,6 +71,7 @@ public class AgentConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(AgentConfiguration.class);

private ChatModel chatModel;
private ModerationModel moderationModel;
private ChatMemoryProvider chatMemoryProvider;
private RetrievalAugmentor retrievalAugmentor;
private List<Class<?>> inputGuardrailClasses;
Expand Down Expand Up @@ -105,6 +108,30 @@ public AgentConfiguration withChatModel(ChatModel chatModel) {
return this;
}

/**
* Gets the configured moderation model.
*
* @return the moderation model instance, or {@code null} if not configured
* @since 4.23
*/
public ModerationModel getModerationModel() {
return moderationModel;
}

/**
* Sets the LangChain4j moderation model for content policy checks on agent user messages. When configured, flagged
* user input raises {@code ModerationException} before the chat model is invoked, so tools and memory are not
* updated for rejected input.
*
* @param moderationModel the moderation model (for example OpenAI or Mistral moderation APIs)
* @return this configuration instance for method chaining
* @since 4.23
*/
public AgentConfiguration withModerationModel(ModerationModel moderationModel) {
this.moderationModel = moderationModel;
return this;
}

/**
* Gets the configured chat memory provider.
*
Expand Down Expand Up @@ -539,6 +566,7 @@ public AgentConfiguration withExecuteToolsConcurrentlyEnabled(Boolean executeToo
public AgentConfiguration duplicate() {
AgentConfiguration copy = new AgentConfiguration();
copy.chatModel = chatModel;
copy.moderationModel = moderationModel;
copy.chatMemoryProvider = chatMemoryProvider;
copy.retrievalAugmentor = retrievalAugmentor;
copy.inputGuardrailClasses = inputGuardrailClasses;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public AgentWithMemory(AgentConfiguration configuration) {

@Override
public Result<String> chat(AiAgentBody<?> aiAgentBody, ToolProvider toolProvider) {
ModerationSupport.moderateUserMessage(configuration.getModerationModel(), aiAgentBody.getUserMessage());

AiAgentWithMemoryService agentService = createAiAgentService(toolProvider);

String userMessage = aiAgentBody.getUserMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public AgentWithoutMemory(AgentConfiguration configuration) {

@Override
public Result<String> chat(AiAgentBody<?> aiAgentBody, ToolProvider toolProvider) {
ModerationSupport.moderateUserMessage(configuration.getModerationModel(), aiAgentBody.getUserMessage());

AiAgentWithoutMemoryService agentService = createAiAgentService(toolProvider);

String userMessage = aiAgentBody.getUserMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,8 @@ public class Headers {
@Metadata(description = "Tool executions performed during agent invocation.",
javaType = "java.util.List<dev.langchain4j.service.tool.ToolExecution>")
public static final String TOOL_EXECUTIONS = "CamelLangChain4jAgentToolExecutions";

@Metadata(description = "Set to true when user input is rejected by the configured moderation model; not set on success.",
javaType = "Boolean")
public static final String MODERATION_FLAGGED = "CamelLangChain4jAgentModerationFlagged";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.langchain4j.agent.api;

import dev.langchain4j.model.moderation.Moderation;
import dev.langchain4j.model.moderation.ModerationModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.service.ModerationException;
import org.apache.camel.util.ObjectHelper;

/**
* Pre-invocation content moderation for agent chat requests.
* <p/>
* Camel runs moderation before building the LangChain4j AI service call so flagged user input is rejected before the
* chat model, tools, or memory are updated. Custom {@link Agent} implementations should call
* {@link #moderateUserMessage(ModerationModel, String)} with the same semantics when they bypass
* {@link AgentWithMemory} or {@link AgentWithoutMemory}.
*
* @since 4.23
*/
public final class ModerationSupport {

private ModerationSupport() {
}

/**
* Moderates the user message when a {@link ModerationModel} is configured.
* <p/>
* Only non-empty user message text is moderated. Multimodal {@code Content} without a user message is not sent to
* the moderation model. When a moderation model is configured, a missing verdict fails closed and raises
* {@link ModerationException} without marking the input as flagged. Provider infrastructure failures (timeouts,
* rate limits, auth errors) are rethrown so callers can classify and retry them appropriately.
*
* @param moderationModel the moderation model, may be {@code null}
* @param userMessage the user message to check
* @throws ModerationException when the moderation model flags the input or returns no verdict
*/
public static void moderateUserMessage(ModerationModel moderationModel, String userMessage) {
if (moderationModel == null || ObjectHelper.isEmpty(userMessage)) {
return;
}

Response<Moderation> response = moderationModel.moderate(userMessage);
if (response == null || response.content() == null) {
throw new ModerationException(
"Moderation model returned no verdict for user message",
Moderation.notFlagged());
}

Moderation moderation = response.content();
if (moderation.flagged()) {
throw new ModerationException("User message flagged by moderation model", moderation);
}
}
}
Loading