From 249b8c4ae25fe76e79f91ad5bf62295fbb46cc19 Mon Sep 17 00:00:00 2001 From: Jan Martiska Date: Fri, 1 Dec 2023 11:46:10 +0100 Subject: [PATCH] Dev UI for Langchain4j --- .../chroma/deployment/ChromaProcessor.java | 5 +- core/deployment/pom.xml | 10 ++ .../deployment/EmbeddingModelBuildItem.java | 9 ++ .../deployment/EmbeddingStoreBuildItem.java | 9 ++ .../InProcessEmbeddingProcessor.java | 4 +- .../langchain4j/deployment/ToolProcessor.java | 4 +- .../deployment/ToolsMetadataBuildItem.java | 23 ++++ .../deployment/devui/AiServiceInfo.java | 23 ++++ .../devui/Langchain4jDevUIProcessor.java | 85 ++++++++++++ .../deployment/devui/ToolMethodInfo.java | 28 ++++ .../main/resources/dev-ui/qwc-aiservices.js | 85 ++++++++++++ .../resources/dev-ui/qwc-embedding-store.js | 129 ++++++++++++++++++ .../src/main/resources/dev-ui/qwc-tools.js | 94 +++++++++++++ core/runtime/pom.xml | 4 + .../devui/EmbeddingStoreJsonRPCService.java | 70 ++++++++++ docs/modules/ROOT/pages/index.adoc | 4 + integration-tests/devui/pom.xml | 68 +++++++++ .../devui/Langchain4jDevUIJsonRpcTest.java | 45 ++++++ integration-tests/pom.xml | 1 + .../pinecone/PineconeProcessor.java | 5 +- .../redis/RedisEmbeddingStoreProcessor.java | 6 +- 21 files changed, 705 insertions(+), 6 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingModelBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingStoreBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolsMetadataBuildItem.java create mode 100644 core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/AiServiceInfo.java create mode 100644 core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/Langchain4jDevUIProcessor.java create mode 100644 core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/ToolMethodInfo.java create mode 100644 core/deployment/src/main/resources/dev-ui/qwc-aiservices.js create mode 100644 core/deployment/src/main/resources/dev-ui/qwc-embedding-store.js create mode 100644 core/deployment/src/main/resources/dev-ui/qwc-tools.js create mode 100644 core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/EmbeddingStoreJsonRPCService.java create mode 100644 integration-tests/devui/pom.xml create mode 100644 integration-tests/devui/src/test/java/devui/Langchain4jDevUIJsonRpcTest.java diff --git a/chroma/deployment/src/main/java/io/quarkiverse/langchain4j/chroma/deployment/ChromaProcessor.java b/chroma/deployment/src/main/java/io/quarkiverse/langchain4j/chroma/deployment/ChromaProcessor.java index 990f106d0..4335a96be 100644 --- a/chroma/deployment/src/main/java/io/quarkiverse/langchain4j/chroma/deployment/ChromaProcessor.java +++ b/chroma/deployment/src/main/java/io/quarkiverse/langchain4j/chroma/deployment/ChromaProcessor.java @@ -11,6 +11,7 @@ import io.quarkiverse.langchain4j.chroma.ChromaEmbeddingStore; import io.quarkiverse.langchain4j.chroma.runtime.ChromaConfig; import io.quarkiverse.langchain4j.chroma.runtime.ChromaRecorder; +import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -34,7 +35,8 @@ FeatureBuildItem feature() { public void createBean( BuildProducer beanProducer, ChromaRecorder recorder, - ChromaConfig config) { + ChromaConfig config, + BuildProducer embeddingStoreProducer) { beanProducer.produce(SyntheticBeanBuildItem .configure(CHROMA_EMBEDDING_STORE) .types(ClassType.create(EmbeddingStore.class), @@ -45,5 +47,6 @@ public void createBean( .scope(ApplicationScoped.class) .supplier(recorder.chromaStoreSupplier(config)) .done()); + embeddingStoreProducer.produce(new EmbeddingStoreBuildItem()); } } diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index b3bbe5ca9..132fbc5e4 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -21,11 +21,20 @@ io.quarkus quarkus-qute-deployment + + io.quarkus + quarkus-vertx-http-deployment + io.quarkiverse.poi quarkus-poi-deployment ${quarkus-poi.version} + + io.quarkus + quarkus-vertx-http-dev-ui-tests + test + io.quarkiverse.langchain4j @@ -47,6 +56,7 @@ quarkus-junit5-internal test + org.assertj assertj-core diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingModelBuildItem.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingModelBuildItem.java new file mode 100644 index 000000000..3e22e4596 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingModelBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkiverse.langchain4j.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Marker that an embedding model was registered in the CDI container. + */ +public final class EmbeddingModelBuildItem extends MultiBuildItem { +} diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingStoreBuildItem.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingStoreBuildItem.java new file mode 100644 index 000000000..9ab41e05b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/EmbeddingStoreBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkiverse.langchain4j.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Marker that an embedding store was registered in the CDI container. + */ +public final class EmbeddingStoreBuildItem extends MultiBuildItem { +} diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/InProcessEmbeddingProcessor.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/InProcessEmbeddingProcessor.java index 9c8a0b8ac..cc215a1d6 100644 --- a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/InProcessEmbeddingProcessor.java +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/InProcessEmbeddingProcessor.java @@ -120,7 +120,8 @@ InProcessEmbeddingBuildItem e5_small_v2() { @Record(ExecutionTime.RUNTIME_INIT) void exposeInProcessEmbeddingBeans(InProcessEmbeddingRecorder recorder, List embeddings, - BuildProducer beanProducer) { + BuildProducer beanProducer, + BuildProducer embeddingModelProducer) { for (InProcessEmbeddingBuildItem embedding : embeddings) { beanProducer.produce(SyntheticBeanBuildItem @@ -131,6 +132,7 @@ void exposeInProcessEmbeddingBeans(InProcessEmbeddingRecorder recorder, .scope(ApplicationScoped.class) .supplier(recorder.instantiate(embedding.className())) .done()); + embeddingModelProducer.produce(new EmbeddingModelBuildItem()); } } diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolProcessor.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolProcessor.java index 541ed3a12..e0ba929ac 100644 --- a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolProcessor.java +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolProcessor.java @@ -95,7 +95,8 @@ public void handleTools(CombinedIndexBuildItem indexBuildItem, BuildProducer transformerProducer, BuildProducer generatedClassProducer, BuildProducer reflectiveClassProducer, - BuildProducer validation) { + BuildProducer validation, + BuildProducer toolsMetadataProducer) { recorderContext.registerSubstitution(ToolSpecification.class, ToolSpecificationObjectSubstitution.Serialized.class, ToolSpecificationObjectSubstitution.class); recorderContext.registerSubstitution(ToolParameters.class, ToolParametersObjectSubstitution.Serialized.class, @@ -229,6 +230,7 @@ public void handleTools(CombinedIndexBuildItem indexBuildItem, .build()); } + toolsMetadataProducer.produce(new ToolsMetadataBuildItem(metadata)); recorder.setMetadata(metadata); } diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolsMetadataBuildItem.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolsMetadataBuildItem.java new file mode 100644 index 000000000..201892a42 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/ToolsMetadataBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkiverse.langchain4j.deployment; + +import java.util.List; +import java.util.Map; + +import io.quarkiverse.langchain4j.runtime.tool.ToolMethodCreateInfo; +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Holds metadata about tools discovered at build time + */ +public final class ToolsMetadataBuildItem extends SimpleBuildItem { + + Map> metadata; + + public ToolsMetadataBuildItem(Map> metadata) { + this.metadata = metadata; + } + + public Map> getMetadata() { + return metadata; + } +} diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/AiServiceInfo.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/AiServiceInfo.java new file mode 100644 index 000000000..4648c9218 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/AiServiceInfo.java @@ -0,0 +1,23 @@ +package io.quarkiverse.langchain4j.deployment.devui; + +import java.util.List; + +public class AiServiceInfo { + + private String clazz; + private List tools; + + public AiServiceInfo(String clazz, List tools) { + this.clazz = clazz; + this.tools = tools; + } + + public List getTools() { + return tools; + } + + public String getClazz() { + return clazz; + } + +} 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 new file mode 100644 index 000000000..fc17c4575 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/Langchain4jDevUIProcessor.java @@ -0,0 +1,85 @@ +package io.quarkiverse.langchain4j.deployment.devui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.quarkiverse.langchain4j.deployment.DeclarativeAiServiceBuildItem; +import io.quarkiverse.langchain4j.deployment.EmbeddingModelBuildItem; +import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem; +import io.quarkiverse.langchain4j.deployment.ToolsMetadataBuildItem; +import io.quarkiverse.langchain4j.runtime.devui.EmbeddingStoreJsonRPCService; +import io.quarkiverse.langchain4j.runtime.tool.ToolMethodCreateInfo; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; + +public class Langchain4jDevUIProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + CardPageBuildItem cardPage(List aiServices, + ToolsMetadataBuildItem toolsMetadataBuildItem, + List embeddingModelBuildItem, + List embeddingStoreBuildItem) { + CardPageBuildItem card = new CardPageBuildItem(); + addAiServicesPage(card, aiServices); + addToolsPage(card, toolsMetadataBuildItem); + // for now, add the embedding store page only if there is a single embedding model and a single embedding store + // if we allow more in the future, we need a way to specify which ones to use for the page + if (embeddingModelBuildItem.size() == 1 && embeddingStoreBuildItem.size() == 1) { + addEmbeddingStorePage(card); + } + return card; + } + + private void addEmbeddingStorePage(CardPageBuildItem card) { + card.addPage(Page.webComponentPageBuilder().title("Embedding store") + .componentLink("qwc-embedding-store.js") + .icon("font-awesome-solid:database")); + } + + private void addAiServicesPage(CardPageBuildItem card, List aiServices) { + List infos = new ArrayList<>(); + for (DeclarativeAiServiceBuildItem aiService : aiServices) { + List tools = aiService.getToolDotNames().stream().map(dotName -> dotName.toString()).toList(); + infos.add(new AiServiceInfo(aiService.getServiceClassInfo().name().toString(), tools)); + } + + card.addBuildTimeData("aiservices", infos); + card.addPage(Page.webComponentPageBuilder().title("AI Services") + .componentLink("qwc-aiservices.js") + .staticLabel(String.valueOf(aiServices.size())) + .icon("font-awesome-solid:robot")); + } + + private void addToolsPage(CardPageBuildItem card, ToolsMetadataBuildItem metadataBuildItem) { + List infos = new ArrayList<>(); + Map> metadata = metadataBuildItem.getMetadata(); + for (Map.Entry> toolClassEntry : metadata.entrySet()) { + for (ToolMethodCreateInfo toolMethodCreateInfo : toolClassEntry.getValue()) { + infos.add(new ToolMethodInfo(toolClassEntry.getKey(), + toolMethodCreateInfo.getMethodName(), + toolMethodCreateInfo.getToolSpecification().description())); + } + } + card.addBuildTimeData("tools", infos); + card.addPage(Page.webComponentPageBuilder().title("Tools") + .componentLink("qwc-tools.js") + .staticLabel(String.valueOf(infos.size())) + .icon("font-awesome-solid:toolbox")); + } + + @BuildStep(onlyIf = IsDevelopment.class) + void jsonRpcProviders(BuildProducer producers, + List embeddingModelBuildItem, + List embeddingStoreBuildItem) { + if (embeddingModelBuildItem.size() == 1 && embeddingStoreBuildItem.size() == 1) { + producers.produce(new JsonRPCProvidersBuildItem(EmbeddingStoreJsonRPCService.class)); + } + + } + +} diff --git a/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/ToolMethodInfo.java b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/ToolMethodInfo.java new file mode 100644 index 000000000..660bb6f05 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkiverse/langchain4j/deployment/devui/ToolMethodInfo.java @@ -0,0 +1,28 @@ +package io.quarkiverse.langchain4j.deployment.devui; + +public class ToolMethodInfo { + + private String className; + + private String name; + + private String description; + + public ToolMethodInfo(String className, String name, String description) { + this.className = className; + this.name = name; + this.description = description; + } + + public String getClassName() { + return className; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } +} diff --git a/core/deployment/src/main/resources/dev-ui/qwc-aiservices.js b/core/deployment/src/main/resources/dev-ui/qwc-aiservices.js new file mode 100644 index 000000000..5d4d11c95 --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/qwc-aiservices.js @@ -0,0 +1,85 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/grid'; +import 'qui-alert'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +import {aiservices} from 'build-time-data'; + + +export class QwcAiservices extends LitElement { + + static styles = css` + .button { + cursor: pointer; + } + .clearIcon { + color: orange; + } + .message { + padding: 15px; + text-align: center; + margin-left: 20%; + margin-right: 20%; + border: 2px solid orange; + border-radius: 10px; + font-size: large; + } + `; + + static properties = { + "_aiservices": {state: true}, + "_message": {state: true} + } + + connectedCallback() { + super.connectedCallback(); + this._aiservices = aiservices; + } + + render() { + if (this._aiservices) { + return this._renderAiServiceTable(); + } else { + return html`Loading AI services...`; + } + } + + _renderAiServiceTable() { + return html` + ${this._message} + + + + + + `; + } + + _nameRenderer(aiservice) { + return html`${aiservice.clazz}`; + } + + _toolsRenderer(aiservice) { + if (aiservice.tools && aiservice.tools.length > 0) { + return html` + ${aiservice.tools.map(tool => + html`
${tool}
` + )}
`; + } + } + +} +customElements.define('qwc-aiservices', QwcAiservices); \ No newline at end of file diff --git a/core/deployment/src/main/resources/dev-ui/qwc-embedding-store.js b/core/deployment/src/main/resources/dev-ui/qwc-embedding-store.js new file mode 100644 index 000000000..a6e3e765b --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/qwc-embedding-store.js @@ -0,0 +1,129 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/grid'; +import 'qui-alert'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + + +export class QwcEmbeddingStore extends LitElement { + + jsonRpc = new JsonRpc(this); + + static styles = css` + .button { + cursor: pointer; + } + .clearIcon { + color: orange; + } + .message { + padding: 15px; + text-align: center; + margin-left: 20%; + margin-right: 20%; + border: 2px solid orange; + border-radius: 10px; + font-size: large; + } + `; + + static properties = { + "_addEmbeddingConfirmation": {state: true}, + "_relevantEmbeddingsOutput": {state: true} + } + + connectedCallback() { + super.connectedCallback(); + } + + render() { + return html` +

Add a new embedding

+ ${this._addEmbeddingConfirmation} +
+
+
+ this._addEmbedding( + this.shadowRoot.getElementById('embedding-id').value, + this.shadowRoot.getElementById('embedding-text').value, + this.shadowRoot.getElementById('metadata').value + )}>Create and store + +

Search for relevant embeddings

+
+
+ this._findRelevant( + this.shadowRoot.getElementById('search-text').value, + this.shadowRoot.getElementById('search-limit').value + )}>Search
+ ${this._relevantEmbeddingsOutput} + `; + } + + _addEmbedding(id, text, metadata){ + this._addEmbeddingConfirmation = ''; + this.jsonRpc.add({id: id, text: text, metadata: metadata}).then(jsonRpcResponse => { + if(jsonRpcResponse.result === false) { + this._addEmbeddingConfirmation = html` + + The embedding could not be added: ${jsonRpcResponse} + `; + } else { + this._addEmbeddingConfirmation = html` + + The embedding was added with ID ${jsonRpcResponse.result}. + `; + } + }); + } + + _findRelevant(text, limit){ + this._relevantEmbeddingsOutput = ''; + this.jsonRpc.findRelevant({text: text, limit: limit}).then(jsonRpcResponse => { + this._relevantEmbeddingsOutput = html` + + + + + + + `; + }); + } + + _embeddingMatchIdRenderer(match) { + return html`${ match.embeddingId }` + } + + _embeddingMatchScoreRenderer(match) { + return html`${ match.score }` + } + + _embeddingMatchEmbeddedRenderer(match) { + return html`${ match.embedded }` + } + + _embeddingMatchMetadataRenderer(match) { + // return html`${ match.metadata }` + if (match.metadata && match.metadata.length > 0) { + return html` + ${match.metadata.map((entry) => + html`
${entry.key}:${entry.value}
` + )}
`; + } + } + + +} +customElements.define('qwc-embedding-store', QwcEmbeddingStore); \ No newline at end of file diff --git a/core/deployment/src/main/resources/dev-ui/qwc-tools.js b/core/deployment/src/main/resources/dev-ui/qwc-tools.js new file mode 100644 index 000000000..4c75f9cb3 --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/qwc-tools.js @@ -0,0 +1,94 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/grid'; +import 'qui-alert'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +import {tools} from 'build-time-data'; + + +export class QwcTools extends LitElement { + + static styles = css` + .button { + cursor: pointer; + } + .clearIcon { + color: orange; + } + .message { + padding: 15px; + text-align: center; + margin-left: 20%; + margin-right: 20%; + border: 2px solid orange; + border-radius: 10px; + font-size: large; + } + `; + + static properties = { + "_tools": {state: true}, + } + + connectedCallback() { + super.connectedCallback(); + this._tools = tools; + } + + render() { + if (this._tools) { + return this._renderToolTable(); + } else { + return html`Loading tools...`; + } + } + + _renderToolTable() { + return html` + + + + + + + + `; + } + + _actionRenderer(tool) { + return html` + this._reset(ds)} class="button"> + Reset + `; + } + + _classNameRenderer(tool) { + return html`${tool.className}`; + } + + + _nameRenderer(tool) { + return html`${tool.name}`; + } + + _descriptionRenderer(tool) { + return html`${tool.description}`; + } + +} +customElements.define('qwc-tools', QwcTools); \ No newline at end of file diff --git a/core/runtime/pom.xml b/core/runtime/pom.xml index 208eacd4a..f5bd5b5cb 100644 --- a/core/runtime/pom.xml +++ b/core/runtime/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-qute
+ + io.quarkus + quarkus-vertx-http + io.micrometer micrometer-core diff --git a/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/EmbeddingStoreJsonRPCService.java b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/EmbeddingStoreJsonRPCService.java new file mode 100644 index 000000000..e1fd618b5 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/langchain4j/runtime/devui/EmbeddingStoreJsonRPCService.java @@ -0,0 +1,70 @@ +package io.quarkiverse.langchain4j.runtime.devui; + +import java.util.regex.Pattern; + +import jakarta.inject.Inject; + +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingStore; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class EmbeddingStoreJsonRPCService { + + @Inject + EmbeddingStore embeddingStore; + + @Inject + EmbeddingModel embeddingModel; + + private static final Pattern COMMA_OR_NEWLINE = Pattern.compile(",|\\r?\\n"); + + public String add(String id, String text, String metadata) { + if (id == null || id.isEmpty()) { + return embeddingStore.add(embeddingModel.embed(text).content(), TextSegment.from(text, parseMetadata(metadata))); + } else { + embeddingStore.add(id, embeddingModel.embed(TextSegment.from(text, parseMetadata(metadata))).content()); + return id; + } + } + + private Metadata parseMetadata(String metadata) { + Metadata metadataObject = new Metadata(); + for (String metadataField : COMMA_OR_NEWLINE.split(metadata)) { + // FIXME: this doesn't allow any kind of escaping the `=` or `,` characters; do we need that? + String[] keyValue = metadataField.split("="); + if (keyValue.length == 2) { + metadataObject.add(keyValue[0].trim(), keyValue[1].trim()); + } + } + return metadataObject; + } + + // FIXME: the limit argument can be changed to int after https://github.com/quarkusio/quarkus/issues/37481 is fixed + // Langchain4jDevUIJsonRpcTest will need to be adjusted accordingly + public JsonArray findRelevant(String text, String limit) { + int limitInt = Integer.parseInt(limit); + JsonArray result = new JsonArray(); + for (EmbeddingMatch match : embeddingStore.findRelevant(embeddingModel.embed(text).content(), limitInt)) { + JsonObject matchJson = new JsonObject(); + matchJson.put("embeddingId", match.embeddingId()); + matchJson.put("score", match.score()); + matchJson.put("embedded", match.embedded() != null ? match.embedded().text() : null); + JsonArray metadata = new JsonArray(); + if (match.embedded() != null && match.embedded().metadata() != null) { + for (String key : match.embedded().metadata().asMap().keySet()) { + JsonObject metadataEntry = new JsonObject(); + metadataEntry.put("key", key); + metadataEntry.put("value", match.embedded().metadata().get(key)); + metadata.add(metadataEntry); + } + } + matchJson.put("metadata", metadata); + result.add(matchJson); + } + return result; + } +} diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 24dcf1af0..71b36c2d0 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -82,4 +82,8 @@ The extension offers the following advantages over using the vanilla https://git ** REST calls and JSON handling are performed using the libraries used throughout Quarkus *** Results in reduces library footprint *** Enables GraalVM native image compilation +* Dev UI features +** View table with information about AI services and tools +** Add embeddings into the embedding store +** Search for relevant embeddings in the embedding store diff --git a/integration-tests/devui/pom.xml b/integration-tests/devui/pom.xml new file mode 100644 index 000000000..c02feaa24 --- /dev/null +++ b/integration-tests/devui/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + io.quarkiverse.langchain4j + quarkus-langchain4j-integration-tests-parent + 999-SNAPSHOT + + quarkus-langchain4j-integration-tests-devui + Quarkus Langchain4j - Integration Tests - Dev UI + + true + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-chroma + ${project.version} + + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2-q + ${langchain4j.version} + + + io.quarkus + quarkus-vertx-http-dev-ui-tests + + + io.quarkus + quarkus-junit5-internal + + + + + io.quarkiverse.langchain4j + quarkus-langchain4j-core-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + diff --git a/integration-tests/devui/src/test/java/devui/Langchain4jDevUIJsonRpcTest.java b/integration-tests/devui/src/test/java/devui/Langchain4jDevUIJsonRpcTest.java new file mode 100644 index 000000000..2bec40575 --- /dev/null +++ b/integration-tests/devui/src/test/java/devui/Langchain4jDevUIJsonRpcTest.java @@ -0,0 +1,45 @@ +package devui; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.databind.JsonNode; + +import io.quarkus.devui.tests.DevUIJsonRPCTest; +import io.quarkus.test.QuarkusDevModeTest; + +/** + * Tests for the EmbeddingStoreJsonRPCService class that is used as the backend + * called by the Dev UI. + */ +public class Langchain4jDevUIJsonRpcTest extends DevUIJsonRPCTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withEmptyApplication(); + + public Langchain4jDevUIJsonRpcTest() { + super("io.quarkiverse.langchain4j.quarkus-langchain4j-core"); + } + + @Test + public void testAddAndSearchEmbedding() throws Exception { + String id = executeJsonRPCMethod(String.class, "add", Map.of( + "text", "Hello world", + "metadata", "k1=v1,k2=v2")); + assertNotNull(id); + JsonNode relevantEmbeddings = executeJsonRPCMethod("findRelevant", Map.of( + "text", "Hello world", + "limit", "10")); + assertEquals(1, relevantEmbeddings.size()); + assertEquals(id, relevantEmbeddings.get(0).get("embeddingId").asText()); + assertEquals("Hello world", relevantEmbeddings.get(0).get("embedded").asText()); + assertEquals(2, relevantEmbeddings.get(0).get("metadata").size()); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index e0636307d..19c25c258 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -15,6 +15,7 @@ hugging-face ollama azure-openai + devui in-process-embedding-all-minilm-l6-v2-q in-process-embedding-all-minilm-l6-v2 in-process-embedding-bge-small-en-q diff --git a/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java b/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java index 31852e95a..49d8e1c87 100644 --- a/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java +++ b/pinecone/deployment/src/main/java/io/quarkiverse/langchain4j/pinecone/PineconeProcessor.java @@ -8,6 +8,7 @@ import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; +import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem; import io.quarkiverse.langchain4j.pinecone.runtime.PineconeConfig; import io.quarkiverse.langchain4j.pinecone.runtime.PineconeRecorder; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; @@ -32,7 +33,8 @@ FeatureBuildItem feature() { public void createBean( BuildProducer beanProducer, PineconeRecorder recorder, - PineconeConfig config) { + PineconeConfig config, + BuildProducer embeddingStoreProducer) { beanProducer.produce(SyntheticBeanBuildItem .configure(PINECONE_EMBEDDING_STORE) .types(ClassType.create(EmbeddingStore.class), @@ -43,6 +45,7 @@ public void createBean( .scope(ApplicationScoped.class) .supplier(recorder.pineconeStoreSupplier(config)) .done()); + embeddingStoreProducer.produce(new EmbeddingStoreBuildItem()); } } diff --git a/redis/deployment/src/main/java/io/quarkiverse/langchain4j/redis/RedisEmbeddingStoreProcessor.java b/redis/deployment/src/main/java/io/quarkiverse/langchain4j/redis/RedisEmbeddingStoreProcessor.java index 71ae3bc24..f05659b9a 100644 --- a/redis/deployment/src/main/java/io/quarkiverse/langchain4j/redis/RedisEmbeddingStoreProcessor.java +++ b/redis/deployment/src/main/java/io/quarkiverse/langchain4j/redis/RedisEmbeddingStoreProcessor.java @@ -8,6 +8,7 @@ import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.store.embedding.EmbeddingStore; +import io.quarkiverse.langchain4j.deployment.EmbeddingStoreBuildItem; import io.quarkiverse.langchain4j.redis.runtime.RedisEmbeddingStoreConfig; import io.quarkiverse.langchain4j.redis.runtime.RedisEmbeddingStoreRecorder; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; @@ -41,7 +42,8 @@ public RequestedRedisClientBuildItem requestRedisClient(RedisEmbeddingStoreConfi public void createBean( BuildProducer beanProducer, RedisEmbeddingStoreRecorder recorder, - RedisEmbeddingStoreConfig config) { + RedisEmbeddingStoreConfig config, + BuildProducer embeddingStoreProducer) { beanProducer.produce(SyntheticBeanBuildItem .configure(REDIS_EMBEDDING_STORE) .types(ClassType.create(EmbeddingStore.class), @@ -52,7 +54,7 @@ public void createBean( .addInjectionPoint(ClassType.create(DotName.createSimple(ReactiveRedisDataSource.class))) .createWith(recorder.embeddingStoreFunction(config)) .done()); - + embeddingStoreProducer.produce(new EmbeddingStoreBuildItem()); } }