diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/Langchain4jDevUIProcessor.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/Langchain4jDevUIProcessor.java index 6af18281d..ea745ff13 100644 --- a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/Langchain4jDevUIProcessor.java +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/Langchain4jDevUIProcessor.java @@ -8,6 +8,8 @@ import io.quarkiverse.langchain4j.deployment.EmbeddingModelBuildItem; import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem; import io.quarkiverse.langchain4j.deployment.ToolsMetadataBuildItem; +import io.quarkiverse.langchain4j.deployment.items.ChatModelProviderCandidateBuildItem; +import io.quarkiverse.langchain4j.runtime.devui.ChatJsonRPCService; import io.quarkiverse.langchain4j.runtime.devui.EmbeddingStoreJsonRPCService; import io.quarkiverse.langchain4j.runtime.tool.ToolMethodCreateInfo; import io.quarkus.deployment.IsDevelopment; @@ -23,7 +25,8 @@ public class Langchain4jDevUIProcessor { CardPageBuildItem cardPage(List aiServices, ToolsMetadataBuildItem toolsMetadataBuildItem, List embeddingModelBuildItem, - List embeddingStoreBuildItem) { + List embeddingStoreBuildItem, + List chatModelCandidates) { CardPageBuildItem card = new CardPageBuildItem(); addAiServicesPage(card, aiServices); if (toolsMetadataBuildItem != null) { @@ -34,6 +37,9 @@ CardPageBuildItem cardPage(List aiServices, if (embeddingModelBuildItem.size() == 1 && embeddingStoreBuildItem.size() == 1) { addEmbeddingStorePage(card); } + if (!chatModelCandidates.isEmpty()) { + addChatPage(card); + } return card; } @@ -74,14 +80,23 @@ private void addToolsPage(CardPageBuildItem card, ToolsMetadataBuildItem metadat .icon("font-awesome-solid:toolbox")); } + private void addChatPage(CardPageBuildItem card) { + card.addPage(Page.webComponentPageBuilder().title("Chat") + .componentLink("qwc-chat.js") + .icon("font-awesome-solid:comments")); + } + @BuildStep(onlyIf = IsDevelopment.class) void jsonRpcProviders(BuildProducer producers, List embeddingModelBuildItem, - List embeddingStoreBuildItem) { + List embeddingStoreBuildItem, + List chatModelCandidates) { if (embeddingModelBuildItem.size() == 1 && embeddingStoreBuildItem.size() == 1) { producers.produce(new JsonRPCProvidersBuildItem(EmbeddingStoreJsonRPCService.class)); } - + if (!chatModelCandidates.isEmpty()) { + producers.produce(new JsonRPCProvidersBuildItem(ChatJsonRPCService.class)); + } } } diff --git a/core/deployment/src/main/resources/dev-ui/qwc-chat.js b/core/deployment/src/main/resources/dev-ui/qwc-chat.js new file mode 100644 index 000000000..50d22e3cc --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/qwc-chat.js @@ -0,0 +1,89 @@ +import {html, LitElement} from 'lit'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-column.js'; +import '@vaadin/text-area'; +import '@vaadin/button'; +import { JsonRpc } from 'jsonrpc'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; + +export class QwcChat extends LitElement { + + jsonRpc = new JsonRpc(this); + + static properties = { + "_chatHistory": {state: true} + } + + constructor() { + super(); + this._chatHistory = []; + } + + render() { + return html` +

Chat

+
+
+ this._doChat( + this.shadowRoot.getElementById('chat-message').value + )}>Submit + +
+ this._reset()}>New conversation +
+ ${this._renderHistory()} + `; + } + + _doChat(message) { + // if no chat history exists, start a new conversation + if(this._chatHistory.length === 0) { + var systemMessage = this.shadowRoot.getElementById('system-message').value; + this._chatHistory = [{message: message, type:"User"}, {type: "System", message: systemMessage}]; + this.shadowRoot.getElementById('chat-message').value = ""; + this.shadowRoot.getElementById('chat-button').disabled = true; + this.requestUpdate(); + this.jsonRpc.newConversation({message: message, systemMessage: systemMessage}).then(jsonRpcResponse => { + this._chatHistory = [{message: jsonRpcResponse.result, type:"AI"}].concat(this._chatHistory); + this.shadowRoot.getElementById('chat-button').disabled = null; + this.requestUpdate(); + }); + } else { + this._chatHistory = [{message: message, type: "User"}].concat(this._chatHistory); + this.requestUpdate(); + this.shadowRoot.getElementById('chat-message').value = ""; + this.shadowRoot.getElementById('chat-button').disabled = true; + this.jsonRpc.chat({message: message}).then(jsonRpcResponse => { + this._chatHistory = [{message: jsonRpcResponse.result, type: "AI"}].concat(this._chatHistory); + this.shadowRoot.getElementById('chat-button').disabled = null; + this.requestUpdate(); + }); + } + } + + _reset() { + var systemMessage = this.shadowRoot.getElementById('system-message').value; + if(systemMessage) { + this._chatHistory = [{type: "System", message: systemMessage}]; + } + this.shadowRoot.getElementById('chat-button').disabled = null; + this.jsonRpc.reset({systemMessage: systemMessage}); + } + + _renderHistory() { + return html` + + + + + + `; + } + +} + +customElements.define('qwc-chat', QwcChat); \ No newline at end of file diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/ChatJsonRPCService.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/ChatJsonRPCService.java new file mode 100644 index 000000000..c3a52e143 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/ChatJsonRPCService.java @@ -0,0 +1,51 @@ +package io.quarkiverse.langchain4j.runtime.devui; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.inject.Inject; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.memory.chat.ChatMemoryProvider; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.output.Response; + +public class ChatJsonRPCService { + + @Inject + ChatLanguageModel model; + + @Inject + ChatMemoryProvider memoryProvider; + + private AtomicReference currentMemory = new AtomicReference<>(); + + public String reset(String systemMessage) { + if (currentMemory.get() != null) { + currentMemory.get().clear(); + } + ChatMemory memory = memoryProvider.get(ThreadLocalRandom.current().nextLong()); + currentMemory.set(memory); + if (systemMessage != null && !systemMessage.isEmpty()) { + memory.add(new SystemMessage(systemMessage)); + } + return "OK"; + } + + public String newConversation(String systemMessage, String message) { + reset(systemMessage); + return chat(message); + } + + public String chat(String message) { + ChatMemory memory = currentMemory.get(); + memory.add(new UserMessage(message)); + Response response = model.generate(memory.messages()); + memory.add(response.content()); + return response.content().text(); + } + +}