diff --git a/samples/chatbot/pom.xml b/samples/chatbot/pom.xml index 5f3f92220..e736ed086 100644 --- a/samples/chatbot/pom.xml +++ b/samples/chatbot/pom.xml @@ -1,10 +1,11 @@ - + 4.0.0 io.quarkiverse.langchain4j quarkus-langchain4j-sample-chatbot - Quarkus LangChain4j - Sample - Chatbot & RAG + Quarkus LangChain4j - Sample - Chatbot & RAG 1.0-SNAPSHOT @@ -15,24 +16,12 @@ UTF-8 quarkus-bom io.quarkus - 3.20.0 + 3.26.4 true + true 3.2.5 - 1.2.0.CR3 - - - - ${quarkus.platform.group-id} - ${quarkus.platform.artifact-id} - ${quarkus.platform.version} - pom - import - - - - io.quarkus @@ -43,14 +32,14 @@ quarkus-websockets-next - io.quarkiverse.langchain4j - quarkus-langchain4j-openai - ${quarkus-langchain4j.version} + io.quarkus + quarkus-junit5 + test - io.quarkiverse.langchain4j - quarkus-langchain4j-redis - ${quarkus-langchain4j.version} + org.awaitility + awaitility + test @@ -71,34 +60,6 @@ 0.2.1 runtime - - - - io.quarkiverse.langchain4j - quarkus-langchain4j-openai-deployment - ${quarkus-langchain4j.version} - test - pom - - - * - * - - - - - io.quarkiverse.langchain4j - quarkus-langchain4j-redis-deployment - ${quarkus-langchain4j.version} - test - pom - - - * - * - - - @@ -132,6 +93,76 @@ + + default-project-deps + + + !platform-deps + + + + 1.2.0.CR3 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai + ${quarkus-langchain4j.version} + + + io.quarkiverse.langchain4j + quarkus-langchain4j-redis + ${quarkus-langchain4j.version} + + + + + platform-deps + + + platform-deps + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + ${quarkus.platform.group-id} + quarkus-langchain4j-bom + ${quarkus.platform.version} + pom + import + + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-openai + + + io.quarkiverse.langchain4j + quarkus-langchain4j-redis + + + native @@ -152,8 +183,11 @@ - ${project.build.directory}/${project.build.finalName}-runner - org.jboss.logmanager.LogManager + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + ${maven.home} @@ -163,6 +197,7 @@ + false native diff --git a/samples/chatbot/src/test/java/io/quarkiverse/langchain4j/tests/rag/RAGIT.java b/samples/chatbot/src/test/java/io/quarkiverse/langchain4j/tests/rag/RAGIT.java new file mode 100644 index 000000000..b046ecb42 --- /dev/null +++ b/samples/chatbot/src/test/java/io/quarkiverse/langchain4j/tests/rag/RAGIT.java @@ -0,0 +1,8 @@ +package io.quarkiverse.langchain4j.tests.rag; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class RAGIT extends RAGTest { + +} \ No newline at end of file diff --git a/samples/chatbot/src/test/java/io/quarkiverse/langchain4j/tests/rag/RAGTest.java b/samples/chatbot/src/test/java/io/quarkiverse/langchain4j/tests/rag/RAGTest.java new file mode 100644 index 000000000..788d0d62c --- /dev/null +++ b/samples/chatbot/src/test/java/io/quarkiverse/langchain4j/tests/rag/RAGTest.java @@ -0,0 +1,112 @@ +package io.quarkiverse.langchain4j.tests.rag; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import org.awaitility.Awaitility; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@QuarkusTest +public class RAGTest { + private static final Logger LOG = Logger.getLogger(RAGTest.class); + + private List answers; + private WebSocket webSocket; + + @TestHTTPResource + URL url; + + @BeforeEach + void setUp() throws URISyntaxException, ExecutionException, InterruptedException { + String socket = "/chatbot"; + URI uri = new URI("ws", null, + url.getHost(), + url.getPort(), + socket, null, null); + LOG.info("Connecting to: " + socket + " at " + uri); + answers = Collections.synchronizedList(new ArrayList<>()); + webSocket = HttpClient.newHttpClient().newWebSocketBuilder() + .buildAsync(uri, new WebSocketListener(answers)).get(); + } + + @AfterEach + void tearDown() throws ExecutionException, InterruptedException { + answers = null; + webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "").get(); + } + + @Test + public void smoke() { + Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + Assertions.assertFalse(answers.isEmpty()); + }); + Assertions.assertEquals("Hello, I'm Bob, how can I help you?", answers.get(0)); + } + + @Test + public void documentBasedAnswer() throws InterruptedException { + // Input tokens are usually much cheaper, than output tokens, so let's stop the LLM from talking too much + String prompt = "What is the opening deposit (in USD) for a standard savings account? Answer with number only"; + webSocket.sendText(prompt, true); + + // The answer is split to many messages due to Multi in the Bot. + // We need to wait for the end of the answer, detected as no new messages during a second. + // The prompt should protect against this, but it is not guaranteed. + int repeats = 0; + int lastSize=0; + while (answers.size() <= 1 || answers.size()!=lastSize) { + if (repeats++ > 10) { + LOG.warn("We have waited for: " + repeats + " seconds and it is too much!"); + break; + } + lastSize=answers.size(); + Thread.sleep(1000); + } + + String response = String.join("", answers); + Assertions.assertTrue(response.contains("25")); + } + + class WebSocketListener implements WebSocket.Listener { + private final List answers; + private StringBuilder current; + + WebSocketListener(List answers) { + this.answers = answers; + } + + @Override + public void onOpen(WebSocket webSocket) { + WebSocket.Listener.super.onOpen(webSocket); + } + + @Override + public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { + LOG.info("Message: " + data); + if (current == null) { + current = new StringBuilder(); + } + current.append(data); + if (last) { + answers.add(current.toString()); + current = null; + } + return WebSocket.Listener.super.onText(webSocket, data, last); + } + } +}