Skip to content

Commit

Permalink
Merge pull request #84 from jmartisk/devui
Browse files Browse the repository at this point in the history
Dev UI for Langchain4j
  • Loading branch information
geoand authored Dec 4, 2023
2 parents 48bacf9 + 249b8c4 commit 18ab50d
Show file tree
Hide file tree
Showing 21 changed files with 705 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,7 +35,8 @@ FeatureBuildItem feature() {
public void createBean(
BuildProducer<SyntheticBeanBuildItem> beanProducer,
ChromaRecorder recorder,
ChromaConfig config) {
ChromaConfig config,
BuildProducer<EmbeddingStoreBuildItem> embeddingStoreProducer) {
beanProducer.produce(SyntheticBeanBuildItem
.configure(CHROMA_EMBEDDING_STORE)
.types(ClassType.create(EmbeddingStore.class),
Expand All @@ -45,5 +47,6 @@ public void createBean(
.scope(ApplicationScoped.class)
.supplier(recorder.chromaStoreSupplier(config))
.done());
embeddingStoreProducer.produce(new EmbeddingStoreBuildItem());
}
}
10 changes: 10 additions & 0 deletions core/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.poi</groupId>
<artifactId>quarkus-poi-deployment</artifactId>
<version>${quarkus-poi.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-dev-ui-tests</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
Expand All @@ -47,6 +56,7 @@
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ InProcessEmbeddingBuildItem e5_small_v2() {
@Record(ExecutionTime.RUNTIME_INIT)
void exposeInProcessEmbeddingBeans(InProcessEmbeddingRecorder recorder,
List<InProcessEmbeddingBuildItem> embeddings,
BuildProducer<SyntheticBeanBuildItem> beanProducer) {
BuildProducer<SyntheticBeanBuildItem> beanProducer,
BuildProducer<EmbeddingModelBuildItem> embeddingModelProducer) {

for (InProcessEmbeddingBuildItem embedding : embeddings) {
beanProducer.produce(SyntheticBeanBuildItem
Expand All @@ -131,6 +132,7 @@ void exposeInProcessEmbeddingBeans(InProcessEmbeddingRecorder recorder,
.scope(ApplicationScoped.class)
.supplier(recorder.instantiate(embedding.className()))
.done());
embeddingModelProducer.produce(new EmbeddingModelBuildItem());
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public void handleTools(CombinedIndexBuildItem indexBuildItem,
BuildProducer<BytecodeTransformerBuildItem> transformerProducer,
BuildProducer<GeneratedClassBuildItem> generatedClassProducer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
BuildProducer<ValidationPhaseBuildItem.ValidationErrorBuildItem> validation) {
BuildProducer<ValidationPhaseBuildItem.ValidationErrorBuildItem> validation,
BuildProducer<ToolsMetadataBuildItem> toolsMetadataProducer) {
recorderContext.registerSubstitution(ToolSpecification.class, ToolSpecificationObjectSubstitution.Serialized.class,
ToolSpecificationObjectSubstitution.class);
recorderContext.registerSubstitution(ToolParameters.class, ToolParametersObjectSubstitution.Serialized.class,
Expand Down Expand Up @@ -229,6 +230,7 @@ public void handleTools(CombinedIndexBuildItem indexBuildItem,
.build());
}

toolsMetadataProducer.produce(new ToolsMetadataBuildItem(metadata));
recorder.setMetadata(metadata);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, List<ToolMethodCreateInfo>> metadata;

public ToolsMetadataBuildItem(Map<String, List<ToolMethodCreateInfo>> metadata) {
this.metadata = metadata;
}

public Map<String, List<ToolMethodCreateInfo>> getMetadata() {
return metadata;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkiverse.langchain4j.deployment.devui;

import java.util.List;

public class AiServiceInfo {

private String clazz;
private List<String> tools;

public AiServiceInfo(String clazz, List<String> tools) {
this.clazz = clazz;
this.tools = tools;
}

public List<String> getTools() {
return tools;
}

public String getClazz() {
return clazz;
}

}
Original file line number Diff line number Diff line change
@@ -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<DeclarativeAiServiceBuildItem> aiServices,
ToolsMetadataBuildItem toolsMetadataBuildItem,
List<EmbeddingModelBuildItem> embeddingModelBuildItem,
List<EmbeddingStoreBuildItem> 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<DeclarativeAiServiceBuildItem> aiServices) {
List<AiServiceInfo> infos = new ArrayList<>();
for (DeclarativeAiServiceBuildItem aiService : aiServices) {
List<String> 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<ToolMethodInfo> infos = new ArrayList<>();
Map<String, List<ToolMethodCreateInfo>> metadata = metadataBuildItem.getMetadata();
for (Map.Entry<String, List<ToolMethodCreateInfo>> 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<JsonRPCProvidersBuildItem> producers,
List<EmbeddingModelBuildItem> embeddingModelBuildItem,
List<EmbeddingStoreBuildItem> embeddingStoreBuildItem) {
if (embeddingModelBuildItem.size() == 1 && embeddingStoreBuildItem.size() == 1) {
producers.produce(new JsonRPCProvidersBuildItem(EmbeddingStoreJsonRPCService.class));
}

}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
85 changes: 85 additions & 0 deletions core/deployment/src/main/resources/dev-ui/qwc-aiservices.js
Original file line number Diff line number Diff line change
@@ -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`<span>Loading AI services...</span>`;
}
}

_renderAiServiceTable() {
return html`
${this._message}
<vaadin-grid .items="${this._aiservices}" class="datatable" theme="no-border">
<vaadin-grid-column auto-width
header="Name"
${columnBodyRenderer(this._nameRenderer, [])}>
</vaadin-grid-column>
<vaadin-grid-column auto-width
header="Tools"
${columnBodyRenderer(this._toolsRenderer, [])}>
</vaadin-grid-column>
</vaadin-grid>`;
}

_nameRenderer(aiservice) {
return html`${aiservice.clazz}`;
}

_toolsRenderer(aiservice) {
if (aiservice.tools && aiservice.tools.length > 0) {
return html`<vaadin-vertical-layout>
${aiservice.tools.map(tool =>
html`<div><code>${tool}</code></div>`
)}</vaadin-vertical-layout>`;
}
}

}
customElements.define('qwc-aiservices', QwcAiservices);
Loading

0 comments on commit 18ab50d

Please sign in to comment.