diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-chat.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-chat.json index b75b00e3d0681..9ce394ac4b756 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-chat.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/langchain4j-chat.json @@ -30,7 +30,8 @@ "chatModel": { "index": 4, "kind": "property", "displayName": "Chat Model", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.model.chat.ChatLanguageModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.chat.LangChain4jChatConfiguration", "configurationField": "configuration", "description": "Chat Language Model of type dev.langchain4j.model.chat.ChatLanguageModel" } }, "headers": { - "CamelLangChain4jChatPromptTemplate": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prompt Template.", "constantName": "org.apache.camel.component.langchain4j.chat.LangChain4jChat$Headers#PROMPT_TEMPLATE" } + "CamelLangChain4jChatPromptTemplate": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prompt Template.", "constantName": "org.apache.camel.component.langchain4j.chat.LangChain4jChat$Headers#PROMPT_TEMPLATE" }, + "CamelLangChain4jChatAugmentedData": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Augmented Data for RAG", "constantName": "org.apache.camel.component.langchain4j.chat.LangChain4jChat$Headers#AUGMENTED_DATA" } }, "properties": { "chatId": { "index": 0, "kind": "path", "displayName": "Chat Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The id" }, diff --git a/components/camel-ai/camel-langchain4j-chat/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/chat/langchain4j-chat.json b/components/camel-ai/camel-langchain4j-chat/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/chat/langchain4j-chat.json index b75b00e3d0681..9ce394ac4b756 100644 --- a/components/camel-ai/camel-langchain4j-chat/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/chat/langchain4j-chat.json +++ b/components/camel-ai/camel-langchain4j-chat/src/generated/resources/META-INF/org/apache/camel/component/langchain4j/chat/langchain4j-chat.json @@ -30,7 +30,8 @@ "chatModel": { "index": 4, "kind": "property", "displayName": "Chat Model", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "dev.langchain4j.model.chat.ChatLanguageModel", "deprecated": false, "deprecationNote": "", "autowired": true, "secret": false, "configurationClass": "org.apache.camel.component.langchain4j.chat.LangChain4jChatConfiguration", "configurationField": "configuration", "description": "Chat Language Model of type dev.langchain4j.model.chat.ChatLanguageModel" } }, "headers": { - "CamelLangChain4jChatPromptTemplate": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prompt Template.", "constantName": "org.apache.camel.component.langchain4j.chat.LangChain4jChat$Headers#PROMPT_TEMPLATE" } + "CamelLangChain4jChatPromptTemplate": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prompt Template.", "constantName": "org.apache.camel.component.langchain4j.chat.LangChain4jChat$Headers#PROMPT_TEMPLATE" }, + "CamelLangChain4jChatAugmentedData": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Augmented Data for RAG", "constantName": "org.apache.camel.component.langchain4j.chat.LangChain4jChat$Headers#AUGMENTED_DATA" } }, "properties": { "chatId": { "index": 0, "kind": "path", "displayName": "Chat Id", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The id" }, diff --git a/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc b/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc index aa64d7ec8ad69..e791fae038929 100644 --- a/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc +++ b/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc @@ -180,3 +180,64 @@ List messages = new ArrayList<>(); Exchange message = fluentTemplate.to("direct:test").withBody(messages).request(Exchange.class); ---- + +== RAG (Retrieval Augmented Generation) +Use the RAG feature to enrich exchanges with data retrieved from any type of Camel endpoint. The feature is compatible with all LangChain4 Chat operations and is ideal for orchestrating the RAG workflow, utilizing the extensive library of components and Enterprise Integration Patterns (EIPs) available in Apache Camel. + +There are two ways for utilizing the RAG feature: + +=== Using RAG with Content Enricher and LangChain4jRagAggregatorStrategy +Enrich the exchange by retrieving a list of strings using any Camel producer. The `LangChain4jRagAggregatorStrategy` is specifically designed to augment data within LangChain4j chat producers. + +Example of usage: +[source, java] +---- +// Create an instance of the RAG aggregator strategy +LangChain4jRagAggregatorStrategy aggregatorStrategy = new LangChain4jRagAggregatorStrategy(); + +from("direct:test") + .enrich("direct:rag", aggregatorStrategy) + .to("langchain4j-chat:test1?chatOperation=CHAT_SIMPLE_MESSAGE"); + + from("direct:rag") + .process(exchange -> { + List augmentedData = List.of("data 1", "data 2" ); + exchange.getIn().setBody(augmentedData); + }); +---- + +[NOTE] +==== +This method leverages a separate Camel route to fetch and process the augmented data. +==== + +It is possible to enrich the message from multiple sources within the same exchange. + +Example of usage: +[source, java] +---- +// Create an instance of the RAG aggregator strategy +LangChain4jRagAggregatorStrategy aggregatorStrategy = new LangChain4jRagAggregatorStrategy(); + +from("direct:test") + .enrich("direct:rag-from-source-1", aggregatorStrategy) + .enrich("direct:rag-from-source-2", aggregatorStrategy) + .to("langchain4j-chat:test1?chatOperation=CHAT_SIMPLE_MESSAGE"); +---- + +=== Using RAG with Header +Directly add augmented data into the header. This method is particularly efficient for straightforward use cases where the augmented data is predefined or static. +You must add augmented data as a List of `dev.langchain4j.rag.content.Content` directly inside the header `CamelLangChain4jChatAugmentedData`. + +Example of usage: +[source, java] +---- +import dev.langchain4j.rag.content.Content; + +... + +Content augmentedContent = new Content("data test"); +List contents = List.of(augmentedContent); + +String response = template.requestBodyAndHeader("direct:send-multiple", messages, LangChain4jChat.Headers.AUGMENTED_DATA , contents, String.class); +---- diff --git a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChat.java b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChat.java index 7c2a13a5cbbfe..f08a08d2e76cc 100644 --- a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChat.java +++ b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChat.java @@ -28,5 +28,8 @@ private LangChain4jChat() { public static class Headers { @Metadata(description = "The prompt Template.", javaType = "String") public static final String PROMPT_TEMPLATE = "CamelLangChain4jChatPromptTemplate"; + + @Metadata(description = "Augmented Data for RAG", javaType = "String") + public static final String AUGMENTED_DATA = "CamelLangChain4jChatAugmentedData"; } } diff --git a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java index 455fbe9450d98..153eb6f4cf6b9 100644 --- a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java +++ b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java @@ -27,10 +27,14 @@ import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ToolExecutionResultMessage; +import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.input.Prompt; import dev.langchain4j.model.input.PromptTemplate; import dev.langchain4j.model.output.Response; +import dev.langchain4j.rag.content.Content; +import dev.langchain4j.rag.content.injector.ContentInjector; +import dev.langchain4j.rag.content.injector.DefaultContentInjector; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; import org.apache.camel.NoSuchHeaderException; @@ -39,6 +43,8 @@ import org.apache.camel.support.DefaultProducer; import org.apache.camel.util.ObjectHelper; +import static org.apache.camel.component.langchain4j.chat.LangChain4jChat.Headers.AUGMENTED_DATA; + public class LangChain4jChatProducer extends DefaultProducer { private final LangChain4jChatEndpoint endpoint; @@ -76,19 +82,19 @@ private void processSingleMessageWithPrompt(Exchange exchange) throws NoSuchHead Map variables = (Map) exchange.getIn().getMandatoryBody(Map.class); - var response = sendWithPromptTemplate(promptTemplate, variables); + var response = sendWithPromptTemplate(promptTemplate, variables, exchange); populateResponse(response, exchange); } private void processSingleMessage(Exchange exchange) throws InvalidPayloadException { + // Retrieve the mandatory body from the exchange final var message = exchange.getIn().getMandatoryBody(); - if (message instanceof String text) { - populateResponse(sendMessage(text), exchange); - } else if (message instanceof ChatMessage chatMessage) { - populateResponse(sendChatMessage(chatMessage), exchange); - } + // Use pattern matching with instanceof to streamline type checks and assignments + ChatMessage userMessage = (message instanceof String) ? new UserMessage((String) message) : (ChatMessage) message; + + populateResponse(sendChatMessage(userMessage, exchange), exchange); } @@ -110,24 +116,36 @@ private void populateResponse(String response, Exchange exchange) { } /** - * Send one simple message + * Send a ChatMessage * - * @param message + * @param chatMessage * @return */ - public String sendMessage(String message) { - return this.chatLanguageModel.generate(message); + private String sendChatMessage(ChatMessage chatMessage, Exchange exchange) { + var augmentedChatMessage = addAugmentedData(chatMessage, exchange); + + Response response = this.chatLanguageModel.generate(augmentedChatMessage); + return extractAiResponse(response); } /** - * Send a ChatMessage + * Augment the message for RAG if the header is specified * - * @param chatMessage - * @return + * @param chatMessage + * @param exchange */ - private String sendChatMessage(ChatMessage chatMessage) { - Response response = this.chatLanguageModel.generate(chatMessage); - return extractAiResponse(response); + private ChatMessage addAugmentedData(ChatMessage chatMessage, Exchange exchange) { + // check if there's any augmented data + List augmentedData = exchange.getIn().getHeader(AUGMENTED_DATA, List.class); + + // inject data + if (augmentedData != null && augmentedData.size() != 0) { + ContentInjector contentInjector = new DefaultContentInjector(); + // inject with new List of Contents + return contentInjector.inject(augmentedData, chatMessage); + } else { + return chatMessage; + } } /** @@ -140,6 +158,18 @@ private String sendListChatMessage(List chatMessages, Exchange exch LangChain4jChatEndpoint langChain4jChatEndpoint = (LangChain4jChatEndpoint) getEndpoint(); Response response; + + // Check if the last message is a UserMessage and if there's a need to augment the message for RAG + int size = chatMessages.size(); + if (size != 0) { + int lastIndex = size - 1; + ChatMessage lastUserMessage = chatMessages.get(lastIndex); + if (lastUserMessage instanceof UserMessage) { + chatMessages.set(lastIndex, addAugmentedData(lastUserMessage, exchange)); + } + + } + if (CamelToolExecutorCache.getInstance().getTools().containsKey(langChain4jChatEndpoint.getChatId())) { List toolSpecifications = CamelToolExecutorCache.getInstance().getTools() .get(langChain4jChatEndpoint.getChatId()).stream() @@ -191,10 +221,10 @@ private String extractAiResponse(Response response) { return message == null ? null : message.text(); } - public String sendWithPromptTemplate(String promptTemplate, Map variables) { + public String sendWithPromptTemplate(String promptTemplate, Map variables, Exchange exchange) { PromptTemplate template = PromptTemplate.from(promptTemplate); Prompt prompt = template.apply(variables); - return this.sendMessage(prompt.text()); + return this.sendChatMessage(new UserMessage(prompt.text()), exchange); } } diff --git a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/rag/LangChain4jRagAggregatorStrategy.java b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/rag/LangChain4jRagAggregatorStrategy.java new file mode 100644 index 0000000000000..b046a48996fcb --- /dev/null +++ b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/rag/LangChain4jRagAggregatorStrategy.java @@ -0,0 +1,59 @@ +/* + * 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.chat.rag; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import dev.langchain4j.rag.content.Content; +import org.apache.camel.AggregationStrategy; +import org.apache.camel.Exchange; + +import static org.apache.camel.component.langchain4j.chat.LangChain4jChat.Headers.AUGMENTED_DATA; + +public class LangChain4jRagAggregatorStrategy implements AggregationStrategy { + @Override + public Exchange aggregate(Exchange oldExchange, Exchange newExchange) { + // In theory old exchange shouldn't be null + if (oldExchange == null) { + return newExchange; + } + + // check that we got new Augmented Data + Optional> newAugmentedData = Optional.ofNullable(newExchange.getIn().getBody(List.class)); + if (newAugmentedData.isEmpty()) { + return oldExchange; + } + + // create a list of contents from the retrieved Strings + List newContents = newAugmentedData.get().stream() + .map(Content::new) + .collect(Collectors.toList()); + + // Get or create the augmented data list from the old exchange + List augmentedData = Optional.ofNullable(oldExchange.getIn().getHeader(AUGMENTED_DATA, List.class)) + .orElse(new ArrayList()); + augmentedData.addAll(newContents); + + // add the retrieved data in the body, langchain4j-chat will know it has to add inside the body + oldExchange.getIn().setHeader(AUGMENTED_DATA, augmentedData); + + return oldExchange; + } +} diff --git a/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jChatIT.java b/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jChatIT.java index ead521f9b0f19..a48fa8f66e454 100644 --- a/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jChatIT.java +++ b/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jChatIT.java @@ -20,27 +20,39 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.rag.content.Content; import org.apache.camel.InvalidPayloadException; import org.apache.camel.NoSuchHeaderException; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.langchain4j.chat.rag.LangChain4jRagAggregatorStrategy; import org.apache.camel.component.mock.MockEndpoint; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; +import static org.apache.camel.component.langchain4j.chat.LangChain4jChat.Headers.AUGMENTED_DATA; +import static org.apache.camel.component.langchain4j.chat.LangChain4jChat.Headers.PROMPT_TEMPLATE; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", disabledReason = "Requires too much network resources") public class LangChain4jChatIT extends OllamaTestSupport { + final static String AUGMENTEG_DATA_FOR_RAG + = "Sweden's Armand Duplantis set a new world record of 6.25m after winning gold in the men's pole vault at Paris 2024 Olympics."; + final static String QUESTION_FOR_RAG = "Who got the gold medal in pole vault at Paris 2024?"; + @Override protected RouteBuilder createRouteBuilder() { this.context.getRegistry().bind("chatModel", chatLanguageModel); + LangChain4jRagAggregatorStrategy aggregatorStrategy = new LangChain4jRagAggregatorStrategy(); return new RouteBuilder() { public void configure() { @@ -72,6 +84,25 @@ public void configure() { .end() .to("mock:response"); + from("direct:send-with-rag") + .enrich("direct:add-augmented-data", aggregatorStrategy) + .to("direct:send-simple-message"); + + from("direct:send-message-prompt-enrich") + .enrich("direct:add-augmented-data", aggregatorStrategy) + .to("direct:send-message-prompt"); + + from("direct:send-multiple-enrich") + .enrich("direct:add-augmented-data", aggregatorStrategy) + .to("direct:send-multiple"); + + from("direct:add-augmented-data") + .process(exchange -> { + List augmentedData = List.of( + AUGMENTEG_DATA_FOR_RAG); + exchange.getIn().setBody(augmentedData); + }); + } }; } @@ -121,7 +152,7 @@ void testSendMessageWithPrompt() throws InterruptedException { variables.put("ingredients", "potato, tomato, feta, olive oil"); String response = template.requestBodyAndHeader("direct:send-message-prompt", variables, - LangChain4jChat.Headers.PROMPT_TEMPLATE, promptTemplate, String.class); + PROMPT_TEMPLATE, promptTemplate, String.class); mockEndpoint.assertIsSatisfied(); assertTrue(response.contains("potato")); @@ -153,7 +184,7 @@ void testSendMessageEmptyVariables() throws InterruptedException { var promptTemplate = "Create a recipe for a {{dishType}} with the following ingredients: {{ingredients}}"; template.sendBodyAndHeader("direct:send-message-prompt", null, - LangChain4jChat.Headers.PROMPT_TEMPLATE, promptTemplate); + PROMPT_TEMPLATE, promptTemplate); mockEndpoint.assertIsSatisfied(); } @@ -187,4 +218,123 @@ void testSendMultipleEmpty() throws InterruptedException { mockEndpoint.assertIsSatisfied(); } + @Test + @Timeout(value = 2, unit = TimeUnit.MINUTES) + void testSimpleMessageWithEnrichRAG() throws InterruptedException { + MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response", MockEndpoint.class); + mockEndpoint.expectedMessageCount(1); + + var response = template.requestBody("direct:send-with-rag", QUESTION_FOR_RAG, + String.class); + + // this test could change if using an LLM updated after results of Olympics 2024 + assertTrue(response.toLowerCase().contains("armand duplantis")); + mockEndpoint.assertIsSatisfied(); + } + + @Test + @Timeout(value = 60, unit = TimeUnit.SECONDS) + void testSimpleMessageWithHeaderhRAG() throws InterruptedException { + MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response", MockEndpoint.class); + mockEndpoint.expectedMessageCount(1); + + Content augmentedContent = new Content(AUGMENTEG_DATA_FOR_RAG); + + List contents = List.of(augmentedContent); + + var response = template.requestBodyAndHeader("direct:send-simple-message", QUESTION_FOR_RAG, + AUGMENTED_DATA, contents, String.class); + + // this test could change if using an LLM updated after results of Olympics 2024 + assertTrue(response.toLowerCase().contains("armand duplantis")); + mockEndpoint.assertIsSatisfied(); + } + + @Test + void testSendMessageWithPromptRagEnrich() throws InterruptedException { + MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response", MockEndpoint.class); + mockEndpoint.expectedMessageCount(1); + + var promptTemplate = " Who got the gold medal in {{field}} at: {{competition}}"; + + Map variables = new HashMap<>(); + variables.put("field", "pole vault"); + variables.put("competition", "Paris 2024"); + + String response = template.requestBodyAndHeader("direct:send-message-prompt-enrich", variables, + PROMPT_TEMPLATE, promptTemplate, String.class); + mockEndpoint.assertIsSatisfied(); + + // this test could change if using an LLM updated after results of Olympics 2024 + assertTrue(response.toLowerCase().contains("armand duplantis")); + } + + @Test + void testSendMessageWithPromptRagHeader() throws InterruptedException { + MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response", MockEndpoint.class); + mockEndpoint.expectedMessageCount(1); + + var promptTemplate = " Who got the gold medal in {{field}} at: {{competition}}"; + + Map variables = new HashMap<>(); + variables.put("field", "pole vault"); + variables.put("competition", "Paris 2024"); + + Content augmentedContent = new Content(AUGMENTEG_DATA_FOR_RAG); + + List contents = List.of(augmentedContent); + + Map headerValues = new HashMap<>(); + headerValues.put(PROMPT_TEMPLATE, promptTemplate); + headerValues.put(AUGMENTED_DATA, contents); + + String response = template.requestBodyAndHeaders("direct:send-message-prompt", variables, + headerValues, String.class); + mockEndpoint.assertIsSatisfied(); + + // this test could change if using an LLM updated after results of Olympics 2024 + assertTrue(response.toLowerCase().contains("armand duplantis")); + } + + @Test + void testSendMultipleMessagesRagEnrich() throws InterruptedException { + MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response", MockEndpoint.class); + mockEndpoint.expectedMessageCount(1); + + List messages = new ArrayList<>(); + messages.add(new SystemMessage("You are asked to provide names for athletes that won medals at Paris 2024 Olympics.")); + messages.add(new UserMessage("Hello, my name is Karen.")); + messages.add(new AiMessage("Hello Karen, how can I help you?")); + messages.add(new UserMessage(QUESTION_FOR_RAG)); + + String response = template.requestBody("direct:send-multiple-enrich", messages, String.class); + mockEndpoint.assertIsSatisfied(); + + // this test could change if using an LLM updated after results of Olympics 2024 + assertTrue(response.toLowerCase().contains("armand duplantis")); + } + + @Test + void testSendMultipleMessagesRagHeader() throws InterruptedException { + MockEndpoint mockEndpoint = this.context.getEndpoint("mock:response", MockEndpoint.class); + mockEndpoint.expectedMessageCount(1); + + List messages = new ArrayList<>(); + messages.add(new SystemMessage("You are asked to provide names for athletes that won medals at Paris 2024 Olympics.")); + messages.add(new UserMessage("Hello, my name is Karen.")); + messages.add(new AiMessage("Hello Karen, how can I help you?")); + messages.add(new UserMessage(QUESTION_FOR_RAG)); + + Content augmentedContent = new Content(AUGMENTEG_DATA_FOR_RAG); + List contents = List.of(augmentedContent); + + String response + = template.requestBodyAndHeader("direct:send-multiple", messages, AUGMENTED_DATA, contents, String.class); + mockEndpoint.assertIsSatisfied(); + + // this test could change if using an LLM updated after results of Olympics 2024 + assertTrue(response.toLowerCase().contains("armand duplantis")); + + } + } diff --git a/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jRagAggregatorTest.java b/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jRagAggregatorTest.java new file mode 100644 index 0000000000000..76e22716a678d --- /dev/null +++ b/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/LangChain4jRagAggregatorTest.java @@ -0,0 +1,121 @@ +/* + * 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.chat; + +import java.util.ArrayList; +import java.util.List; + +import dev.langchain4j.rag.content.Content; +import org.apache.camel.Exchange; +import org.apache.camel.component.langchain4j.chat.rag.LangChain4jRagAggregatorStrategy; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.DefaultExchange; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.apache.camel.component.langchain4j.chat.LangChain4jChat.Headers.AUGMENTED_DATA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class LangChain4jRagAggregatorTest { + + private LangChain4jRagAggregatorStrategy aggregator; + private Exchange oldExchange; + private Exchange newExchange; + + @BeforeEach + void setUp() { + aggregator = new LangChain4jRagAggregatorStrategy(); + oldExchange = new DefaultExchange(new DefaultCamelContext()); + newExchange = new DefaultExchange(new DefaultCamelContext()); + } + + @Test + void testAggregateWithNoNewData() { + Exchange result = aggregator.aggregate(oldExchange, newExchange); + assertEquals(oldExchange, result); + } + + @Test + void testAggregateWithNewData() { + + // setting a prompt in the old Exchange + oldExchange.getIn().setBody("Prompt Test"); + + // setting augmented data in the new Exchange + List newData = List.of("data1", "data2"); + newExchange.getIn().setBody(newData); + + Exchange result = aggregator.aggregate(oldExchange, newExchange); + + List contents = result.getIn().getHeader(AUGMENTED_DATA, List.class); + String prompt = result.getIn().getBody(String.class); + + assertNotNull("The body should contain the old body", prompt); + assertEquals("Prompt Test", prompt); + + assertNotNull("The old exchange should contain now the enriched data in type of List of Content", contents); + assertEquals(2, contents.size()); + + assertTrue("The first content item should match one of the new data entries.", + newData.contains(contents.get(0).textSegment().text())); + assertTrue("The second content item should match one of the new data entries.", + newData.contains(contents.get(1).textSegment().text())); + } + + @Test + void testAggregateWithExistingAndNewData() { + + // setting a prompt in the old Exchange + oldExchange.getIn().setBody("Prompt Test"); + + // setting a content in the old exchange + Content oldContent = new Content("Old data"); + List contents = new ArrayList<>(); + contents.add(oldContent); + oldExchange.getIn().setHeader(AUGMENTED_DATA, contents); + + // setting augmented data in the new Exchange + List newData = List.of("data1", "data2"); + newExchange.getIn().setBody(newData); + + Exchange result = aggregator.aggregate(oldExchange, newExchange); + + contents = result.getIn().getHeader(AUGMENTED_DATA, List.class); + String prompt = result.getIn().getBody(String.class); + + assertNotNull("The body should contain the old body", prompt); + assertEquals("Prompt Test", prompt); + + assertNotNull("The old exchange should contain now the enriched data in type of List of Content", contents); + assertEquals(3, contents.size()); + + assertEquals("The first content item should match the old content", "Old data", contents.get(0).textSegment().text()); + assertTrue("The second content item should match one of the new data entries.", + newData.contains(contents.get(1).textSegment().text())); + assertTrue("The third content item should match one of the new data entries.", + newData.contains(contents.get(2).textSegment().text())); + } + + @Test + void testOldExchangeIsNull() { + newExchange.getMessage().setHeader(AUGMENTED_DATA, "Additional data"); + Exchange result = aggregator.aggregate(null, newExchange); + assertEquals(newExchange, result); + } +} diff --git a/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/OllamaTestSupport.java b/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/OllamaTestSupport.java index f19e44c5df297..f23197a8ec8ac 100644 --- a/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/OllamaTestSupport.java +++ b/components/camel-ai/camel-langchain4j-chat/src/test/java/org/apache/camel/component/langchain4j.chat/OllamaTestSupport.java @@ -44,7 +44,7 @@ public ChatLanguageModel createModel() { .baseUrl(OLLAMA.getBaseUrl()) .modelName(OLLAMA.getModel()) .temperature(0.3) - .timeout(ofSeconds(3000)) + .timeout(ofSeconds(60)) .build(); } } diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jChatEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jChatEndpointBuilderFactory.java index cfab07c4891b5..db74cdad193f4 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jChatEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LangChain4jChatEndpointBuilderFactory.java @@ -252,6 +252,18 @@ public static class LangChain4jChatHeaderNameBuilder { public String langChain4jChatPromptTemplate() { return "CamelLangChain4jChatPromptTemplate"; } + /** + * Augmented Data for RAG. + * + * The option is a: {@code String} type. + * + * Group: producer + * + * @return the name of the header {@code LangChain4jChatAugmentedData}. + */ + public String langChain4jChatAugmentedData() { + return "CamelLangChain4jChatAugmentedData"; + } } static LangChain4jChatEndpointBuilder endpointBuilder(String componentName, String path) { class LangChain4jChatEndpointBuilderImpl extends AbstractEndpointBuilder implements LangChain4jChatEndpointBuilder, AdvancedLangChain4jChatEndpointBuilder {