This is an automated email from the ASF dual-hosted git repository.
fmariani pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new bf38b30ad657 use UserMessage for Multimodality
bf38b30ad657 is described below
commit bf38b30ad657505374e59215ed7847a1da0af5de
Author: Croway <[email protected]>
AuthorDate: Tue Dec 9 12:56:50 2025 +0100
use UserMessage for Multimodality
---
.../catalog/components/langchain4j-agent.json | 4 +-
.../{AgentWithMemory.java => AbstractAgent.java} | 69 ++--
.../component/langchain4j/agent/api/Agent.java | 21 +-
.../langchain4j/agent/api/AgentWithMemory.java | 77 ++--
.../langchain4j/agent/api/AgentWithoutMemory.java | 74 ++--
.../langchain4j/agent/api/AiAgentBody.java | 4 +-
.../agent/api/AiAgentWithMemoryService.java | 29 +-
.../agent/api/AiAgentWithoutMemoryService.java | 32 +-
.../component/langchain4j/agent/api/Headers.java | 7 +
.../agent/LangChain4jAgentConverterLoader.java | 76 ++++
.../langchain4j/agent/langchain4j-agent.json | 4 +-
.../services/org/apache/camel/TypeConverterLoader | 2 +
.../src/main/docs/langchain4j-agent-component.adoc | 119 ++++++
.../agent/LangChain4jAgentConverter.java | 408 +++++++++++++++++++++
.../agent/integration/AbstractRAGIT.java | 2 -
.../integration/LangChain4jAgentCustomToolsIT.java | 2 -
.../LangChain4jAgentGuardrailsIntegrationIT.java | 2 -
.../integration/LangChain4jAgentMcpToolsIT.java | 2 -
.../integration/LangChain4jAgentMixedToolsIT.java | 2 -
.../LangChain4jAgentMultimodalityIT.java | 168 +++++++++
.../integration/LangChain4jAgentWithMemoryIT.java | 2 -
.../integration/LangChain4jAgentWithToolsIT.java | 2 -
.../integration/LangChain4jAgentWrappedFileIT.java | 148 ++++++++
.../integration/LangChain4jSimpleAgentIT.java | 2 -
.../langchain4j/agent/integration/ModelHelper.java | 24 +-
.../src/test/resources/camel-logo.png | Bin 0 -> 190434 bytes
.../src/test/resources/test-document.pdf | 20 +
.../camel-langchain4j-agent/test-execution.md | 2 +-
.../LangChain4jAgentEndpointBuilderFactory.java | 26 ++
.../test/infra/openai/mock/RequestContext.java | 28 +-
30 files changed, 1186 insertions(+), 172 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
index 8f87aaa2d87f..6594b80bf1fc 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-agent.json
@@ -33,7 +33,9 @@
},
"headers": {
"CamelLangChain4jAgentSystemMessage": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The system prompt.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#SYSTEM_MESSAGE" },
- "CamelLangChain4jAgentMemoryId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Memory ID.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEMORY_ID" }
+ "CamelLangChain4jAgentMemoryId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Memory ID.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEMORY_ID" },
+ "CamelLangChain4jAgentUserMessage": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The user message to accompany file
content when using WrappedFile as input.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#USER_MESSAGE" },
+ "CamelLangChain4jAgentMediaType": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The media type (MIME type) of the file
content. Overrides auto-detection from file extension.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEDIA_TYPE" }
},
"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" },
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
similarity index 60%
copy from
components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
copy to
components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
index b6c7238393e0..f7a19d62973b 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java
@@ -22,49 +22,67 @@ import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
- * Implementation of Agent for AI agents with memory support. This agent
handles chat interactions while maintaining
- * conversation history.
+ * Abstract base class for AI agents that provides common configuration logic
for building LangChain4j AI services.
*
- * This is an internal class used only within the LangChain4j agent component.
+ * <p>
+ * This class encapsulates the shared logic for configuring:
+ * <ul>
+ * <li>Apache Camel Tool Provider</li>
+ * <li>MCP (Model Context Protocol) clients</li>
+ * <li>Custom LangChain4j tools</li>
+ * <li>RAG (Retrieval Augmented Generation)</li>
+ * <li>Input and Output Guardrails</li>
+ * </ul>
+ *
+ * <p>
+ * Subclasses must implement the {@link #chat(AiAgentBody, ToolProvider)}
method to provide specific chat behavior.
+ *
+ * @param <S> the type of the AI service interface (e.g.,
AiAgentWithMemoryService or AiAgentWithoutMemoryService)
*/
-public class AgentWithMemory implements Agent {
- private static final Logger LOG =
LoggerFactory.getLogger(AgentWithMemory.class);
+public abstract class AbstractAgent<S> implements Agent {
- private final AgentConfiguration configuration;
+ protected final AgentConfiguration configuration;
- public AgentWithMemory(AgentConfiguration configuration) {
+ protected AbstractAgent(AgentConfiguration configuration) {
this.configuration = configuration;
}
- @Override
- public String chat(AiAgentBody<?> aiAgentBody, ToolProvider toolProvider) {
- AiAgentWithMemoryService agentService =
createAiAgentService(toolProvider);
-
- return aiAgentBody.getSystemMessage() != null
- ? agentService.chat(aiAgentBody.getMemoryId(),
aiAgentBody.getUserMessage(), aiAgentBody.getSystemMessage())
- : agentService.chat(aiAgentBody.getMemoryId(),
aiAgentBody.getUserMessage());
+ /**
+ * Gets the agent configuration.
+ *
+ * @return the agent configuration
+ */
+ protected AgentConfiguration getConfiguration() {
+ return configuration;
}
/**
- * Create AI service with a single universal tool that handles multiple
Camel routes, Memory Provider, and
- * additional tools
+ * Configures the common aspects of the AI service builder.
+ *
+ * <p>
+ * This method applies the following configurations to the builder:
+ * <ul>
+ * <li>Apache Camel Tool Provider (if provided)</li>
+ * <li>MCP Tool Provider (if MCP clients are configured)</li>
+ * <li>Custom LangChain4j tools (if configured)</li>
+ * <li>RAG Retrieval Augmentor (if configured)</li>
+ * <li>Input Guardrails (if configured)</li>
+ * <li>Output Guardrails (if configured)</li>
+ * </ul>
+ *
+ * @param builder the AI services builder to configure
+ * @param toolProvider the Apache Camel tool provider (may be null)
*/
- private AiAgentWithMemoryService createAiAgentService(ToolProvider
toolProvider) {
- var builder = AiServices.builder(AiAgentWithMemoryService.class)
- .chatModel(configuration.getChatModel())
- .chatMemoryProvider(configuration.getChatMemoryProvider());
-
+ @SuppressWarnings("unchecked")
+ protected void configureBuilder(AiServices<S> builder, ToolProvider
toolProvider) {
// Apache Camel Tool Provider
if (toolProvider != null) {
builder.toolProvider(toolProvider);
}
// MCP Clients - create MCP ToolProvider if MCP clients are configured
- // import org.apache.camel.util.ObjectHelper
if (ObjectHelper.isNotEmpty(configuration.getMcpClients())) {
McpToolProvider.Builder mcpBuilder = McpToolProvider.builder()
.mcpClients(configuration.getMcpClients());
@@ -96,8 +114,5 @@ public class AgentWithMemory implements Agent {
if (configuration.getOutputGuardrailClasses() != null &&
!configuration.getOutputGuardrailClasses().isEmpty()) {
builder.outputGuardrailClasses((List)
configuration.getOutputGuardrailClasses());
}
-
- return builder.build();
}
-
}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Agent.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Agent.java
index ac1e006787d9..d8be18186470 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Agent.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Agent.java
@@ -70,7 +70,10 @@ public interface Agent {
* <li>The {@link Headers#MEMORY_ID} header value as the memory identifier
(if present)</li>
* </ul>
* </li>
- * <li>For any other payload type, it throws an {@link
InvalidPayloadRuntimeException}</li>
+ * <li>For other payload types (WrappedFile, byte[], InputStream), it uses
the Camel TypeConverter to convert to an
+ * {@link AiAgentBody} with the appropriate content type. This supports
file, ftp, sftp, aws2-s3,
+ * azure-storage-blob, and other components.</li>
+ * <li>If no conversion is possible, it throws an {@link
InvalidPayloadRuntimeException}</li>
* </ul>
*
* <p>
@@ -93,14 +96,20 @@ public interface Agent {
return payload;
}
- if (!(messagePayload instanceof String)) {
- throw new InvalidPayloadRuntimeException(exchange,
AiAgentBody.class);
+ if (messagePayload instanceof String) {
+ String systemMessage = exchange.getIn().getHeader(SYSTEM_MESSAGE,
String.class);
+ Object memoryId = exchange.getIn().getHeader(MEMORY_ID);
+ return new AiAgentBody<>((String) messagePayload, systemMessage,
memoryId);
}
- String systemMessage = exchange.getIn().getHeader(SYSTEM_MESSAGE,
String.class);
- Object memoryId = exchange.getIn().getHeader(MEMORY_ID);
+ // Try to convert using TypeConverter (supports WrappedFile, byte[],
InputStream, etc.)
+ AiAgentBody<?> body = exchange.getContext().getTypeConverter()
+ .tryConvertTo(AiAgentBody.class, exchange, messagePayload);
+ if (body != null) {
+ return body;
+ }
- return new AiAgentBody<>((String) messagePayload, systemMessage,
memoryId);
+ throw new InvalidPayloadRuntimeException(exchange, AiAgentBody.class);
}
/**
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
index b6c7238393e0..2f33a925349a 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithMemory.java
@@ -16,88 +16,55 @@
*/
package org.apache.camel.component.langchain4j.agent.api;
-import java.util.List;
-
-import dev.langchain4j.mcp.McpToolProvider;
+import dev.langchain4j.data.message.Content;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
-import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Implementation of Agent for AI agents with memory support. This agent
handles chat interactions while maintaining
* conversation history.
*
+ * <p>
* This is an internal class used only within the LangChain4j agent component.
*/
-public class AgentWithMemory implements Agent {
- private static final Logger LOG =
LoggerFactory.getLogger(AgentWithMemory.class);
-
- private final AgentConfiguration configuration;
+public class AgentWithMemory extends AbstractAgent<AiAgentWithMemoryService> {
public AgentWithMemory(AgentConfiguration configuration) {
- this.configuration = configuration;
+ super(configuration);
}
@Override
public String chat(AiAgentBody<?> aiAgentBody, ToolProvider toolProvider) {
AiAgentWithMemoryService agentService =
createAiAgentService(toolProvider);
- return aiAgentBody.getSystemMessage() != null
- ? agentService.chat(aiAgentBody.getMemoryId(),
aiAgentBody.getUserMessage(), aiAgentBody.getSystemMessage())
- : agentService.chat(aiAgentBody.getMemoryId(),
aiAgentBody.getUserMessage());
+ String userMessage = aiAgentBody.getUserMessage();
+ Object memoryId = aiAgentBody.getMemoryId();
+ String systemMessage = aiAgentBody.getSystemMessage();
+ Content content = aiAgentBody.getContent();
+
+ if (content != null) {
+ // Multi-modal message with content
+ return systemMessage != null
+ ? agentService.chat(memoryId, userMessage, content,
systemMessage)
+ : agentService.chat(memoryId, userMessage, content);
+ } else {
+ // Text-only message
+ return systemMessage != null
+ ? agentService.chat(memoryId, userMessage, systemMessage)
+ : agentService.chat(memoryId, userMessage);
+ }
}
/**
- * Create AI service with a single universal tool that handles multiple
Camel routes, Memory Provider, and
- * additional tools
+ * Create AI service with memory provider and common configurations.
*/
private AiAgentWithMemoryService createAiAgentService(ToolProvider
toolProvider) {
var builder = AiServices.builder(AiAgentWithMemoryService.class)
.chatModel(configuration.getChatModel())
.chatMemoryProvider(configuration.getChatMemoryProvider());
- // Apache Camel Tool Provider
- if (toolProvider != null) {
- builder.toolProvider(toolProvider);
- }
-
- // MCP Clients - create MCP ToolProvider if MCP clients are configured
- // import org.apache.camel.util.ObjectHelper
- if (ObjectHelper.isNotEmpty(configuration.getMcpClients())) {
- McpToolProvider.Builder mcpBuilder = McpToolProvider.builder()
- .mcpClients(configuration.getMcpClients());
-
- // Apply MCP tool filter if configured
- if (configuration.getMcpToolProviderFilter() != null) {
- mcpBuilder.filter(configuration.getMcpToolProviderFilter());
- }
-
- builder.toolProvider(mcpBuilder.build());
- }
-
- // Additional custom LangChain4j Tool Instances (objects with @Tool
methods)
- if (configuration.getCustomTools() != null &&
!configuration.getCustomTools().isEmpty()) {
- builder.tools(configuration.getCustomTools());
- }
-
- // RAG
- if (configuration.getRetrievalAugmentor() != null) {
- builder.retrievalAugmentor(configuration.getRetrievalAugmentor());
- }
-
- // Input Guardrails
- if (configuration.getInputGuardrailClasses() != null &&
!configuration.getInputGuardrailClasses().isEmpty()) {
- builder.inputGuardrailClasses((List)
configuration.getInputGuardrailClasses());
- }
-
- // Output Guardrails
- if (configuration.getOutputGuardrailClasses() != null &&
!configuration.getOutputGuardrailClasses().isEmpty()) {
- builder.outputGuardrailClasses((List)
configuration.getOutputGuardrailClasses());
- }
+ configureBuilder(builder, toolProvider);
return builder.build();
}
-
}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithoutMemory.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithoutMemory.java
index c5b8baa6b042..82d26fbf0fd6 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithoutMemory.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentWithoutMemory.java
@@ -16,82 +16,52 @@
*/
package org.apache.camel.component.langchain4j.agent.api;
-import java.util.List;
-
-import dev.langchain4j.mcp.McpToolProvider;
+import dev.langchain4j.data.message.Content;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
-import org.apache.camel.util.ObjectHelper;
/**
* Implementation of Agent for AI agents without memory support. This agent
handles chat interactions without
* maintaining conversation history.
*
+ * <p>
* This is an internal class used only within the LangChain4j agent component.
*/
-public class AgentWithoutMemory implements Agent {
-
- private final AgentConfiguration configuration;
+public class AgentWithoutMemory extends
AbstractAgent<AiAgentWithoutMemoryService> {
public AgentWithoutMemory(AgentConfiguration configuration) {
- this.configuration = configuration;
+ super(configuration);
}
@Override
public String chat(AiAgentBody<?> aiAgentBody, ToolProvider toolProvider) {
AiAgentWithoutMemoryService agentService =
createAiAgentService(toolProvider);
- return aiAgentBody.getSystemMessage() != null
- ? agentService.chat(aiAgentBody.getUserMessage(),
aiAgentBody.getSystemMessage())
- : agentService.chat(aiAgentBody.getUserMessage());
+ String userMessage = aiAgentBody.getUserMessage();
+ String systemMessage = aiAgentBody.getSystemMessage();
+ Content content = aiAgentBody.getContent();
+
+ if (content != null) {
+ // Multi-modal message with content
+ return systemMessage != null
+ ? agentService.chat(userMessage, content, systemMessage)
+ : agentService.chat(userMessage, content);
+ } else {
+ // Text-only message
+ return systemMessage != null
+ ? agentService.chat(userMessage, systemMessage)
+ : agentService.chat(userMessage);
+ }
}
/**
- * Create AI service with a single universal tool that handles multiple
Camel routes and additional tools
+ * Create AI service with common configurations (no memory provider).
*/
- private AiAgentWithoutMemoryService createAiAgentService(
- ToolProvider toolProvider) {
+ private AiAgentWithoutMemoryService createAiAgentService(ToolProvider
toolProvider) {
var builder = AiServices.builder(AiAgentWithoutMemoryService.class)
.chatModel(configuration.getChatModel());
- // Apache Camel Tool Provider
- if (toolProvider != null) {
- builder.toolProvider(toolProvider);
- }
-
- // MCP Clients - create MCP ToolProvider if MCP clients are configured
- // import org.apache.camel.util.ObjectHelper
- if (ObjectHelper.isNotEmpty(configuration.getMcpClients())) {
- McpToolProvider.Builder mcpBuilder = McpToolProvider.builder()
- .mcpClients(configuration.getMcpClients());
-
- // Apply MCP tool filter if configured
- if (configuration.getMcpToolProviderFilter() != null) {
- mcpBuilder.filter(configuration.getMcpToolProviderFilter());
- }
-
- builder.toolProvider(mcpBuilder.build());
- }
-
- // Additional custom LangChain4j Tool Instances (objects with @Tool
methods)
- if (configuration.getCustomTools() != null &&
!configuration.getCustomTools().isEmpty()) {
- builder.tools(configuration.getCustomTools());
- }
-
- // RAG
- if (configuration.getRetrievalAugmentor() != null) {
- builder.retrievalAugmentor(configuration.getRetrievalAugmentor());
- }
-
- // Input Guardrails
- if (configuration.getInputGuardrailClasses() != null &&
!configuration.getInputGuardrailClasses().isEmpty()) {
- builder.inputGuardrailClasses((List)
configuration.getInputGuardrailClasses());
- }
-
- // Output Guardrails
- if (configuration.getOutputGuardrailClasses() != null &&
!configuration.getOutputGuardrailClasses().isEmpty()) {
- builder.outputGuardrailClasses((List)
configuration.getOutputGuardrailClasses());
- }
+ configureBuilder(builder, toolProvider);
return builder.build();
}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentBody.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentBody.java
index f07eb022f8aa..c7386498cb27 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentBody.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentBody.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.component.langchain4j.agent.api;
+import dev.langchain4j.data.message.Content;
+
/**
* Request body class for AI agent chat interactions in the Apache Camel
LangChain4j integration.
*
@@ -56,7 +58,7 @@ package org.apache.camel.component.langchain4j.agent.api;
* @param <C> the type of content (e.g., TextContent, ImageContent,
AudioContent, VideoContent, PdfFileContent)
* @since 4.9.0
*/
-public class AiAgentBody<C> {
+public class AiAgentBody<C extends Content> {
private String userMessage;
private String systemMessage;
private Object memoryId;
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithMemoryService.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithMemoryService.java
index 8f7d292690ec..4837a096e917 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithMemoryService.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithMemoryService.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.langchain4j.agent.api;
+import dev.langchain4j.data.message.Content;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
@@ -36,6 +37,16 @@ public interface AiAgentWithMemoryService {
*/
String chat(@MemoryId Object memoryId, @UserMessage String message);
+ /**
+ * Chat with a user message containing both text and additional content
(e.g., images, audio) with memory support.
+ *
+ * @param memoryId the memory identifier for conversation history
+ * @param message the text portion of the user message
+ * @param content additional content such as ImageContent, AudioContent,
etc.
+ * @return the AI response
+ */
+ String chat(@MemoryId Object memoryId, @UserMessage String message,
@UserMessage Content content);
+
/**
* Simple chat with a user message, system message and memory window
*
@@ -45,6 +56,22 @@ public interface AiAgentWithMemoryService {
* @return
*/
@SystemMessage("{{prompt}}")
- String chat(@MemoryId Object memoryId, @UserMessage String message,
@V("prompt") String prompt);
+ String chat(
+ @MemoryId Object memoryId, @UserMessage String message,
+ @V("prompt") String prompt);
+
+ /**
+ * Chat with a user message containing both text and additional content,
with system message and memory support.
+ *
+ * @param memoryId the memory identifier for conversation history
+ * @param message the text portion of the user message
+ * @param content additional content such as ImageContent, AudioContent,
etc.
+ * @param prompt the system message template
+ * @return the AI response
+ */
+ @SystemMessage("{{prompt}}")
+ String chat(
+ @MemoryId Object memoryId, @UserMessage String message,
+ @UserMessage Content content, @V("prompt") String prompt);
}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithoutMemoryService.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithoutMemoryService.java
index 961a73e04375..dfa72a61e6e6 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithoutMemoryService.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AiAgentWithoutMemoryService.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.langchain4j.agent.api;
+import dev.langchain4j.data.message.Content;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
@@ -28,18 +29,39 @@ public interface AiAgentWithoutMemoryService {
/**
* Simple chat with a single user message
*
- * @param message the user message
+ * @param message the user message
+ * @return the AI response
*/
String chat(@UserMessage String message);
/**
- * Simple chat with a single user message and single prompt
+ * Chat with a user message containing both text and additional content
(e.g., images, audio).
*
- * @param message
- * @param prompt
- * @return
+ * @param message the text portion of the user message
+ * @param content additional content such as ImageContent, AudioContent,
etc.
+ * @return the AI response
+ */
+ String chat(@UserMessage String message, @UserMessage Content content);
+
+ /**
+ * Simple chat with a single user message and system message
+ *
+ * @param message the user message
+ * @param prompt the system message template
+ * @return the AI response
*/
@SystemMessage("{{prompt}}")
String chat(@UserMessage String message, @V("prompt") String prompt);
+ /**
+ * Chat with a user message containing both text and additional content,
with system message.
+ *
+ * @param message the text portion of the user message
+ * @param content additional content such as ImageContent, AudioContent,
etc.
+ * @param prompt the system message template
+ * @return the AI response
+ */
+ @SystemMessage("{{prompt}}")
+ String chat(@UserMessage String message, @UserMessage Content content,
@V("prompt") String prompt);
+
}
diff --git
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
index 5e53a96dcd52..c6a56b3a26df 100644
---
a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
+++
b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Headers.java
@@ -28,4 +28,11 @@ public class Headers {
@Metadata(description = "Memory ID.", javaType = "Object")
public static final String MEMORY_ID = "CamelLangChain4jAgentMemoryId";
+
+ @Metadata(description = "The user message to accompany file content when
using WrappedFile as input.", javaType = "String")
+ public static final String USER_MESSAGE =
"CamelLangChain4jAgentUserMessage";
+
+ @Metadata(description = "The media type (MIME type) of the file content.
Overrides auto-detection from file extension.",
+ javaType = "String")
+ public static final String MEDIA_TYPE = "CamelLangChain4jAgentMediaType";
}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterLoader.java
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterLoader.java
new file mode 100644
index 000000000000..1186963e3e9c
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterLoader.java
@@ -0,0 +1,76 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.langchain4j.agent;
+
+import javax.annotation.processing.Generated;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.DeferredContextBinding;
+import org.apache.camel.Exchange;
+import org.apache.camel.TypeConversionException;
+import org.apache.camel.TypeConverterLoaderException;
+import org.apache.camel.spi.TypeConverterLoader;
+import org.apache.camel.spi.TypeConverterRegistry;
+import org.apache.camel.support.SimpleTypeConverter;
+import org.apache.camel.support.TypeConverterSupport;
+import org.apache.camel.util.DoubleMap;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.TypeConverterLoaderGeneratorMojo")
+@SuppressWarnings("unchecked")
+@DeferredContextBinding
+public final class LangChain4jAgentConverterLoader implements
TypeConverterLoader, CamelContextAware {
+
+ private CamelContext camelContext;
+
+ public LangChain4jAgentConverterLoader() {
+ }
+
+ @Override
+ public void setCamelContext(CamelContext camelContext) {
+ this.camelContext = camelContext;
+ }
+
+ @Override
+ public CamelContext getCamelContext() {
+ return camelContext;
+ }
+
+ @Override
+ public void load(TypeConverterRegistry registry) throws
TypeConverterLoaderException {
+ registerConverters(registry);
+ }
+
+ private void registerConverters(TypeConverterRegistry registry) {
+ addTypeConverter(registry,
org.apache.camel.component.langchain4j.agent.api.AiAgentBody.class,
byte[].class, false,
+ (type, exchange, value) -> {
+ Object answer =
org.apache.camel.component.langchain4j.agent.LangChain4jAgentConverter.byteArrayToAiAgentBody((byte[])
value, exchange);
+ if (false && answer == null) {
+ answer = Void.class;
+ }
+ return answer;
+ });
+ addTypeConverter(registry,
org.apache.camel.component.langchain4j.agent.api.AiAgentBody.class,
java.io.InputStream.class, false,
+ (type, exchange, value) -> {
+ Object answer =
org.apache.camel.component.langchain4j.agent.LangChain4jAgentConverter.inputStreamToAiAgentBody((java.io.InputStream)
value, exchange);
+ if (false && answer == null) {
+ answer = Void.class;
+ }
+ return answer;
+ });
+ addTypeConverter(registry,
org.apache.camel.component.langchain4j.agent.api.AiAgentBody.class,
org.apache.camel.WrappedFile.class, false,
+ (type, exchange, value) -> {
+ Object answer =
org.apache.camel.component.langchain4j.agent.LangChain4jAgentConverter.toAiAgentBody((org.apache.camel.WrappedFile)
value, exchange);
+ if (false && answer == null) {
+ answer = Void.class;
+ }
+ return answer;
+ });
+ }
+
+ private static void addTypeConverter(TypeConverterRegistry registry,
Class<?> toType, Class<?> fromType, boolean allowNull,
SimpleTypeConverter.ConversionMethod method) {
+ registry.addTypeConverter(toType, fromType, new
SimpleTypeConverter(allowNull, method));
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
index 8f87aaa2d87f..6594b80bf1fc 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/agent/langchain4j-agent.json
@@ -33,7 +33,9 @@
},
"headers": {
"CamelLangChain4jAgentSystemMessage": { "index": 0, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The system prompt.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#SYSTEM_MESSAGE" },
- "CamelLangChain4jAgentMemoryId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Memory ID.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEMORY_ID" }
+ "CamelLangChain4jAgentMemoryId": { "index": 1, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Memory ID.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEMORY_ID" },
+ "CamelLangChain4jAgentUserMessage": { "index": 2, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The user message to accompany file
content when using WrappedFile as input.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#USER_MESSAGE" },
+ "CamelLangChain4jAgentMediaType": { "index": 3, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The media type (MIME type) of the file
content. Overrides auto-detection from file extension.", "constantName":
"org.apache.camel.component.langchain4j.agent.api.Headers#MEDIA_TYPE" }
},
"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" },
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
new file mode 100644
index 000000000000..59ed6c3979de
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+org.apache.camel.component.langchain4j.agent.LangChain4jAgentConverterLoader
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
index c47c8fe26d3d..bd83b7b2e245 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/docs/langchain4j-agent-component.adoc
@@ -28,6 +28,7 @@ The LangChain4j Agent component offers the following key
features:
* **RAG Support**: Integration with retrieval systems for naive and advanced
RAG
* **Guardrails**: Input and output validation and transformation
* **Configuration Flexibility**: Centralized agent configuration using
`AgentConfiguration`
+* **Multimodal Content**: Support for images, PDFs, audio, video, and text
files from file-based Camel components
== Component Options
@@ -576,3 +577,121 @@ from("direct:agent-with-guardrails")
====
The current version of the component returns a String as response. If the
outputGuardrails extends JsonExtractorOutputGuardrail class, make sure to
return a Json in String format.
====
+
+=== Multimodal Content Support
+
+The LangChain4j Agent component supports multimodal content, allowing you to
send images, PDFs, audio, video, and text files to AI models that support
vision and document understanding capabilities.
+
+==== Sending Multimodal Content via AiAgentBody
+
+You can explicitly create an `AiAgentBody` with multimodal content:
+
+[source,java]
+----
+// Load an image and create ImageContent
+byte[] imageBytes = Files.readAllBytes(Path.of("image.png"));
+String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+Image image = Image.builder()
+ .base64Data(base64Image)
+ .mimeType("image/png")
+ .build();
+ImageContent imageContent = ImageContent.from(image);
+
+// Create request body with image content
+AiAgentBody<ImageContent> body = new AiAgentBody<ImageContent>()
+ .withUserMessage("What do you see in this image?")
+ .withContent(imageContent);
+
+String response = template.requestBody("direct:chat", body, String.class);
+----
+
+==== Automatic File Conversion from Camel Components
+
+The agent component automatically converts files from various Camel components
to multimodal content. This enables seamless integration with file-based
sources.
+
+===== Supported Input Types
+
+[cols="1,2,2"]
+|===
+|Input Type |Source Components |MIME Type Detection
+
+|`WrappedFile`
+|`file:`, `ftp:`, `sftp:`, `smb:`
+|From file extension or headers
+
+|`byte[]`
+|`aws2-s3:`, `azure-storage-blob:`, `google-storage:`
+|From content type headers (required)
+
+|`InputStream`
+|Various streaming components
+|From content type headers (required)
+|===
+
+===== Example: Processing Images from File Component
+
+[source,java]
+----
+from("file:inbox/images?noop=true&include=.*\\.png")
+ .setHeader("CamelLangChain4jAgentUserMessage", constant("Describe this
image"))
+ .to("langchain4j-agent:vision?agent=#visionAgent")
+ .to("log:response");
+----
+
+===== Example: Processing Files from AWS S3
+
+[source,java]
+----
+from("aws2-s3://my-bucket?prefix=images/&includeBody=true")
+ .setHeader("CamelLangChain4jAgentUserMessage", constant("What do you see
in this image?"))
+ .to("langchain4j-agent:vision?agent=#visionAgent")
+ .to("log:response");
+----
+
+[NOTE]
+====
+When using `byte[]` or `InputStream` inputs, a MIME type header is required
since the type cannot be auto-detected from the content. The component checks
for MIME type in this priority order:
+
+1. `CamelLangChain4jAgentMediaType` header (highest priority - explicit
override)
+2. `CamelAwsS3ContentType` header (from AWS S3)
+3. `Content-Type` header
+4. `CamelFileContentType` header (from file components)
+====
+
+===== Example: Overriding MIME Type
+
+[source,java]
+----
+from("direct:process-file")
+ .setHeader("CamelLangChain4jAgentUserMessage", constant("Analyze this
document"))
+ .setHeader("CamelLangChain4jAgentMediaType", constant("application/pdf"))
+ .to("langchain4j-agent:analyzer?agent=#analyzerAgent");
+----
+
+==== Complete Multimodal Route Example
+
+Here's a complete example showing how to process images from a file system and
send them to an AI agent for analysis:
+
+[source,java]
+----
+// Create a vision-capable chat model
+ChatModel chatModel = OpenAiChatModel.builder()
+ .apiKey(apiKey)
+ .modelName("gpt-4o") // Vision-capable model
+ .build();
+
+// Create agent configuration
+AgentConfiguration configuration = new AgentConfiguration()
+ .withChatModel(chatModel);
+
+Agent visionAgent = new AgentWithoutMemory(configuration);
+context.getRegistry().bind("visionAgent", visionAgent);
+
+// Route to process images
+from("file:inbox/images?noop=true&include=.*\\.(png|jpg|jpeg)")
+ .setHeader("CamelLangChain4jAgentUserMessage",
+ constant("Describe what you see in this image. Be detailed but
concise."))
+ .to("langchain4j-agent:vision?agent=#visionAgent")
+ .log("AI Response: ${body}")
+ .to("file:outbox/descriptions");
+----
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
new file mode 100644
index 000000000000..07ec8ca41e80
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java
@@ -0,0 +1,408 @@
+/*
+ * 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;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Base64;
+
+import dev.langchain4j.data.audio.Audio;
+import dev.langchain4j.data.image.Image;
+import dev.langchain4j.data.message.AudioContent;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.PdfFileContent;
+import dev.langchain4j.data.message.TextContent;
+import dev.langchain4j.data.message.VideoContent;
+import dev.langchain4j.data.pdf.PdfFile;
+import dev.langchain4j.data.video.Video;
+import org.apache.camel.Converter;
+import org.apache.camel.Exchange;
+import org.apache.camel.WrappedFile;
+import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static
org.apache.camel.component.langchain4j.agent.api.Headers.MEDIA_TYPE;
+import static
org.apache.camel.component.langchain4j.agent.api.Headers.MEMORY_ID;
+import static
org.apache.camel.component.langchain4j.agent.api.Headers.SYSTEM_MESSAGE;
+import static
org.apache.camel.component.langchain4j.agent.api.Headers.USER_MESSAGE;
+
+/**
+ * Type converters for the LangChain4j Agent component.
+ * <p>
+ * Provides automatic conversion from various input types to {@link
AiAgentBody} with appropriate LangChain4j
+ * {@link Content} types based on the MIME type.
+ * </p>
+ * <p>
+ * Supported input types:
+ * </p>
+ * <ul>
+ * <li>{@link WrappedFile} - from file, ftp, sftp components</li>
+ * <li>{@code byte[]} - from aws2-s3, azure-storage-blob, and other cloud
components</li>
+ * <li>{@link InputStream} - from various streaming components</li>
+ * </ul>
+ * <p>
+ * <strong>Note:</strong> For {@code byte[]} and {@link InputStream}, the MIME
type must be provided via the
+ * {@code CamelLangChain4jAgentMediaType} header or a component-specific
content type header.
+ * </p>
+ */
+@Converter(generateLoader = true)
+public final class LangChain4jAgentConverter {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(LangChain4jAgentConverter.class);
+
+ private LangChain4jAgentConverter() {
+ }
+
+ /**
+ * Converts a {@link WrappedFile} to an {@link AiAgentBody} with the
appropriate {@link Content} type.
+ * <p>
+ * The conversion uses the following headers from the exchange:
+ * </p>
+ * <ul>
+ * <li>{@code CamelLangChain4jAgentUserMessage} - The text message to
accompany the file content</li>
+ * <li>{@code CamelLangChain4jAgentSystemMessage} - Optional system
message for the AI agent</li>
+ * <li>{@code CamelLangChain4jAgentMemoryId} - Optional memory ID for
stateful conversations</li>
+ * <li>{@code CamelLangChain4jAgentMediaType} - Optional MIME type
override (highest priority)</li>
+ * <li>{@code Exchange.FILE_CONTENT_TYPE} - MIME type from file components
(second priority)</li>
+ * </ul>
+ * <p>
+ * If no MIME type header is found, the type is auto-detected from the
file extension.
+ * </p>
+ *
+ * @param wrappedFile the wrapped file from file-based
components (file, ftp, sftp, etc.)
+ * @param exchange the Camel exchange containing headers
+ * @return an AiAgentBody with the appropriate
Content type
+ * @throws IllegalArgumentException if the file cannot be read or the MIME
type is unsupported
+ */
+ @Converter
+ public static AiAgentBody<?> toAiAgentBody(WrappedFile<?> wrappedFile,
Exchange exchange) {
+ Object fileObj = wrappedFile.getFile();
+ if (fileObj == null) {
+ throw new IllegalArgumentException("WrappedFile contains null
file");
+ }
+ if (!(fileObj instanceof File)) {
+ throw new IllegalArgumentException(
+ "WrappedFile must contain a java.io.File instance, got: "
+ fileObj.getClass().getName());
+ }
+
+ File file = (File) fileObj;
+ String mimeType = detectMimeType(file, exchange);
+ byte[] fileData = readFileBytes(file);
+ Content content = createContent(fileData, mimeType);
+
+ String userMessage = exchange.getIn().getHeader(USER_MESSAGE,
String.class);
+ String systemMessage = exchange.getIn().getHeader(SYSTEM_MESSAGE,
String.class);
+ Object memoryId = exchange.getIn().getHeader(MEMORY_ID);
+
+ AiAgentBody<Content> body = new AiAgentBody<>();
+ body.setUserMessage(userMessage != null ? userMessage : "");
+ body.setSystemMessage(systemMessage);
+ body.setMemoryId(memoryId);
+ body.setContent(content);
+
+ return body;
+ }
+
+ /**
+ * Converts a {@code byte[]} to an {@link AiAgentBody} with the
appropriate {@link Content} type.
+ * <p>
+ * This converter is useful for cloud storage components like aws2-s3,
azure-storage-blob, etc. that return file
+ * content as byte arrays.
+ * </p>
+ * <p>
+ * <strong>Important:</strong> The MIME type must be provided via headers
since it cannot be auto-detected from byte
+ * arrays. Supported headers (in priority order):
+ * </p>
+ * <ul>
+ * <li>{@code CamelLangChain4jAgentMediaType} header (highest
priority)</li>
+ * <li>{@code CamelAwsS3ContentType} header (from AWS S3)</li>
+ * <li>{@code CamelAzureStorageBlobContentType} header (from Azure Blob
Storage)</li>
+ * <li>{@code CamelAzureStorageDataLakeContentType} header (from Azure
Data Lake Storage)</li>
+ * <li>{@code CamelGoogleCloudStorageContentType} header (from Google
Cloud Storage)</li>
+ * <li>{@code CamelMinioContentType} header (from Minio)</li>
+ * <li>{@code CamelIBMCOSContentType} header (from IBM Cloud Object
Storage)</li>
+ * <li>{@code Content-Type} header</li>
+ * <li>{@code CamelFileContentType} header (from file components)</li>
+ * </ul>
+ *
+ * @param data the file content as a byte array
+ * @param exchange the Camel exchange containing headers
+ * @return an AiAgentBody with the appropriate
Content type
+ * @throws IllegalArgumentException if the MIME type is not provided or is
unsupported
+ */
+ @Converter
+ public static AiAgentBody<?> byteArrayToAiAgentBody(byte[] data, Exchange
exchange) {
+ String mimeType = detectMimeTypeFromHeaders(exchange);
+ Content content = createContent(data, mimeType);
+
+ String userMessage = exchange.getIn().getHeader(USER_MESSAGE,
String.class);
+ String systemMessage = exchange.getIn().getHeader(SYSTEM_MESSAGE,
String.class);
+ Object memoryId = exchange.getIn().getHeader(MEMORY_ID);
+
+ AiAgentBody<Content> body = new AiAgentBody<>();
+ body.setUserMessage(userMessage != null ? userMessage : "");
+ body.setSystemMessage(systemMessage);
+ body.setMemoryId(memoryId);
+ body.setContent(content);
+
+ return body;
+ }
+
+ /**
+ * Converts an {@link InputStream} to an {@link AiAgentBody} with the
appropriate {@link Content} type.
+ * <p>
+ * This converter is useful for streaming components that return file
content as input streams.
+ * </p>
+ * <p>
+ * <strong>Important:</strong> The MIME type must be provided via headers
since it cannot be auto-detected from
+ * streams.
+ * </p>
+ *
+ * @param inputStream the file content as an input stream
+ * @param exchange the Camel exchange containing headers
+ * @return an AiAgentBody with the appropriate
Content type
+ * @throws IllegalArgumentException if the stream cannot be read or the
MIME type is not provided/unsupported
+ */
+ @Converter
+ public static AiAgentBody<?> inputStreamToAiAgentBody(InputStream
inputStream, Exchange exchange) {
+ try {
+ byte[] data = inputStream.readAllBytes();
+ return byteArrayToAiAgentBody(data, exchange);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to read input stream",
e);
+ }
+ }
+
+ /**
+ * Creates the appropriate LangChain4j Content object based on the MIME
type.
+ */
+ static Content createContent(byte[] data, String mimeType) {
+ String base64Data = Base64.getEncoder().encodeToString(data);
+
+ if (mimeType.startsWith("image/")) {
+ Image image = Image.builder()
+ .base64Data(base64Data)
+ .mimeType(mimeType)
+ .build();
+ return ImageContent.from(image);
+ } else if (mimeType.startsWith("audio/")) {
+ Audio audio = Audio.builder()
+ .base64Data(base64Data)
+ .mimeType(mimeType)
+ .build();
+ return AudioContent.from(audio);
+ } else if (mimeType.startsWith("video/")) {
+ Video video = Video.builder()
+ .base64Data(base64Data)
+ .mimeType(mimeType)
+ .build();
+ return VideoContent.from(video);
+ } else if ("application/pdf".equals(mimeType)) {
+ PdfFile pdfFile = PdfFile.builder()
+ .base64Data(base64Data)
+ .build();
+ return PdfFileContent.from(pdfFile);
+ } else if (mimeType.startsWith("text/")) {
+ return TextContent.from(new String(data));
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported MIME type: " + mimeType
+ + ". Supported types: image/*,
audio/*, video/*, application/pdf, text/*");
+ }
+ }
+
+ /**
+ * Detects the MIME type from headers or file extension.
+ * <p>
+ * Priority:
+ * </p>
+ * <ol>
+ * <li>CamelLangChain4jAgentMediaType header (highest priority)</li>
+ * <li>Exchange.FILE_CONTENT_TYPE header (from file components)</li>
+ * <li>Auto-detection from file extension</li>
+ * </ol>
+ */
+ private static String detectMimeType(File file, Exchange exchange) {
+ // Check agent-specific header first (highest priority)
+ String mediaType = exchange.getIn().getHeader(MEDIA_TYPE,
String.class);
+ if (mediaType != null) {
+ return mediaType;
+ }
+
+ // Check file component's content type header
+ String fileContentType =
exchange.getIn().getHeader(Exchange.FILE_CONTENT_TYPE, String.class);
+ if (fileContentType != null) {
+ return fileContentType;
+ }
+
+ // Auto-detect from file extension
+ return detectMimeTypeFromExtension(file.getName());
+ }
+
+ /**
+ * Detects the MIME type from headers only (for byte[] and InputStream
where file extension is not available).
+ * <p>
+ * Priority:
+ * </p>
+ * <ol>
+ * <li>CamelLangChain4jAgentMediaType header (highest priority)</li>
+ * <li>Cloud storage component headers (AWS S3, Azure Blob, Google Cloud,
Minio, IBM COS)</li>
+ * <li>Exchange.CONTENT_TYPE header</li>
+ * <li>Exchange.FILE_CONTENT_TYPE header</li>
+ * </ol>
+ *
+ * @throws IllegalArgumentException if no MIME type header is found
+ */
+ private static String detectMimeTypeFromHeaders(Exchange exchange) {
+ // Check agent-specific header first (highest priority)
+ String mediaType = exchange.getIn().getHeader(MEDIA_TYPE,
String.class);
+ if (mediaType != null) {
+ return normalizeContentType(mediaType);
+ }
+
+ // Cloud storage component content type headers
+ String[] cloudContentTypeHeaders = {
+ "CamelAwsS3ContentType", // AWS S3
+ "CamelAzureStorageBlobContentType", // Azure Blob Storage
+ "CamelAzureStorageDataLakeContentType", // Azure Data Lake
Storage
+ "CamelGoogleCloudStorageContentType", // Google Cloud
Storage
+ "CamelMinioContentType", // Minio
(S3-compatible)
+ "CamelIBMCOSContentType" // IBM Cloud Object
Storage
+ };
+
+ for (String header : cloudContentTypeHeaders) {
+ String cloudContentType = exchange.getIn().getHeader(header,
String.class);
+ if (cloudContentType != null) {
+ return normalizeContentType(cloudContentType);
+ }
+ }
+
+ // Check standard content type header
+ String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE,
String.class);
+ if (contentType != null) {
+ return normalizeContentType(contentType);
+ }
+
+ // Check file component's content type header
+ String fileContentType =
exchange.getIn().getHeader(Exchange.FILE_CONTENT_TYPE, String.class);
+ if (fileContentType != null) {
+ return normalizeContentType(fileContentType);
+ }
+
+ throw new IllegalArgumentException(
+ "MIME type is required for byte[] or InputStream input. "
+ + "Please set the
CamelLangChain4jAgentMediaType header.");
+ }
+
+ /**
+ * Normalizes a content type by removing charset and other parameters.
+ * <p>
+ * For example: "text/html; charset=utf-8" becomes "text/html"
+ * </p>
+ */
+ private static String normalizeContentType(String contentType) {
+ int semicolon = contentType.indexOf(';');
+ return semicolon > 0 ? contentType.substring(0, semicolon).trim() :
contentType;
+ }
+
+ /**
+ * Detects the MIME type from the file extension.
+ */
+ private static String detectMimeTypeFromExtension(String fileName) {
+ String lowerName = fileName.toLowerCase();
+
+ // Image formats
+ if (lowerName.endsWith(".png")) {
+ return "image/png";
+ } else if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) {
+ return "image/jpeg";
+ } else if (lowerName.endsWith(".gif")) {
+ return "image/gif";
+ } else if (lowerName.endsWith(".webp")) {
+ return "image/webp";
+ } else if (lowerName.endsWith(".bmp")) {
+ return "image/bmp";
+ } else if (lowerName.endsWith(".tiff") || lowerName.endsWith(".tif")) {
+ return "image/tiff";
+ } else if (lowerName.endsWith(".svg")) {
+ return "image/svg+xml";
+ }
+ // Video formats
+ else if (lowerName.endsWith(".mp4")) {
+ return "video/mp4";
+ } else if (lowerName.endsWith(".webm")) {
+ return "video/webm";
+ } else if (lowerName.endsWith(".mov")) {
+ return "video/quicktime";
+ } else if (lowerName.endsWith(".mkv")) {
+ return "video/x-matroska";
+ } else if (lowerName.endsWith(".avi")) {
+ return "video/x-msvideo";
+ }
+ // Audio formats
+ else if (lowerName.endsWith(".wav")) {
+ return "audio/wav";
+ } else if (lowerName.endsWith(".mp3")) {
+ return "audio/mpeg";
+ } else if (lowerName.endsWith(".ogg")) {
+ return "audio/ogg";
+ } else if (lowerName.endsWith(".m4a")) {
+ return "audio/mp4";
+ } else if (lowerName.endsWith(".flac")) {
+ return "audio/flac";
+ }
+ // Document formats
+ else if (lowerName.endsWith(".pdf")) {
+ return "application/pdf";
+ }
+ // Text formats
+ else if (lowerName.endsWith(".txt")) {
+ return "text/plain";
+ } else if (lowerName.endsWith(".csv")) {
+ return "text/csv";
+ } else if (lowerName.endsWith(".html") || lowerName.endsWith(".htm")) {
+ return "text/html";
+ } else if (lowerName.endsWith(".md")) {
+ return "text/markdown";
+ } else if (lowerName.endsWith(".xml")) {
+ return "text/xml";
+ } else if (lowerName.endsWith(".json")) {
+ return "application/json";
+ }
+
+ LOG.warn("Could not detect MIME type from file extension: {}. Please
set the CamelLangChain4jAgentMediaType header.",
+ fileName);
+ throw new IllegalArgumentException(
+ "Cannot determine MIME type for file: " + fileName
+ + ". Please set the
CamelLangChain4jAgentMediaType header.");
+ }
+
+ /**
+ * Reads the file content as a byte array.
+ */
+ private static byte[] readFileBytes(File file) {
+ try {
+ return Files.readAllBytes(file.toPath());
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to read file: " +
file.getAbsolutePath(), e);
+ }
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
index bfeca686642d..af2698e6cc35 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
@@ -31,11 +31,9 @@ import
dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import org.apache.camel.component.langchain4j.agent.BaseLangChain4jAgent;
import org.apache.camel.test.infra.ollama.services.OllamaService;
import org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
-import org.junit.jupiter.api.extension.RegisterExtension;
public abstract class AbstractRAGIT extends BaseLangChain4jAgent {
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentCustomToolsIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentCustomToolsIT.java
index edf8ad1b6d6e..43e72c96c0a8 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentCustomToolsIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentCustomToolsIT.java
@@ -33,7 +33,6 @@ import
org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -50,7 +49,6 @@ public class LangChain4jAgentCustomToolsIT extends
CamelTestSupport {
protected ChatModel chatModel;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
index c09d6946b073..98fc914c8e8c 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
@@ -33,7 +33,6 @@ import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -45,7 +44,6 @@ public class LangChain4jAgentGuardrailsIntegrationIT extends
CamelTestSupport {
protected ChatModel chatModel;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpToolsIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpToolsIT.java
index 2ad87bfddc59..6fff04f75f7a 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpToolsIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMcpToolsIT.java
@@ -38,7 +38,6 @@ import
org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -64,7 +63,6 @@ public class LangChain4jAgentMcpToolsIT extends
CamelTestSupport {
protected ChatModel chatModel;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMixedToolsIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMixedToolsIT.java
index b2e73760ad59..3ce1841e466d 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMixedToolsIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMixedToolsIT.java
@@ -32,7 +32,6 @@ import
org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -53,7 +52,6 @@ public class LangChain4jAgentMixedToolsIT extends
CamelTestSupport {
protected ChatModel chatModel;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMultimodalityIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMultimodalityIT.java
new file mode 100644
index 000000000000..a2ac1014d928
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentMultimodalityIT.java
@@ -0,0 +1,168 @@
+/*
+ * 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.integration;
+
+import java.io.InputStream;
+import java.util.Base64;
+import java.util.List;
+
+import dev.langchain4j.data.image.Image;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.TextContent;
+import dev.langchain4j.model.chat.ChatModel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.api.Agent;
+import org.apache.camel.component.langchain4j.agent.api.AgentConfiguration;
+import org.apache.camel.component.langchain4j.agent.api.AgentWithoutMemory;
+import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.infra.ollama.services.OllamaService;
+import org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration tests for multimodal content support in the LangChain4j Agent
component. Tests the ability to send both
+ * TextContent and ImageContent to AI models.
+ */
+@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
disabledReason = "Requires too much network resources")
+public class LangChain4jAgentMultimodalityIT extends CamelTestSupport {
+
+ private static final String TEST_IMAGE_PATH = "camel-logo.png";
+
+ protected ChatModel chatModel;
+
+ static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
+ ? null
+ : OllamaServiceFactory.createSingletonService();
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+
+ chatModel = OLLAMA != null ? ModelHelper.loadChatModel(OLLAMA) :
ModelHelper.loadFromEnv();
+ }
+
+ /**
+ * Tests sending a message with TextContent. This validates that the
Content parameter works correctly with simple
+ * text content.
+ */
+ @Test
+ void testTextContent() throws InterruptedException {
+ MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response",
MockEndpoint.class);
+ mockEndpoint.expectedMessageCount(1);
+
+ TextContent textContent = TextContent.from("This is additional context
about Apache Camel integration framework.");
+
+ AiAgentBody<TextContent> body = new AiAgentBody<TextContent>()
+ .withUserMessage("What can you tell me about the text I
provided?")
+ .withContent(textContent);
+
+ String response = template.requestBody("direct:multimodal-agent",
body, String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response, "AI response should not be null");
+ assertTrue(response.length() > 0, "AI response should not be empty");
+ assertTrue(response.contains("Camel"), "AI response should contain
Camel " + response);
+ }
+
+ /**
+ * Tests sending a message with ImageContent. This validates that the
agent can process image content for
+ * vision-capable models.
+ */
+ @Test
+ void testImageContent() throws Exception {
+ MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response",
MockEndpoint.class);
+ mockEndpoint.expectedMessageCount(1);
+
+ // Load the test image
+ byte[] imageBytes;
+ try (InputStream is =
getClass().getClassLoader().getResourceAsStream(TEST_IMAGE_PATH)) {
+ if (is == null) {
+ throw new IllegalStateException("Test image not found: " +
TEST_IMAGE_PATH);
+ }
+ imageBytes = is.readAllBytes();
+ }
+
+ // Create ImageContent from base64-encoded image
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+ Image image = Image.builder()
+ .base64Data(base64Image)
+ .mimeType("image/png")
+ .build();
+ ImageContent imageContent = ImageContent.from(image);
+
+ AiAgentBody<ImageContent> body = new AiAgentBody<ImageContent>()
+ .withUserMessage("What do you see in this image? Describe it
briefly.")
+ .withContent(imageContent);
+
+ String response = template.requestBody("direct:multimodal-agent",
body, String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response, "AI response should not be null");
+ assertTrue(response.length() > 0, "AI response should not be empty");
+ assertTrue(response.contains("Camel"), "AI response should contain
Camel " + response);
+ }
+
+ /**
+ * Tests sending a message with TextContent and a system message. This
validates that the Content parameter works
+ * correctly alongside system messages.
+ */
+ @Test
+ void testTextContentWithSystemMessage() throws InterruptedException {
+ MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response",
MockEndpoint.class);
+ mockEndpoint.expectedMessageCount(1);
+
+ TextContent textContent = TextContent.from("Apache Camel is an
open-source integration framework.");
+
+ AiAgentBody<TextContent> body = new AiAgentBody<TextContent>()
+ .withUserMessage("Summarize the provided text in one
sentence.")
+ .withSystemMessage("You are a technical documentation
assistant. Be concise and accurate.")
+ .withContent(textContent);
+
+ String response = template.requestBody("direct:multimodal-agent",
body, String.class);
+
+ mockEndpoint.assertIsSatisfied();
+ assertNotNull(response, "AI response should not be null");
+ assertTrue(response.length() > 0, "AI response should not be empty");
+ assertTrue(response.contains("Camel"), "AI response should contain
Camel " + response);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ AgentConfiguration configuration = new AgentConfiguration()
+ .withChatModel(chatModel)
+ .withInputGuardrailClasses(List.of())
+ .withOutputGuardrailClasses(List.of());
+
+ Agent multimodalAgent = new AgentWithoutMemory(configuration);
+
+ this.context.getRegistry().bind("multimodalAgent", multimodalAgent);
+
+ return new RouteBuilder() {
+ public void configure() {
+ from("direct:multimodal-agent")
+
.to("langchain4j-agent:multimodal?agent=#multimodalAgent")
+ .to("mock:response");
+ }
+ };
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
index e906a706eecb..281c02228a98 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
@@ -34,7 +34,6 @@ import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import static
org.apache.camel.component.langchain4j.agent.api.Headers.MEMORY_ID;
import static
org.apache.camel.component.langchain4j.agent.api.Headers.SYSTEM_MESSAGE;
@@ -53,7 +52,6 @@ public class LangChain4jAgentWithMemoryIT extends
CamelTestSupport {
protected ChatMemoryProvider chatMemoryProvider;
private PersistentChatMemoryStore store;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
index 46a3c78cccbf..637fa752889b 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
@@ -30,7 +30,6 @@ import
org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -45,7 +44,6 @@ public class LangChain4jAgentWithToolsIT extends
CamelTestSupport {
protected ChatModel chatModel;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWrappedFileIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWrappedFileIT.java
new file mode 100644
index 000000000000..b2bff35d4157
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWrappedFileIT.java
@@ -0,0 +1,148 @@
+/*
+ * 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.integration;
+
+import java.net.URL;
+import java.util.List;
+
+import dev.langchain4j.model.chat.ChatModel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.api.Agent;
+import org.apache.camel.component.langchain4j.agent.api.AgentConfiguration;
+import org.apache.camel.component.langchain4j.agent.api.AgentWithoutMemory;
+import org.apache.camel.component.langchain4j.agent.api.Headers;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration tests for WrappedFile support in the LangChain4j Agent
component. Tests the ability to process files from
+ * the file component directly, with automatic Content type conversion based
on MIME type.
+ * <p>
+ * Requires environment variables to be set:
+ * <ul>
+ * <li>API_KEY - The API key for the LLM provider</li>
+ * <li>MODEL_PROVIDER - The provider name (e.g., "openai", "gemini",
"ollama")</li>
+ * <li>MODEL_BASE_URL - (Optional) Custom base URL for OpenAI-compatible
endpoints</li>
+ * <li>MODEL_NAME - (Optional) Custom model name</li>
+ * </ul>
+ */
+@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
disabledReason = "Requires too much network resources")
+public class LangChain4jAgentWrappedFileIT extends CamelTestSupport {
+
+ private static final String IMAGE_ROUTE_ID = "image-route";
+ private static final String PDF_ROUTE_ID = "pdf-route";
+
+ protected ChatModel chatModel;
+ private String resourcesPath;
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+
+ if (!ModelHelper.hasEnvironmentConfiguration()) {
+ throw new IllegalStateException(
+ "This test requires environment variables: API_KEY,
MODEL_PROVIDER. "
+ + "Optionally: MODEL_BASE_URL,
MODEL_NAME");
+ }
+
+ chatModel = ModelHelper.loadFromEnv();
+
+ // Get the path to test resources
+ URL resourceUrl =
getClass().getClassLoader().getResource("camel-logo.png");
+ assertNotNull(resourceUrl, "Test resources not found");
+ resourcesPath = resourceUrl.getPath().replace("/camel-logo.png", "");
+ }
+
+ /**
+ * Tests that an image file from the file component is automatically
converted to ImageContent and processed by the
+ * agent.
+ */
+ @Test
+ void testImageFileFromFileComponent() throws Exception {
+ // Start only the image route
+ context.getRouteController().startRoute(IMAGE_ROUTE_ID);
+
+ MockEndpoint mockEndpoint =
this.context.getEndpoint("mock:image-response", MockEndpoint.class);
+ mockEndpoint.expectedMessageCount(1);
+
+ // Wait for the file to be processed
+ mockEndpoint.assertIsSatisfied(60000);
+
+ // Verify the response
+ String response =
mockEndpoint.getExchanges().get(0).getIn().getBody(String.class);
+ assertNotNull(response, "Response should not be null");
+ assertTrue(response.length() > 0, "Response should not be empty");
+ }
+
+ /**
+ * Tests that a PDF file from the file component is automatically
converted to PdfFileContent and processed by the
+ * agent.
+ */
+ @Test
+ void testPdfFileFromFileComponent() throws Exception {
+ // Start only the PDF route
+ context.getRouteController().startRoute(PDF_ROUTE_ID);
+
+ MockEndpoint mockEndpoint =
this.context.getEndpoint("mock:pdf-response", MockEndpoint.class);
+ mockEndpoint.expectedMessageCount(1);
+
+ // Wait for the file to be processed
+ mockEndpoint.assertIsSatisfied(60000);
+
+ // Verify the response
+ String response =
mockEndpoint.getExchanges().get(0).getIn().getBody(String.class);
+ assertNotNull(response, "Response should not be null");
+ assertTrue(response.length() > 0, "Response should not be empty");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ AgentConfiguration configuration = new AgentConfiguration()
+ .withChatModel(chatModel)
+ .withInputGuardrailClasses(List.of())
+ .withOutputGuardrailClasses(List.of());
+
+ Agent agent = new AgentWithoutMemory(configuration);
+
+ this.context.getRegistry().bind("fileAgent", agent);
+
+ return new RouteBuilder() {
+ public void configure() {
+ // Route for processing image files from resources folder
(starts stopped)
+ from("file:" + resourcesPath + "?noop=true&include=.*\\.png")
+ .routeId(IMAGE_ROUTE_ID)
+ .autoStartup(false)
+ .setHeader(Headers.USER_MESSAGE, constant("What do you
see in this image? Describe it briefly."))
+ .to("langchain4j-agent:describe?agent=#fileAgent")
+ .to("mock:image-response");
+
+ // Route for processing PDF files from resources folder
(starts stopped)
+ from("file:" + resourcesPath + "?noop=true&include=.*\\.pdf")
+ .routeId(PDF_ROUTE_ID)
+ .autoStartup(false)
+ .setHeader(Headers.USER_MESSAGE, constant("What is
this document about? Summarize it briefly."))
+ .to("langchain4j-agent:describe?agent=#fileAgent")
+ .to("mock:pdf-response");
+ }
+ };
+ }
+}
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
index 5e0cad56d308..fead99f99156 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
@@ -30,7 +30,6 @@ import
org.apache.camel.test.infra.ollama.services.OllamaServiceFactory;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
-import org.junit.jupiter.api.extension.RegisterExtension;
import static
org.apache.camel.component.langchain4j.agent.api.Headers.SYSTEM_MESSAGE;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -51,7 +50,6 @@ public class LangChain4jSimpleAgentIT extends
CamelTestSupport {
protected ChatModel chatModel;
- @RegisterExtension
static OllamaService OLLAMA = ModelHelper.hasEnvironmentConfiguration()
? null
: OllamaServiceFactory.createSingletonService();
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/ModelHelper.java
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/ModelHelper.java
index 3b711c6e8df4..04379b647141 100644
---
a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/ModelHelper.java
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/ModelHelper.java
@@ -43,7 +43,7 @@ public class ModelHelper {
protected static ChatModel createGeminiModel(String apiKey) {
return GoogleAiGeminiChatModel.builder()
.apiKey(apiKey)
- .modelName("gemini-2.5-flash")
+ .modelName("gemini-2.5-flash-lite")
.temperature(1.0)
.timeout(ofSeconds(60))
.logRequestsAndResponses(true)
@@ -51,14 +51,28 @@ public class ModelHelper {
}
protected static ChatModel createOpenAiModel(String apiKey) {
- return OpenAiChatModel.builder()
+ OpenAiChatModel.OpenAiChatModelBuilder builder =
OpenAiChatModel.builder()
.apiKey(apiKey)
- .modelName(OpenAiChatModelName.GPT_4_O_MINI)
.temperature(1.0)
.timeout(ofSeconds(60))
.logRequests(true)
- .logResponses(true)
- .build();
+ .logResponses(true);
+
+ // Support custom base URL for OpenAI-compatible endpoints
+ String baseUrl = System.getenv(MODEL_BASE_URL);
+ if (baseUrl != null && !baseUrl.trim().isEmpty()) {
+ builder.baseUrl(baseUrl);
+ }
+
+ // Support custom model name, default to GPT-4o-mini
+ String modelName = System.getenv(MODEL_NAME);
+ if (modelName != null && !modelName.trim().isEmpty()) {
+ builder.modelName(modelName);
+ } else {
+ builder.modelName(OpenAiChatModelName.GPT_4_O_MINI);
+ }
+
+ return builder.build();
}
protected static ChatModel createExternalChatModel(String name, String
apiKey) {
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/resources/camel-logo.png
b/components/camel-ai/camel-langchain4j-agent/src/test/resources/camel-logo.png
new file mode 100644
index 000000000000..e4029249e6c4
Binary files /dev/null and
b/components/camel-ai/camel-langchain4j-agent/src/test/resources/camel-logo.png
differ
diff --git
a/components/camel-ai/camel-langchain4j-agent/src/test/resources/test-document.pdf
b/components/camel-ai/camel-langchain4j-agent/src/test/resources/test-document.pdf
new file mode 100644
index 000000000000..3cedc4da23fb
--- /dev/null
+++
b/components/camel-ai/camel-langchain4j-agent/src/test/resources/test-document.pdf
@@ -0,0 +1,20 @@
+%PDF-1.4
+1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
+2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
+3 0 obj << /Type /Page /Parent 2 0 R /Resources << /Font << /F1 4 0 R >> >>
/MediaBox [0 0 612 792] /Contents 5 0 R >> endobj
+4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj
+5 0 obj << /Length 44 >> stream
+BT /F1 12 Tf 100 700 Td (Test PDF) Tj ET
+endstream endobj
+xref
+0 6
+0000000000 65535 f
+0000000009 00000 n
+0000000058 00000 n
+0000000115 00000 n
+0000000261 00000 n
+0000000330 00000 n
+trailer << /Size 6 /Root 1 0 R >>
+startxref
+422
+%%EOF
diff --git a/components/camel-ai/camel-langchain4j-agent/test-execution.md
b/components/camel-ai/camel-langchain4j-agent/test-execution.md
index 4402c014b932..ad514ecc889c 100644
--- a/components/camel-ai/camel-langchain4j-agent/test-execution.md
+++ b/components/camel-ai/camel-langchain4j-agent/test-execution.md
@@ -4,7 +4,7 @@
If ollama is already installed on the system execute the test with
```bash
-mvn verify -Dollama.endpoint=http://localhost:11434/
-Dollama.model=granite4:tiny-h -Dollama.instance.type=remote
+mvn verify -Dollama.endpoint=http://localhost:11434/
-Dollama.model=granite4:3b -Dollama.instance.type=remote
```
The Ollama docker image is really slow on macbook without nvidia hardware
acceleration
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
index 1ce4c56ae4a4..5bf843a02cf4 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jAgentEndpointBuilderFactory.java
@@ -270,6 +270,32 @@ public interface LangChain4jAgentEndpointBuilderFactory {
public String langChain4jAgentMemoryId() {
return "CamelLangChain4jAgentMemoryId";
}
+ /**
+ * The user message to accompany file content when using WrappedFile as
+ * input.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code LangChain4jAgentUserMessage}.
+ */
+ public String langChain4jAgentUserMessage() {
+ return "CamelLangChain4jAgentUserMessage";
+ }
+ /**
+ * The media type (MIME type) of the file content. Overrides
+ * auto-detection from file extension.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code LangChain4jAgentMediaType}.
+ */
+ public String langChain4jAgentMediaType() {
+ return "CamelLangChain4jAgentMediaType";
+ }
}
static LangChain4jAgentEndpointBuilder endpointBuilder(String
componentName, String path) {
class LangChain4jAgentEndpointBuilderImpl extends
AbstractEndpointBuilder implements LangChain4jAgentEndpointBuilder,
AdvancedLangChain4jAgentEndpointBuilder {
diff --git
a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestContext.java
b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestContext.java
index 93eab20d9f86..7ea25229f919 100644
---
a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestContext.java
+++
b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestContext.java
@@ -51,13 +51,39 @@ public class RequestContext {
String role = messageNode.path("role").asText();
if ("user".equals(role)) {
- return messageNode.path("content").asText();
+ return extractContentText(messageNode.path("content"));
}
}
return null;
}
+ /**
+ * Extracts text content from either a plain string or an array of content
parts. Supports formats: - Simple string:
+ * "Hello" - Array of content parts: [{"type": "text", "text": "Hello"}]
+ */
+ private String extractContentText(JsonNode contentNode) {
+ if (contentNode.isTextual()) {
+ return contentNode.asText();
+ }
+
+ if (contentNode.isArray()) {
+ StringBuilder textBuilder = new StringBuilder();
+ for (JsonNode part : contentNode) {
+ String type = part.path("type").asText();
+ if ("text".equals(type)) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append(" ");
+ }
+ textBuilder.append(part.path("text").asText());
+ }
+ }
+ return textBuilder.length() > 0 ? textBuilder.toString() : null;
+ }
+
+ return null;
+ }
+
public JsonNode getMessagesNode() {
return messagesNode;
}