You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by fm...@apache.org on 2025/08/01 15:45:35 UTC
(camel) branch main updated (d5407b7adca -> 7713368c305)
This is an automated email from the ASF dual-hosted git repository.
fmariani pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
from d5407b7adca Upgrade Elasticsearch container image version for test-infra to 9.1.0 (#18801)
new 2ae53cdc75a OpenAIMock - handle complex scenarios for camel langchain4j agent
new 7713368c305 Move IT tests to integration + add a comprehensive unit test
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../camel-ai/camel-langchain4j-agent/pom.xml | 11 ++
...tractRAGTest.java => BaseLangChain4jAgent.java} | 97 +++++--------
.../LangChain4jAgentWithMemoryServiceTest.java | 151 +++++++++++++++++++++
.../AbstractRAGIT.java} | 70 +---------
.../LangChain4jAgentGuardrailsIntegrationIT.java | 2 +-
.../LangChain4jAgentNaiveRagIT.java | 4 +-
.../LangChain4jAgentServiceIT.java | 5 +-
.../LangChain4jAgentWithMemoryIT.java | 2 +-
.../LangChain4jAgentWithMemoryServiceIT.java} | 5 +-
.../LangChain4jAgentWithToolsIT.java | 5 +-
.../LangChain4jSimpleAgentIT.java | 3 +-
.../test/infra/openai/mock/MockExpectation.java | 9 ++
.../test/infra/openai/mock/OpenAIMockBuilder.java | 7 +
.../test/infra/openai/mock/RequestContext.java | 20 +--
.../test/infra/openai/mock/RequestHandler.java | 14 +-
.../test/infra/openai/mock/ResponseBuilder.java | 18 ++-
.../mock/OpenAIMockConversationHistoryTest.java | 82 +++++++++++
....java => OpenAIMockReplyWithAfterToolTest.java} | 64 ++++-----
...ava => OpenAIMockReplyWithToolContentTest.java} | 78 +++++------
.../openai/mock/OpenAIMockSimpleAssertionTest.java | 106 +++++++++++++++
20 files changed, 522 insertions(+), 231 deletions(-)
copy components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{AbstractRAGTest.java => BaseLangChain4jAgent.java} (53%)
create mode 100644 components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryServiceTest.java
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{AbstractRAGTest.java => integration/AbstractRAGIT.java} (56%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{ => integration}/LangChain4jAgentGuardrailsIntegrationIT.java (99%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{ => integration}/LangChain4jAgentNaiveRagIT.java (98%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{ => integration}/LangChain4jAgentServiceIT.java (96%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{ => integration}/LangChain4jAgentWithMemoryIT.java (99%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{LangChain4jAgenWithMemoryServiceIT.java => integration/LangChain4jAgentWithMemoryServiceIT.java} (97%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{ => integration}/LangChain4jAgentWithToolsIT.java (97%)
rename components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/{ => integration}/LangChain4jSimpleAgentIT.java (97%)
create mode 100644 test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockConversationHistoryTest.java
copy test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/{OpenAIMockMultipleToolsTest.java => OpenAIMockReplyWithAfterToolTest.java} (54%)
copy test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/{OpenAIMockMultipleToolsTest.java => OpenAIMockReplyWithToolContentTest.java} (50%)
create mode 100644 test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockSimpleAssertionTest.java
(camel) 01/02: OpenAIMock - handle complex scenarios for camel langchain4j agent
Posted by fm...@apache.org.
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
commit 2ae53cdc75a05fc9c2a3709cbe9042604c5e6af5
Author: Croway <fe...@gmail.com>
AuthorDate: Fri Aug 1 16:09:44 2025 +0200
OpenAIMock - handle complex scenarios for camel langchain4j agent
---
.../test/infra/openai/mock/MockExpectation.java | 9 ++
.../test/infra/openai/mock/OpenAIMockBuilder.java | 7 ++
.../test/infra/openai/mock/RequestContext.java | 20 ++--
.../test/infra/openai/mock/RequestHandler.java | 14 ++-
.../test/infra/openai/mock/ResponseBuilder.java | 18 +++-
.../mock/OpenAIMockConversationHistoryTest.java | 82 ++++++++++++++++
.../mock/OpenAIMockReplyWithAfterToolTest.java | 96 +++++++++++++++++++
.../mock/OpenAIMockReplyWithToolContentTest.java | 102 ++++++++++++++++++++
.../openai/mock/OpenAIMockSimpleAssertionTest.java | 106 +++++++++++++++++++++
9 files changed, 436 insertions(+), 18 deletions(-)
diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java
index e84c936f081..c5503b024a0 100644
--- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java
+++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java
@@ -29,6 +29,7 @@ public class MockExpectation {
private final String expectedInput;
private final ToolExecutionSequence toolSequence;
private String expectedResponse;
+ private String toolContentResponse;
private BiFunction<HttpExchange, String, String> customResponseFunction;
private Consumer<String> requestAssertion;
@@ -46,6 +47,10 @@ public class MockExpectation {
return expectedResponse;
}
+ public String getToolContentResponse() {
+ return toolContentResponse;
+ }
+
public BiFunction<HttpExchange, String, String> getCustomResponseFunction() {
return customResponseFunction;
}
@@ -71,6 +76,10 @@ public class MockExpectation {
this.requestAssertion = requestAssertion;
}
+ public void setToolContentResponse(String toolContentResponse) {
+ this.toolContentResponse = toolContentResponse;
+ }
+
// Tool sequence delegation methods
public void addToolExecutionStep(ToolExecutionStep step) {
toolSequence.addStep(step);
diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java
index b52dc8a16cc..2ce1eb1326d 100644
--- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java
+++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java
@@ -52,6 +52,13 @@ public class OpenAIMockBuilder {
return this;
}
+ public OpenAIMockBuilder replyWithToolContent(String customMessage) {
+ validateCurrentExpectation("replyWithToolContent()");
+ log.debug("Setting tool content response with custom message: {}", customMessage);
+ currentExpectation.setToolContentResponse(customMessage);
+ return this;
+ }
+
public OpenAIMockBuilder invokeTool(String toolName) {
validateCurrentExpectation("invokeTool()");
log.debug("Adding new tool execution step with tool: {}", toolName);
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 ee191ab9f5e..93eab20d9f8 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
@@ -31,30 +31,30 @@ public class RequestContext {
}
public boolean hasToolRole() {
- if (!messagesNode.isArray()) {
+ if (!messagesNode.isArray() || messagesNode.size() == 0) {
return false;
}
- for (JsonNode messageNode : messagesNode) {
- String role = messageNode.path("role").asText();
- if ("tool".equals(role)) {
- return true;
- }
- }
- return false;
+ // Check if the last message has a tool role
+ JsonNode lastMessage = messagesNode.get(messagesNode.size() - 1);
+ String role = lastMessage.path("role").asText();
+ return "tool".equals(role);
}
- public String getFirstUserMessage() {
+ public String getLastUserMessage() {
if (!messagesNode.isArray()) {
return null;
}
- for (JsonNode messageNode : messagesNode) {
+ for (int i = messagesNode.size(); i > 0; i--) {
+ JsonNode messageNode = messagesNode.get(i - 1);
+
String role = messageNode.path("role").asText();
if ("user".equals(role)) {
return messageNode.path("content").asText();
}
}
+
return null;
}
diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java
index b10c5b1bd1b..5a198f697f1 100644
--- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java
+++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java
@@ -50,7 +50,9 @@ public class RequestHandler {
JsonNode rootNode = objectMapper.readTree(requestBody);
RequestContext context = new RequestContext(rootNode);
- if (context.hasToolRole()) {
+ MockExpectation expectation = findExpectationByInput(context.getLastUserMessage());
+
+ if (context.hasToolRole() && !expectation.getToolSequence().isEmpty()) {
return handleToolSequenceResponse(context, exchange);
} else {
return handleUserInput(requestBody, context, exchange);
@@ -63,7 +65,7 @@ public class RequestHandler {
}
private String handleToolSequenceResponse(RequestContext context, HttpExchange exchange) throws Exception {
- String originalInput = context.getFirstUserMessage();
+ String originalInput = context.getLastUserMessage();
if (originalInput == null) {
LOG.warn("Could not find original user input in message history");
return responseBuilder.createErrorResponse(400, "Original user input not found", exchange);
@@ -79,15 +81,17 @@ public class RequestHandler {
if (expectation.hasMoreToolSteps()) {
LOG.debug("Executing next tool step for expectation: {}", originalInput);
- return createToolCallResponse(expectation);
+ String result = createToolCallResponse(expectation);
+ return result;
} else {
LOG.debug("Tool sequence completed for expectation: {}", originalInput);
- return responseBuilder.createFinalToolResponse(context.getMessagesNode(), expectation.getExpectedResponse());
+ return responseBuilder.createFinalToolResponse(context.getMessagesNode(), expectation.getExpectedResponse(),
+ expectation.getToolContentResponse());
}
}
private String handleUserInput(String requestBody, RequestContext context, HttpExchange exchange) throws Exception {
- String userInput = context.getFirstUserMessage();
+ String userInput = context.getLastUserMessage();
if (userInput == null) {
LOG.warn("User message content not found in request");
throw new IllegalArgumentException("User message content not found in request");
diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java
index f1618c316fd..606df57efd2 100644
--- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java
+++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java
@@ -61,11 +61,19 @@ public class ResponseBuilder {
return objectMapper.writeValueAsString(chatCompletion);
}
- public String createFinalToolResponse(JsonNode messagesNode, String fallbackContent) throws Exception {
+ public String createFinalToolResponse(JsonNode messagesNode, String fallbackContent, String toolContentResponse)
+ throws Exception {
Map<String, Object> responseMessage = createBaseMessage();
- String content = extractLastToolContent(messagesNode)
- .orElse(fallbackContent != null ? fallbackContent : "All tools processed");
+ String content;
+ if (fallbackContent != null) {
+ content = fallbackContent;
+ } else if (toolContentResponse != null) {
+ String toolContent = extractLastToolContent(messagesNode).orElse("");
+ content = toolContent + " " + toolContentResponse;
+ } else {
+ content = extractLastToolContent(messagesNode).orElse("All tools processed");
+ }
responseMessage.put("content", content);
Map<String, Object> choice = createBaseChoice("stop", responseMessage);
@@ -75,6 +83,10 @@ public class ResponseBuilder {
return objectMapper.writeValueAsString(chatCompletion);
}
+ public String createFinalToolResponse(JsonNode messagesNode, String fallbackContent) throws Exception {
+ return createFinalToolResponse(messagesNode, fallbackContent, null);
+ }
+
public String createErrorResponse(int statusCode, String errorMessage, HttpExchange exchange) {
String jsonErrorMessage = String.format("{\"error\": \"%s\"}", errorMessage);
diff --git a/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockConversationHistoryTest.java b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockConversationHistoryTest.java
new file mode 100644
index 00000000000..4bc3a9fab91
--- /dev/null
+++ b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockConversationHistoryTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.test.infra.openai.mock;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OpenAIMockConversationHistoryTest {
+
+ private final AtomicBoolean secondAssertionExecuted = new AtomicBoolean(false);
+
+ @RegisterExtension
+ public OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("What's his preferred vehicle type?")
+ .assertRequest(request -> {
+ secondAssertionExecuted.set(true);
+ // Assert that memory is working as expected
+ assertTrue(request.contains("Hi! Can you look up user 123 and tell me about our rental policies?"));
+ })
+ .replyWith("SUV")
+ .end()
+ .build();
+
+ @Test
+ public void testConversationHistoryAssertion() throws Exception {
+ HttpClient client = HttpClient.newHttpClient();
+
+ // Send request with conversation history
+ String requestBody = "{\"messages\": [" +
+ "{\"role\": \"user\", \"content\": \"Hi! Can you look up user 123 and tell me about our rental policies?\"},"
+ +
+ "{\"role\": \"assistant\", \"content\": \"Previous response\"}," +
+ "{\"role\": \"user\", \"content\": \"What's his preferred vehicle type?\"}" +
+ "]}";
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ String responseBody = response.body();
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode responseJson = objectMapper.readTree(responseBody);
+
+ JsonNode choice = responseJson.path("choices").get(0);
+ JsonNode message = choice.path("message");
+
+ assertEquals("assistant", message.path("role").asText());
+ assertEquals("SUV", message.path("content").asText());
+
+ // Verify assertion was executed
+ assertTrue(secondAssertionExecuted.get(), "Second assertion should have been executed");
+ }
+}
diff --git a/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockReplyWithAfterToolTest.java b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockReplyWithAfterToolTest.java
new file mode 100644
index 00000000000..0ce27dacf19
--- /dev/null
+++ b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockReplyWithAfterToolTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.test.infra.openai.mock;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class OpenAIMockReplyWithAfterToolTest {
+
+ @RegisterExtension
+ public OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("What is the weather in london?")
+ .invokeTool("FindsTheLatitudeAndLongitudeOfAGivenCity")
+ .withParam("name", "London")
+ .replyWith("the latitude of london is 1")
+ .end()
+ .build();
+
+ @Test
+ public void testReplyWithAfterTool() throws Exception {
+ HttpClient client = HttpClient.newHttpClient();
+
+ // First request - should trigger tool call
+ HttpRequest request1 = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers
+ .ofString("{\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather in london?\"}]}"))
+ .build();
+
+ HttpResponse<String> response1 = client.send(request1, HttpResponse.BodyHandlers.ofString());
+ String responseBody1 = response1.body();
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode responseJson1 = objectMapper.readTree(responseBody1);
+
+ JsonNode choice1 = responseJson1.path("choices").get(0);
+ JsonNode message1 = choice1.path("message");
+
+ assertEquals("assistant", message1.path("role").asText());
+ assertEquals("tool_calls", choice1.path("finish_reason").asText());
+
+ JsonNode toolCalls = message1.path("tool_calls");
+ assertEquals(1, toolCalls.size());
+
+ JsonNode toolCall = toolCalls.get(0);
+ String toolCallId = toolCall.path("id").asText();
+ assertEquals("function", toolCall.path("type").asText());
+ assertEquals("FindsTheLatitudeAndLongitudeOfAGivenCity", toolCall.path("function").path("name").asText());
+ assertEquals("{\"name\":\"London\"}", toolCall.path("function").path("arguments").asText());
+
+ // Second request with tool result - should return custom reply
+ String secondRequestBody = String.format(
+ "{\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather in london?\"}, {\"role\":\"tool\", \"tool_call_id\":\"%s\", \"content\":\"{\\\"latitude\\\": \\\"51.5074\\\", \\\"longitude\\\": \\\"-0.1278\\\"}\"}]}",
+ toolCallId);
+ HttpRequest request2 = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(secondRequestBody))
+ .build();
+
+ HttpResponse<String> response2 = client.send(request2, HttpResponse.BodyHandlers.ofString());
+ String responseBody2 = response2.body();
+ JsonNode responseJson2 = objectMapper.readTree(responseBody2);
+
+ JsonNode choice2 = responseJson2.path("choices").get(0);
+ JsonNode message2 = choice2.path("message");
+
+ assertEquals("assistant", message2.path("role").asText());
+ assertEquals("stop", choice2.path("finish_reason").asText());
+ assertEquals("the latitude of london is 1", message2.path("content").asText());
+ }
+}
diff --git a/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockReplyWithToolContentTest.java b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockReplyWithToolContentTest.java
new file mode 100644
index 00000000000..095a532a903
--- /dev/null
+++ b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockReplyWithToolContentTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.test.infra.openai.mock;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OpenAIMockReplyWithToolContentTest {
+
+ @RegisterExtension
+ public OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("Get location coordinates")
+ .invokeTool("GetCoordinates")
+ .withParam("location", "Paris")
+ .replyWithToolContent("- This is the location data I found.")
+ .end()
+ .build();
+
+ @Test
+ public void testReplyWithToolContent() throws Exception {
+ HttpClient client = HttpClient.newHttpClient();
+
+ // First request - should trigger tool call
+ HttpRequest request1 = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers
+ .ofString("{\"messages\": [{\"role\": \"user\", \"content\": \"Get location coordinates\"}]}"))
+ .build();
+
+ HttpResponse<String> response1 = client.send(request1, HttpResponse.BodyHandlers.ofString());
+ String responseBody1 = response1.body();
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode responseJson1 = objectMapper.readTree(responseBody1);
+
+ JsonNode choice1 = responseJson1.path("choices").get(0);
+ JsonNode message1 = choice1.path("message");
+
+ assertEquals("assistant", message1.path("role").asText());
+ assertEquals("tool_calls", choice1.path("finish_reason").asText());
+
+ JsonNode toolCalls = message1.path("tool_calls");
+ assertEquals(1, toolCalls.size());
+
+ JsonNode toolCall = toolCalls.get(0);
+ String toolCallId = toolCall.path("id").asText();
+ assertEquals("function", toolCall.path("type").asText());
+ assertEquals("GetCoordinates", toolCall.path("function").path("name").asText());
+ assertEquals("{\"location\":\"Paris\"}", toolCall.path("function").path("arguments").asText());
+
+ // Second request with tool result - should return tool content + custom message
+ String secondRequestBody = String.format(
+ "{\"messages\": [{\"role\": \"user\", \"content\": \"Get location coordinates\"}, {\"role\":\"tool\", \"tool_call_id\":\"%s\", \"content\":\"{\\\"latitude\\\": \\\"48.8566\\\", \\\"longitude\\\": \\\"2.3522\\\"}\"}]}",
+ toolCallId);
+ HttpRequest request2 = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(secondRequestBody))
+ .build();
+
+ HttpResponse<String> response2 = client.send(request2, HttpResponse.BodyHandlers.ofString());
+ String responseBody2 = response2.body();
+ JsonNode responseJson2 = objectMapper.readTree(responseBody2);
+
+ JsonNode choice2 = responseJson2.path("choices").get(0);
+ JsonNode message2 = choice2.path("message");
+
+ assertEquals("assistant", message2.path("role").asText());
+ assertEquals("stop", choice2.path("finish_reason").asText());
+
+ String content = message2.path("content").asText();
+ // Should contain both the tool content and the custom message
+ assertTrue(content.contains("{\"latitude\": \"48.8566\", \"longitude\": \"2.3522\"}"));
+ assertTrue(content.contains("- This is the location data I found."));
+ assertEquals("{\"latitude\": \"48.8566\", \"longitude\": \"2.3522\"} - This is the location data I found.", content);
+ }
+}
diff --git a/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockSimpleAssertionTest.java b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockSimpleAssertionTest.java
new file mode 100644
index 00000000000..6d0254e7c5b
--- /dev/null
+++ b/test-infra/camel-test-infra-openai-mock/src/test/java/org/apache/camel/test/infra/openai/mock/OpenAIMockSimpleAssertionTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.test.infra.openai.mock;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode;
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OpenAIMockSimpleAssertionTest {
+
+ private final AtomicBoolean firstAssertionExecuted = new AtomicBoolean(false);
+ private final AtomicBoolean secondAssertionExecuted = new AtomicBoolean(false);
+
+ @RegisterExtension
+ public OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("first message")
+ .assertRequest(request -> {
+ firstAssertionExecuted.set(true);
+ assertTrue(request.contains("first message"));
+ })
+ .replyWith("first response")
+ .end()
+ .when("second message")
+ .assertRequest(request -> {
+ secondAssertionExecuted.set(true);
+ assertTrue(request.contains("second message"));
+ })
+ .replyWith("second response")
+ .end()
+ .build();
+
+ @Test
+ public void testBothAssertionsExecuted() throws Exception {
+ HttpClient client = HttpClient.newHttpClient();
+
+ // First request
+ HttpRequest request1 = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers
+ .ofString("{\"messages\": [{\"role\": \"user\", \"content\": \"first message\"}]}"))
+ .build();
+
+ HttpResponse<String> response1 = client.send(request1, HttpResponse.BodyHandlers.ofString());
+ String responseBody1 = response1.body();
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ JsonNode responseJson1 = objectMapper.readTree(responseBody1);
+
+ JsonNode choice1 = responseJson1.path("choices").get(0);
+ JsonNode message1 = choice1.path("message");
+
+ assertEquals("assistant", message1.path("role").asText());
+ assertEquals("first response", message1.path("content").asText());
+
+ // Verify first assertion was executed
+ assertTrue(firstAssertionExecuted.get(), "First assertion should have been executed");
+
+ // Second request
+ HttpRequest request2 = HttpRequest.newBuilder()
+ .uri(URI.create(openAIMock.getBaseUrl() + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers
+ .ofString("{\"messages\": [{\"role\": \"user\", \"content\": \"second message\"}]}"))
+ .build();
+
+ HttpResponse<String> response2 = client.send(request2, HttpResponse.BodyHandlers.ofString());
+ String responseBody2 = response2.body();
+
+ ObjectMapper objectMapper2 = new ObjectMapper();
+ JsonNode responseJson2 = objectMapper2.readTree(responseBody2);
+
+ JsonNode choice2 = responseJson2.path("choices").get(0);
+ JsonNode message2 = choice2.path("message");
+
+ assertEquals("assistant", message2.path("role").asText());
+ assertEquals("second response", message2.path("content").asText());
+
+ // Verify second assertion was executed
+ assertTrue(secondAssertionExecuted.get(), "Second assertion should have been executed");
+ }
+}
(camel) 02/02: Move IT tests to integration + add a comprehensive unit test
Posted by fm...@apache.org.
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
commit 7713368c305a4a0ab037d551e37a715c752dc31a
Author: Croway <fe...@gmail.com>
AuthorDate: Fri Aug 1 16:10:18 2025 +0200
Move IT tests to integration + add a comprehensive unit test
---
.../camel-ai/camel-langchain4j-agent/pom.xml | 11 ++
...tractRAGTest.java => BaseLangChain4jAgent.java} | 97 +++++--------
.../LangChain4jAgentWithMemoryServiceTest.java | 151 +++++++++++++++++++++
.../AbstractRAGIT.java} | 70 +---------
.../LangChain4jAgentGuardrailsIntegrationIT.java | 2 +-
.../LangChain4jAgentNaiveRagIT.java | 4 +-
.../LangChain4jAgentServiceIT.java | 5 +-
.../LangChain4jAgentWithMemoryIT.java | 2 +-
.../LangChain4jAgentWithMemoryServiceIT.java} | 5 +-
.../LangChain4jAgentWithToolsIT.java | 5 +-
.../LangChain4jSimpleAgentIT.java | 3 +-
11 files changed, 214 insertions(+), 141 deletions(-)
diff --git a/components/camel-ai/camel-langchain4j-agent/pom.xml b/components/camel-ai/camel-langchain4j-agent/pom.xml
index bb0e4ff81b9..4a45240f472 100644
--- a/components/camel-ai/camel-langchain4j-agent/pom.xml
+++ b/components/camel-ai/camel-langchain4j-agent/pom.xml
@@ -73,6 +73,17 @@
<version>${langchain4j-beta-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-test-infra-openai-mock</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AbstractRAGTest.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/BaseLangChain4jAgent.java
similarity index 53%
copy from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AbstractRAGTest.java
copy to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/BaseLangChain4jAgent.java
index ede421457c4..c1062d13a93 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AbstractRAGTest.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/BaseLangChain4jAgent.java
@@ -16,30 +16,35 @@
*/
package org.apache.camel.component.langchain4j.agent;
-import java.util.List;
-
-import dev.langchain4j.data.document.Document;
-import dev.langchain4j.data.document.splitter.DocumentSplitters;
-import dev.langchain4j.data.embedding.Embedding;
-import dev.langchain4j.data.segment.TextSegment;
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
-import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
-import dev.langchain4j.rag.DefaultRetrievalAugmentor;
import dev.langchain4j.rag.RetrievalAugmentor;
-import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
-import dev.langchain4j.store.embedding.EmbeddingStore;
-import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
+import dev.langchain4j.store.memory.chat.ChatMemoryStore;
+import org.apache.camel.component.langchain4j.agent.pojos.PersistentChatMemoryStore;
import org.apache.camel.test.junit5.CamelTestSupport;
import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
import static java.time.Duration.ofSeconds;
-public abstract class AbstractRAGTest extends CamelTestSupport {
+public class BaseLangChain4jAgent extends CamelTestSupport {
+
+ protected ChatModel chatModel;
+ protected ChatMemoryProvider chatMemoryProvider;
+ protected RetrievalAugmentor retrievalAugmentor;
+ protected PersistentChatMemoryStore chatMemoryStore;
+
+ protected static final int MEMORY_ID_SESSION = 42;
+
+ protected static final String USER_DATABASE = """
+ {"id": "123", "name": "John Smith", "membership": "Gold", "rentals": 15, "preferredVehicle": "SUV"}
+ """;
+
+ protected static final String WEATHER_INFO = "Sunny, 22°C, perfect driving conditions";
// Company knowledge base for RAG
- private static final String COMPANY_KNOWLEDGE_BASE = """
+ protected static final String COMPANY_KNOWLEDGE_BASE = """
Miles of Camels Car Rental - Company Information
BUSINESS HOURS:
@@ -84,28 +89,18 @@ public abstract class AbstractRAGTest extends CamelTestSupport {
""";
- protected ChatModel chatModel;
- private String openAiApiKey;
- protected RetrievalAugmentor retrievalAugmentor;
-
- @Override
- protected void setupResources() throws Exception {
- super.setupResources();
+ protected ChatModel createChatModel(String apiKey, String baseUrl) {
+ OpenAiChatModel.OpenAiChatModelBuilder builder = OpenAiChatModel.builder();
- openAiApiKey = System.getenv("OPENAI_API_KEY");
- if (openAiApiKey == null || openAiApiKey.trim().isEmpty()) {
- throw new IllegalStateException("OPENAI_API_KEY system property is required for testing");
+ if (baseUrl != null) {
+ builder.baseUrl(baseUrl);
}
- // Setup components
- chatModel = createChatModel();
- retrievalAugmentor = createRetrievalAugmentor();
- }
+ if (apiKey != null) {
+ builder.apiKey(apiKey);
+ }
- protected ChatModel createChatModel() {
- return OpenAiChatModel.builder()
- .apiKey(openAiApiKey)
- .modelName(GPT_4_O_MINI)
+ return builder.modelName(GPT_4_O_MINI)
.temperature(1.0)
.timeout(ofSeconds(60))
.logRequests(true)
@@ -113,37 +108,13 @@ public abstract class AbstractRAGTest extends CamelTestSupport {
.build();
}
- private RetrievalAugmentor createRetrievalAugmentor() {
- // Create document from knowledge base
- Document document = Document.from(COMPANY_KNOWLEDGE_BASE);
-
- // Split into chunks
- List<TextSegment> segments = DocumentSplitters.recursive(300, 100).split(document);
-
- // Create embeddings
- EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()
- .apiKey(openAiApiKey)
- .modelName("text-embedding-ada-002")
- .timeout(ofSeconds(30))
- .build();
-
- List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
-
- // Store in embedding store
- EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
- embeddingStore.addAll(embeddings, segments);
-
- // Create content retriever
- EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
- .embeddingStore(embeddingStore)
- .embeddingModel(embeddingModel)
- .maxResults(3)
- .minScore(0.6)
- .build();
-
- // Create a RetreivalAugmentor that uses only a content retriever : naive rag scenario
- return DefaultRetrievalAugmentor.builder()
- .contentRetriever(contentRetriever)
+ protected ChatMemoryProvider createMemoryProvider(ChatMemoryStore chatMemoryStore) {
+ // Create a message window memory that keeps the last 10 messages
+ ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
+ .id(memoryId)
+ .maxMessages(10)
+ .chatMemoryStore(chatMemoryStore)
.build();
+ return chatMemoryProvider;
}
}
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryServiceTest.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryServiceTest.java
new file mode 100644
index 00000000000..aae1003a4d1
--- /dev/null
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryServiceTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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 org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.pojos.PersistentChatMemoryStore;
+import org.apache.camel.component.langchain4j.agent.pojos.TestSuccessInputGuardrail;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LangChain4jAgentWithMemoryServiceTest extends BaseLangChain4jAgent {
+
+ @RegisterExtension
+ public static OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("Hi! Can you look up user 123 and tell me about our rental policies?")
+ .assertRequest(request -> {
+ // Both tools are part of the request
+ Assertions.assertThat(request).contains("QueryUserDatabaseByUserID", "GetCurrentWeatherInformation");
+ })
+ .invokeTool("QueryUserDatabaseByUserID")
+ .withParam("userId", "123")
+ .replyWithToolContent(" " + COMPANY_KNOWLEDGE_BASE)
+ .end()
+ .when("What's his preferred vehicle type?")
+ .assertRequest(request -> {
+ // Assert that memory is working as expected
+ Assertions.assertThat(request)
+ .contains("Hi! Can you look up user 123 and tell me about our rental policies?");
+ })
+ .replyWith("SUV")
+ .end()
+ .when("What's the weather in London?")
+ .invokeTool("GetCurrentWeatherInformation")
+ .withParam("location", "London")
+ .end()
+ .build();
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+
+ chatMemoryStore = new PersistentChatMemoryStore();
+ chatModel = createChatModel(null, openAIMock.getBaseUrl());
+ chatMemoryProvider = createMemoryProvider(chatMemoryStore);
+ }
+
+ @Test
+ public void testToolThenMemoryThenAnotherTool() throws Exception {
+ MockEndpoint mockEndpoint = this.context.getEndpoint("mock:agent-response", MockEndpoint.class);
+ mockEndpoint.expectedMessageCount(3);
+
+ AiAgentBody firstRequest = new AiAgentBody(
+ "Hi! Can you look up user 123 and tell me about our rental policies?",
+ null,
+ MEMORY_ID_SESSION);
+
+ String firstResponse = template.requestBody(
+ "direct:complete-agent",
+ firstRequest,
+ String.class);
+
+ assertNotNull(firstResponse, "First response should not be null");
+ Assertions.assertThat(firstResponse).contains("John Smith", "Gold")
+ .withFailMessage("Response should contain user information from tools");
+ Assertions.assertThat(firstResponse).contains("21", "age", "rental")
+ .withFailMessage("Response should contain rental policy information from RAG");
+
+ // Second interaction: Follow-up question
+ AiAgentBody secondRequest = new AiAgentBody(
+ "What's his preferred vehicle type?",
+ null,
+ MEMORY_ID_SESSION);
+
+ String secondResponse = template.requestBody(
+ "direct:complete-agent",
+ secondRequest,
+ String.class);
+
+ assertNotNull(secondResponse, "Second response should not be null");
+ Assertions.assertThat(secondResponse).isEqualTo("SUV");
+
+ // Third interaction: Follow-up weather question
+ AiAgentBody thirdRequest = new AiAgentBody(
+ "What's the weather in London?",
+ null,
+ MEMORY_ID_SESSION);
+
+ String thirdResponse = template.requestBody(
+ "direct:complete-agent",
+ thirdRequest,
+ String.class);
+
+ assertNotNull(thirdRequest, "Third response should not be null");
+ Assertions.assertThat(thirdResponse).contains(WEATHER_INFO);
+
+ mockEndpoint.assertIsSatisfied();
+
+ // Verify guardrails were called
+ assertTrue(TestSuccessInputGuardrail.wasValidated(),
+ "Input guardrail should have been called");
+
+ // Verify memory persistence
+ assertTrue(chatMemoryStore.getMemoryCount() > 0, "Memory should be persisted");
+ assertFalse(chatMemoryStore.getMessages(MEMORY_ID_SESSION).isEmpty(), "Session memory should contain messages");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ this.context.getRegistry().bind("chatModel", chatModel);
+ this.context.getRegistry().bind("memoryProvider", chatMemoryProvider);
+
+ return new RouteBuilder() {
+ public void configure() {
+ // Tools + Memory + Guardrails + RAG
+ from("direct:complete-agent")
+ .to("langchain4j-agent:complete?chatModel=#chatModel&chatMemoryProvider=#memoryProvider&tags=users,weather"
+ +
+ "&inputGuardrails=org.apache.camel.component.langchain4j.agent.pojos.TestSuccessInputGuardrail")
+ .to("mock:agent-response");
+
+ // Tool routes for function calling
+ from("langchain4j-tools:userDb?tags=users&description=Query user database by user ID¶meter.userId=string")
+ .setBody(constant(USER_DATABASE));
+
+ from("langchain4j-tools:weatherService?tags=weather&description=Get current weather information¶meter.location=string")
+ .setBody(constant("{\"weather\": \"" + WEATHER_INFO + "\", \"location\": \"Current Location\"}"));
+ }
+ };
+ }
+}
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AbstractRAGTest.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
similarity index 56%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AbstractRAGTest.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
index ede421457c4..4f08e3a69c0 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/AbstractRAGTest.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/AbstractRAGIT.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import java.util.List;
@@ -22,71 +22,20 @@ import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
-import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
-import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.rag.DefaultRetrievalAugmentor;
import dev.langchain4j.rag.RetrievalAugmentor;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.component.langchain4j.agent.BaseLangChain4jAgent;
-import static dev.langchain4j.model.openai.OpenAiChatModelName.GPT_4_O_MINI;
import static java.time.Duration.ofSeconds;
-public abstract class AbstractRAGTest extends CamelTestSupport {
+public abstract class AbstractRAGIT extends BaseLangChain4jAgent {
- // Company knowledge base for RAG
- private static final String COMPANY_KNOWLEDGE_BASE = """
- Miles of Camels Car Rental - Company Information
-
- BUSINESS HOURS:
- Monday-Friday: 8:00 AM - 6:00 PM
- Saturday: 9:00 AM - 4:00 PM
- Sunday: Closed
-
- RENTAL AGREEMENT
- - This agreement is between Miles of Camels Car Rental ("Company") and the customer ("Renter").
-
- RENTAL POLICIES:
- - Minimum age: 21 years old
- - Valid driver's license required
- - Credit card required for security deposit
- - Full tank of gas required at return
-
- VEHICLE FLEET:
- - Economy cars: Starting at $29/day
- - Mid-size cars: Starting at $39/day
- - SUVs: Starting at $59/day
- - Luxury vehicles: Starting at $89/day
-
- CANCELLATION POLICY
- - Cancellations made 24 hours before pickup: Full refund
- - Cancellations made 12-24 hours before pickup: 50% refund
- - Cancellations made less than 12 hours before pickup: No refund
-
- VEHICLE RETURN
- - Vehicles must be returned with the same fuel level as at pickup.
- - Late returns incur a fee of $25 per hour or fraction thereof.
-
- DAMAGE POLICY
- - Minor damages under $200: Covered by insurance
- - Major damages over $200: Customer responsibility
-
- INSURANCE
- - Basic insurance is included. Premium insurance available for $15/day.
-
- AGE REQUIREMENTS
- - Minimum age: 21 years old
- - Drivers under 25: Additional surcharge of $20/day
-
- """;
-
- protected ChatModel chatModel;
private String openAiApiKey;
- protected RetrievalAugmentor retrievalAugmentor;
@Override
protected void setupResources() throws Exception {
@@ -98,21 +47,10 @@ public abstract class AbstractRAGTest extends CamelTestSupport {
}
// Setup components
- chatModel = createChatModel();
+ chatModel = createChatModel(openAiApiKey, null);
retrievalAugmentor = createRetrievalAugmentor();
}
- protected ChatModel createChatModel() {
- return OpenAiChatModel.builder()
- .apiKey(openAiApiKey)
- .modelName(GPT_4_O_MINI)
- .temperature(1.0)
- .timeout(ofSeconds(60))
- .logRequests(true)
- .logResponses(true)
- .build();
- }
-
private RetrievalAugmentor createRetrievalAugmentor() {
// Create document from knowledge base
Document document = Document.from(COMPANY_KNOWLEDGE_BASE);
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentGuardrailsIntegrationIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
similarity index 99%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentGuardrailsIntegrationIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
index 557856755b7..ad3f437d715 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentGuardrailsIntegrationIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentGuardrailsIntegrationIT.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentNaiveRagIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentNaiveRagIT.java
similarity index 98%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentNaiveRagIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentNaiveRagIT.java
index eebe8b8cf19..eb039465843 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentNaiveRagIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentNaiveRagIT.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
@@ -26,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnabledIfSystemProperty(named = "OPENAI_API_KEY", matches = ".*", disabledReason = "OpenAI API key required")
-public class LangChain4jAgentNaiveRagIT extends AbstractRAGTest {
+public class LangChain4jAgentNaiveRagIT extends AbstractRAGIT {
private static final String SYSTEM_MESSAGE_CUSTOMER_SERVICE
= "You are a friendly customer service representative for Miles of Camels Car Rental. Always be helpful and polite.";
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentServiceIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentServiceIT.java
similarity index 96%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentServiceIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentServiceIT.java
index 15270bbe1a4..d4149596a4a 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentServiceIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentServiceIT.java
@@ -14,9 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.AiAgentBody;
import org.apache.camel.component.langchain4j.agent.pojos.TestJsonOutputGuardrail;
import org.apache.camel.component.langchain4j.agent.pojos.TestSuccessInputGuardrail;
import org.apache.camel.component.mock.MockEndpoint;
@@ -28,7 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnabledIfSystemProperty(named = "OPENAI_API_KEY", matches = ".*", disabledReason = "OpenAI API key required")
-public class LangChain4jAgentServiceIT extends AbstractRAGTest {
+public class LangChain4jAgentServiceIT extends AbstractRAGIT {
private static final String USER_DATABASE = """
{"id": "123", "name": "John Smith", "membership": "Gold", "rentals": 15, "preferredVehicle": "SUV"}
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
similarity index 99%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
index c8b98f164cb..2cf92ccc68f 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithMemoryIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryIT.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import java.util.Map;
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgenWithMemoryServiceIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryServiceIT.java
similarity index 97%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgenWithMemoryServiceIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryServiceIT.java
index 48d48dd6c6e..4749e4557c8 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgenWithMemoryServiceIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithMemoryServiceIT.java
@@ -14,11 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.AiAgentBody;
import org.apache.camel.component.langchain4j.agent.pojos.PersistentChatMemoryStore;
import org.apache.camel.component.langchain4j.agent.pojos.TestJsonOutputGuardrail;
import org.apache.camel.component.langchain4j.agent.pojos.TestSuccessInputGuardrail;
@@ -33,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.*;
* Class to test a mix match between all those different concepts : memory / tool / RAG / guardrails
*/
@EnabledIfSystemProperty(named = "OPENAI_API_KEY", matches = ".*", disabledReason = "OpenAI API key required")
-public class LangChain4jAgenWithMemoryServiceIT extends AbstractRAGTest {
+public class LangChain4jAgentWithMemoryServiceIT extends AbstractRAGIT {
private static final int MEMORY_ID_SESSION = 42;
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithToolsIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
similarity index 97%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithToolsIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
index 6b7e2ed2334..29a1d2e5a35 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentWithToolsIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jAgentWithToolsIT.java
@@ -14,11 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.AiAgentBody;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;
@@ -90,7 +91,6 @@ public class LangChain4jAgentWithToolsIT extends CamelTestSupport {
mockEndpoint.assertIsSatisfied();
assertNotNull(response, "AI response should not be null");
- System.out.println("response testAgentWithWeatherTools is :: " + response);
assertTrue(response.toLowerCase().contains(WEATHER_INFO_1),
"Response should contain weather information from the weather tool");
assertTrue(response.toLowerCase().contains(WEATHER_INFO_2),
@@ -135,7 +135,6 @@ public class LangChain4jAgentWithToolsIT extends CamelTestSupport {
mockEndpoint.assertIsSatisfied();
assertNotNull(response, "AI response should not be null");
- System.out.println("response testAgentWithConfiguredTags is :: " + response);
assertTrue(response.toLowerCase().contains(WEATHER_INFO_1),
"Response should contain weather information from the weather tool");
assertTrue(response.toLowerCase().contains(WEATHER_INFO_2),
diff --git a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jSimpleAgentIT.java b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
similarity index 97%
rename from components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jSimpleAgentIT.java
rename to components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
index 45b8fbec930..b18d0ae6ef3 100644
--- a/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/LangChain4jSimpleAgentIT.java
+++ b/components/camel-ai/camel-langchain4j-agent/src/test/java/org/apache/camel/component/langchain4j/agent/integration/LangChain4jSimpleAgentIT.java
@@ -14,11 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.camel.component.langchain4j.agent;
+package org.apache.camel.component.langchain4j.agent.integration;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.langchain4j.agent.AiAgentBody;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.junit.jupiter.api.Test;