Skip to content

Commit 1cf2c48

Browse files
authored
Camel 22510 avoid leaking internal data types (#21704)
* Implementation to avoid leaking internal data types. * Implementation to avoid leaking internal data types. * Implementation to avoid leaking internal data types. * Implementation to avoid leaking internal data types. * Fixed review comments. * Fixed review comments. * Formatting issues fixed. * More review comments fixed. * Fix for failing tests.
1 parent 6a7faa3 commit 1cf2c48

5 files changed

Lines changed: 234 additions & 40 deletions

File tree

components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/Agent.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,19 @@ public interface Agent {
8282
* request body regardless of how the original message was formatted.
8383
* </p>
8484
*
85-
* @param messagePayload the message payload from the exchange body; must be either an
86-
* {@link AiAgentBody} or a {@link String}
87-
* @param exchange the Camel exchange containing headers and context information
88-
* @return an {@link AiAgentBody} instance ready for agent processing; returns the
89-
* original payload if it's already an {@link AiAgentBody}, or creates a new
90-
* one from a string payload and relevant headers
91-
* @throws InvalidPayloadRuntimeException if the payload is neither an {@link AiAgentBody} nor a {@link String}
92-
* @throws Exception if any other error occurs during payload processing
85+
* @param messagePayload the message payload from the exchange body; must be either an
86+
* {@link AiAgentBody} or a {@link String}
87+
* @param exchange the Camel exchange containing headers and context information
88+
* @return an {@link AiAgentBody} instance ready for agent processing; returns
89+
* the original payload if it's already an {@link AiAgentBody}, or
90+
* creates a new one from a string payload and relevant headers
91+
* @throws InvalidPayloadRuntimeException if the payload is neither an {@link AiAgentBody} nor a {@link String}
92+
* @throws Exception if any other error occurs during payload processing
93+
*
94+
* @deprecated This method is no longer used by {@code LangChain4jAgentProducer}.
95+
* Body conversion is now handled via Camel TypeConverters.
9396
*/
97+
@Deprecated(since = "4.19.0")
9498
default AiAgentBody<?> processBody(Object messagePayload, Exchange exchange) throws Exception {
9599
if (messagePayload instanceof AiAgentBody<?> payload) {
96100
return payload;

components/camel-ai/camel-langchain4j-agent/src/generated/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverterLoader.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ private void registerConverters(TypeConverterRegistry registry) {
6060
}
6161
return answer;
6262
});
63+
addTypeConverter(registry, org.apache.camel.component.langchain4j.agent.api.AiAgentBody.class, java.lang.String.class, false,
64+
(type, exchange, value) -> {
65+
Object answer = org.apache.camel.component.langchain4j.agent.LangChain4jAgentConverter.textToAiAgentBody((java.lang.String) value, exchange);
66+
if (false && answer == null) {
67+
answer = Void.class;
68+
}
69+
return answer;
70+
});
6371
addTypeConverter(registry, org.apache.camel.component.langchain4j.agent.api.AiAgentBody.class, org.apache.camel.WrappedFile.class, false,
6472
(type, exchange, value) -> {
6573
Object answer = org.apache.camel.component.langchain4j.agent.LangChain4jAgentConverter.toAiAgentBody((org.apache.camel.WrappedFile) value, exchange);

components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentConverter.java

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.InputStream;
22+
import java.net.URLConnection;
2223
import java.nio.file.Files;
2324
import java.util.Base64;
2425

@@ -34,6 +35,7 @@
3435
import dev.langchain4j.data.video.Video;
3536
import org.apache.camel.Converter;
3637
import org.apache.camel.Exchange;
38+
import org.apache.camel.Message;
3739
import org.apache.camel.WrappedFile;
3840
import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
3941
import org.slf4j.Logger;
@@ -108,17 +110,7 @@ public static AiAgentBody<?> toAiAgentBody(WrappedFile<?> wrappedFile, Exchange
108110
byte[] fileData = readFileBytes(file);
109111
Content content = createContent(fileData, mimeType);
110112

111-
String userMessage = exchange.getIn().getHeader(USER_MESSAGE, String.class);
112-
String systemMessage = exchange.getIn().getHeader(SYSTEM_MESSAGE, String.class);
113-
Object memoryId = exchange.getIn().getHeader(MEMORY_ID);
114-
115-
AiAgentBody<Content> body = new AiAgentBody<>();
116-
body.setUserMessage(userMessage != null ? userMessage : "");
117-
body.setSystemMessage(systemMessage);
118-
body.setMemoryId(memoryId);
119-
body.setContent(content);
120-
121-
return body;
113+
return buildAiAgentBody(exchange, content, "");
122114
}
123115

124116
/**
@@ -153,17 +145,7 @@ public static AiAgentBody<?> byteArrayToAiAgentBody(byte[] data, Exchange exchan
153145
String mimeType = detectMimeTypeFromHeaders(exchange);
154146
Content content = createContent(data, mimeType);
155147

156-
String userMessage = exchange.getIn().getHeader(USER_MESSAGE, String.class);
157-
String systemMessage = exchange.getIn().getHeader(SYSTEM_MESSAGE, String.class);
158-
Object memoryId = exchange.getIn().getHeader(MEMORY_ID);
159-
160-
AiAgentBody<Content> body = new AiAgentBody<>();
161-
body.setUserMessage(userMessage != null ? userMessage : "");
162-
body.setSystemMessage(systemMessage);
163-
body.setMemoryId(memoryId);
164-
body.setContent(content);
165-
166-
return body;
148+
return buildAiAgentBody(exchange, content, "");
167149
}
168150

169151
/**
@@ -191,6 +173,21 @@ public static AiAgentBody<?> inputStreamToAiAgentBody(InputStream inputStream, E
191173
}
192174
}
193175

176+
/**
177+
* Converts a {@link String} to an {@link AiAgentBody} with the appropriate {@link Content} type.
178+
* <p>
179+
* This converter is useful for the text components that return Text Body.
180+
* </p>
181+
*
182+
* @param text String as message
183+
* @param exchange the Camel exchange containing headers
184+
* @return an AiAgentBody with the appropriate Content type
185+
*/
186+
@Converter
187+
public static AiAgentBody<?> textToAiAgentBody(String text, Exchange exchange) {
188+
return buildAiAgentBody(exchange, null, text);
189+
}
190+
194191
/**
195192
* Creates the appropriate LangChain4j Content object based on the MIME type.
196193
*/
@@ -242,13 +239,13 @@ static Content createContent(byte[] data, String mimeType) {
242239
*/
243240
private static String detectMimeType(File file, Exchange exchange) {
244241
// Check agent-specific header first (highest priority)
245-
String mediaType = exchange.getIn().getHeader(MEDIA_TYPE, String.class);
242+
String mediaType = exchange.getMessage().getHeader(MEDIA_TYPE, String.class);
246243
if (mediaType != null) {
247244
return mediaType;
248245
}
249246

250247
// Check file component's content type header
251-
String fileContentType = exchange.getIn().getHeader(Exchange.FILE_CONTENT_TYPE, String.class);
248+
String fileContentType = exchange.getMessage().getHeader(Exchange.FILE_CONTENT_TYPE, String.class);
252249
if (fileContentType != null) {
253250
return fileContentType;
254251
}
@@ -273,11 +270,12 @@ private static String detectMimeType(File file, Exchange exchange) {
273270
*/
274271
private static String detectMimeTypeFromHeaders(Exchange exchange) {
275272
// Check agent-specific header first (highest priority)
276-
String mediaType = exchange.getIn().getHeader(MEDIA_TYPE, String.class);
273+
Message message = exchange.getMessage();
274+
275+
String mediaType = message.getHeader(MEDIA_TYPE, String.class);
277276
if (mediaType != null) {
278277
return normalizeContentType(mediaType);
279278
}
280-
281279
// Cloud storage component content type headers
282280
String[] cloudContentTypeHeaders = {
283281
"CamelAwsS3ContentType", // AWS S3
@@ -289,24 +287,30 @@ private static String detectMimeTypeFromHeaders(Exchange exchange) {
289287
};
290288

291289
for (String header : cloudContentTypeHeaders) {
292-
String cloudContentType = exchange.getIn().getHeader(header, String.class);
290+
String cloudContentType = message.getHeader(header, String.class);
293291
if (cloudContentType != null) {
294292
return normalizeContentType(cloudContentType);
295293
}
296294
}
297-
298295
// Check standard content type header
299-
String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class);
296+
String contentType = message.getHeader(Exchange.CONTENT_TYPE, String.class);
300297
if (contentType != null) {
301298
return normalizeContentType(contentType);
302299
}
303-
304300
// Check file component's content type header
305-
String fileContentType = exchange.getIn().getHeader(Exchange.FILE_CONTENT_TYPE, String.class);
301+
String fileContentType = message.getHeader(Exchange.FILE_CONTENT_TYPE, String.class);
306302
if (fileContentType != null) {
307303
return normalizeContentType(fileContentType);
308304
}
309305

306+
String fileName = message.getHeader(Exchange.FILE_NAME, String.class);
307+
if (fileName != null) {
308+
String mime = URLConnection.guessContentTypeFromName(fileName);
309+
if (mime != null) {
310+
return normalizeContentType(mime);
311+
}
312+
}
313+
310314
throw new IllegalArgumentException(
311315
"MIME type is required for byte[] or InputStream input. "
312316
+ "Please set the CamelLangChain4jAgentMediaType header.");
@@ -405,4 +409,31 @@ private static byte[] readFileBytes(File file) {
405409
throw new IllegalArgumentException("Failed to read file: " + file.getAbsolutePath(), e);
406410
}
407411
}
412+
413+
/**
414+
* Utility method to build agent body.
415+
*
416+
* @param exchange the Camel exchange containing message headers
417+
* @param content the LangChain4j content to attach to the agent body
418+
* @param defaultUserMessage the fallback user message if the corresponding header is absent
419+
* @return a fully populated {@link AiAgentBody} instance
420+
* @param <T> the type of LangChain4j {@link Content}
421+
*/
422+
private static <
423+
T extends Content> AiAgentBody<T> buildAiAgentBody(Exchange exchange, T content, String defaultUserMessage) {
424+
425+
Message message = exchange.getMessage();
426+
427+
String userMessage = message.getHeader(USER_MESSAGE, String.class);
428+
String systemMessage = message.getHeader(SYSTEM_MESSAGE, String.class);
429+
Object memoryId = message.getHeader(MEMORY_ID);
430+
431+
AiAgentBody<T> body = new AiAgentBody<>();
432+
body.setUserMessage(userMessage != null ? userMessage : defaultUserMessage);
433+
body.setSystemMessage(systemMessage);
434+
body.setMemoryId(memoryId);
435+
body.setContent(content);
436+
437+
return body;
438+
}
408439
}

components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void process(Exchange exchange) throws Exception {
7979
agent = agentFactory.createAgent(exchange);
8080
}
8181

82-
AiAgentBody<?> aiAgentBody = agent.processBody(messagePayload, exchange);
82+
AiAgentBody<?> aiAgentBody = exchange.getMessage().getMandatoryBody(AiAgentBody.class);
8383

8484
ToolProvider toolProvider = createComposedToolProvider(tags, exchange);
8585
String response = agent.chat(aiAgentBody, toolProvider);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.component.langchain4j.agent.integration;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.File;
21+
import java.nio.file.Files;
22+
23+
import org.apache.camel.Exchange;
24+
import org.apache.camel.RoutesBuilder;
25+
import org.apache.camel.builder.RouteBuilder;
26+
import org.apache.camel.component.file.GenericFile;
27+
import org.apache.camel.component.langchain4j.agent.api.Agent;
28+
import org.apache.camel.component.langchain4j.agent.api.AiAgentBody;
29+
import org.apache.camel.component.mock.MockEndpoint;
30+
import org.apache.camel.test.junit6.CamelTestSupport;
31+
import org.junit.jupiter.api.Test;
32+
33+
import static org.junit.jupiter.api.Assertions.assertNotNull;
34+
import static org.junit.jupiter.api.Assertions.assertThrows;
35+
36+
public class LangChain4jAgentAutoConversionTest extends CamelTestSupport {
37+
38+
@Override
39+
protected RoutesBuilder createRouteBuilder() throws Exception {
40+
41+
Agent mockAgent = (body, exchange) -> "Processed";
42+
43+
context.getRegistry().bind("mockAgent", mockAgent);
44+
45+
return new RouteBuilder() {
46+
public void configure() {
47+
from("direct:start")
48+
.to("langchain4j-agent:test?agent=#mockAgent")
49+
.to("mock:result");
50+
}
51+
};
52+
53+
}
54+
55+
@Test
56+
void shouldAutoConvertPlainString() throws Exception {
57+
MockEndpoint mock = getMockEndpoint("mock:result");
58+
mock.expectedMessageCount(1);
59+
60+
template.sendBody("direct:start", "Hello world");
61+
62+
mock.assertIsSatisfied();
63+
64+
String response = mock.getExchanges().get(0)
65+
.getMessage()
66+
.getMandatoryBody(String.class);
67+
68+
assertNotNull(response);
69+
}
70+
71+
@Test
72+
void shouldAutoConvertInputStream() throws Exception {
73+
MockEndpoint mock = getMockEndpoint("mock:result");
74+
mock.expectedMessageCount(1);
75+
context.setStreamCaching(true);
76+
template.send("direct:start", exchange -> {
77+
exchange.getMessage().setBody(
78+
new ByteArrayInputStream("Hello stream".getBytes()));
79+
exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, "text/plain");
80+
});
81+
82+
mock.assertIsSatisfied();
83+
AiAgentBody<?> body = mock.getExchanges().get(0)
84+
.getMessage()
85+
.getBody(AiAgentBody.class);
86+
87+
assertNotNull(body);
88+
}
89+
90+
@Test
91+
void shouldConvertWrappedFile() throws Exception {
92+
Exchange exchange = context.getEndpoint("direct:test").createExchange();
93+
94+
File file = File.createTempFile("camel", ".txt");
95+
Files.writeString(file.toPath(), "Hello file");
96+
97+
GenericFile<File> genericFile = new GenericFile<>();
98+
genericFile.setFile(file);
99+
genericFile.setFileName(file.getName());
100+
101+
AiAgentBody<?> body = context.getTypeConverter()
102+
.convertTo(AiAgentBody.class, exchange, genericFile);
103+
104+
assertNotNull(body);
105+
}
106+
107+
@Test
108+
void shouldConvertByteArray() {
109+
Exchange exchange = context.getEndpoint("direct:test").createExchange();
110+
111+
exchange.getMessage().setHeader(
112+
"CamelLangChain4jAgentMediaType",
113+
"text/plain");
114+
115+
AiAgentBody<?> body = context.getTypeConverter()
116+
.convertTo(AiAgentBody.class, exchange, "Hello".getBytes());
117+
118+
assertNotNull(body);
119+
}
120+
121+
@Test
122+
void shouldConvertInputStream() {
123+
Exchange exchange = context.getEndpoint("direct:test").createExchange();
124+
125+
exchange.getMessage().setHeader(
126+
"CamelLangChain4jAgentMediaType",
127+
"text/plain");
128+
129+
ByteArrayInputStream stream = new ByteArrayInputStream("Hello".getBytes());
130+
131+
AiAgentBody<?> body = context.getTypeConverter()
132+
.convertTo(AiAgentBody.class, exchange, stream);
133+
134+
assertNotNull(body);
135+
}
136+
137+
@Test
138+
void shouldFailForUnsupportedMimeType() {
139+
Exchange exchange = context.getEndpoint("direct:test").createExchange();
140+
141+
exchange.getMessage().setHeader(
142+
"CamelLangChain4jAgentMediaType",
143+
"application/zip");
144+
145+
assertThrows(
146+
org.apache.camel.TypeConversionException.class,
147+
() -> context.getTypeConverter()
148+
.convertTo(AiAgentBody.class, exchange, "data".getBytes()));
149+
}
150+
151+
}

0 commit comments

Comments
 (0)