diff --git a/docs/modules/ROOT/pages/dev-ui.adoc b/docs/modules/ROOT/pages/dev-ui.adoc index a9b739093..db3bcd646 100644 --- a/docs/modules/ROOT/pages/dev-ui.adoc +++ b/docs/modules/ROOT/pages/dev-ui.adoc @@ -16,4 +16,9 @@ page is only available if the application contains a chat model. * *Images* page: allows you to test the outputs of image models and tune its parameters. This page is provided specifically by the `openai-vanilla` extension and is currently specific to OpenAI's image models. It appears if the application uses the `openai-vanilla` extension -and doesn't have image models explicitly disabled. \ No newline at end of file +and doesn't have image models explicitly disabled. + +* *Moderation* page: allows you to test the outputs of moderation models - you submit a prompt +and receive a list of scores for each appropriateness category (violence, sexual, hate,...). +This page is currently only available with the `openai-vanilla` extension, and it +appears if the application doesn't explicitly disable moderation models. \ No newline at end of file diff --git a/openai/openai-vanilla/deployment/src/main/java/io/quarkiverse/langchain4j/openai/deployment/devui/OpenAiDevUIImagePageProcessor.java b/openai/openai-vanilla/deployment/src/main/java/io/quarkiverse/langchain4j/openai/deployment/devui/OpenAiDevUIImagePageProcessor.java deleted file mode 100644 index 60478dd12..000000000 --- a/openai/openai-vanilla/deployment/src/main/java/io/quarkiverse/langchain4j/openai/deployment/devui/OpenAiDevUIImagePageProcessor.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.quarkiverse.langchain4j.openai.deployment.devui; - -import io.quarkiverse.langchain4j.openai.deployment.Langchain4jOpenAiBuildConfig; -import io.quarkiverse.langchain4j.openai.runtime.devui.OpenAiImagesJsonRPCService; -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 OpenAiDevUIImagePageProcessor { - - @BuildStep(onlyIf = IsDevelopment.class) - CardPageBuildItem cardPage( - BuildProducer producers, - Langchain4jOpenAiBuildConfig config) { - if (config.imageModel().enabled().orElse(true)) { - CardPageBuildItem card = new CardPageBuildItem(); - card.addPage(Page.webComponentPageBuilder().title("Images") - .componentLink("qwc-images.js") - .icon("font-awesome-solid:palette")); - producers.produce(new JsonRPCProvidersBuildItem(OpenAiImagesJsonRPCService.class)); - return card; - } else { - return null; - } - } - -} diff --git a/openai/openai-vanilla/deployment/src/main/java/io/quarkiverse/langchain4j/openai/deployment/devui/OpenAiDevUIProcessor.java b/openai/openai-vanilla/deployment/src/main/java/io/quarkiverse/langchain4j/openai/deployment/devui/OpenAiDevUIProcessor.java new file mode 100644 index 000000000..fcad2b98b --- /dev/null +++ b/openai/openai-vanilla/deployment/src/main/java/io/quarkiverse/langchain4j/openai/deployment/devui/OpenAiDevUIProcessor.java @@ -0,0 +1,48 @@ +package io.quarkiverse.langchain4j.openai.deployment.devui; + +import io.quarkiverse.langchain4j.openai.deployment.Langchain4jOpenAiBuildConfig; +import io.quarkiverse.langchain4j.openai.runtime.devui.OpenAiImagesJsonRPCService; +import io.quarkiverse.langchain4j.openai.runtime.devui.OpenAiModerationModelsJsonRPCService; +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 OpenAiDevUIProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + CardPageBuildItem cardPage( + BuildProducer producers, + Langchain4jOpenAiBuildConfig config) { + CardPageBuildItem card = new CardPageBuildItem(); + addImageModelPage(producers, config, card); + addModerationModelPage(producers, config, card); + return card; + } + + private void addImageModelPage( + BuildProducer producers, + Langchain4jOpenAiBuildConfig config, + CardPageBuildItem card) { + if (config.imageModel().enabled().orElse(true)) { + card.addPage(Page.webComponentPageBuilder().title("Images") + .componentLink("qwc-images.js") + .icon("font-awesome-solid:palette")); + producers.produce(new JsonRPCProvidersBuildItem(OpenAiImagesJsonRPCService.class)); + } + } + + private void addModerationModelPage(BuildProducer producers, + Langchain4jOpenAiBuildConfig config, + CardPageBuildItem card) { + if (config.moderationModel().enabled().orElse(true)) { + card.addPage(Page.webComponentPageBuilder().title("Moderation model") + .componentLink("qwc-moderation.js") + .icon("font-awesome-solid:triangle-exclamation")); + producers.produce(new JsonRPCProvidersBuildItem(OpenAiModerationModelsJsonRPCService.class)); + } + } + +} diff --git a/openai/openai-vanilla/deployment/src/main/resources/dev-ui/qwc-moderation.js b/openai/openai-vanilla/deployment/src/main/resources/dev-ui/qwc-moderation.js new file mode 100644 index 000000000..e76a1a83b --- /dev/null +++ b/openai/openai-vanilla/deployment/src/main/resources/dev-ui/qwc-moderation.js @@ -0,0 +1,86 @@ +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'; + +export class QwcModerationModels extends LitElement { + + jsonRpc = new JsonRpc(this); + + supportedModels = [ + { label: "text-moderation-latest", value: "text-moderation-latest"}, + { label: "text-moderation-stable", value: "text-moderation-stable"}] + + static properties = { + "_moderationResponse": {state: true} + } + + constructor() { + super(); + } + + render() { + return html` +

Moderation model

+ + + +
+ + this._doGenerate( + this.shadowRoot.getElementById('model-name').value, + this.shadowRoot.getElementById('prompt').value + )}>Moderate the prompt + +
+

Moderation response

+ ${this._moderationResponse} + `; + } + + _doGenerate(modelName, prompt) { + this._moderationResponse = html`Retrieving...
`; + this.jsonRpc.moderate({modelName: modelName, prompt: prompt}).then((jsonRpcResponse) => { + this._moderationResponse = this._printResponse(jsonRpcResponse.result); + }).catch((error) => { + this._moderationResponse = html` + + ${JSON.stringify(error.error)} + ` + }); + } + + _printResponse(response) { + if (response) { + return html` + Flagged: ${response.flagged}
+ + + + + + + + `; + } else { + return html``; + } + + } + +} + +customElements.define('qwc-moderation', QwcModerationModels); \ No newline at end of file diff --git a/openai/openai-vanilla/runtime/src/main/java/io/quarkiverse/langchain4j/openai/runtime/devui/OpenAiModerationModelsJsonRPCService.java b/openai/openai-vanilla/runtime/src/main/java/io/quarkiverse/langchain4j/openai/runtime/devui/OpenAiModerationModelsJsonRPCService.java new file mode 100644 index 000000000..a81419ed4 --- /dev/null +++ b/openai/openai-vanilla/runtime/src/main/java/io/quarkiverse/langchain4j/openai/runtime/devui/OpenAiModerationModelsJsonRPCService.java @@ -0,0 +1,90 @@ +package io.quarkiverse.langchain4j.openai.runtime.devui; + +import java.time.Duration; +import java.util.Optional; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import dev.ai4j.openai4j.OpenAiClient; +import dev.ai4j.openai4j.moderation.Categories; +import dev.ai4j.openai4j.moderation.CategoryScores; +import dev.ai4j.openai4j.moderation.ModerationRequest; +import dev.ai4j.openai4j.moderation.ModerationResponse; +import dev.ai4j.openai4j.moderation.ModerationResult; +import dev.langchain4j.internal.RetryUtils; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class OpenAiModerationModelsJsonRPCService { + + @Inject + @ConfigProperty(name = "quarkus.langchain4j.openai.base-url") + String baseUrl; + + @Inject + @ConfigProperty(name = "quarkus.langchain4j.openai.api-key") + String apiKey; + + @Inject + @ConfigProperty(name = "quarkus.langchain4j.openai.timeout") + Duration timeout; + + @Inject + @ConfigProperty(name = "quarkus.langchain4j.openai.image-model.user") + Optional user; + + @Inject + @ConfigProperty(name = "quarkus.langchain4j.openai.max-retries") + Integer maxRetries; + + OpenAiClient client; + + @PostConstruct + public void init() { + client = OpenAiClient.builder().openAiApiKey(apiKey).baseUrl(baseUrl) + .callTimeout(timeout).connectTimeout(timeout) + .readTimeout(timeout).writeTimeout(timeout).build(); + } + + @PreDestroy + public void cleanup() { + if (client != null) { + client.shutdown(); + } + } + + public JsonObject moderate(String modelName, String prompt) { + ModerationRequest request = ModerationRequest.builder().model(modelName).input(prompt).build(); + ModerationResponse response = RetryUtils.withRetry(() -> client.moderation(request).execute(), this.maxRetries); + ModerationResult moderationResult = response.results().get(0); + CategoryScores categoryScores = moderationResult.categoryScores(); + Categories categoryFlags = moderationResult.categories(); + JsonObject result = new JsonObject(); + result.put("flagged", moderationResult.isFlagged()); + JsonArray categories = new JsonArray(); + addCategoryScore(categories, "sexual", categoryScores.sexual(), categoryFlags.sexual()); + addCategoryScore(categories, "hate", categoryScores.hate(), categoryFlags.hate()); + addCategoryScore(categories, "hate-threatening", categoryScores.hateThreatening(), categoryFlags.hateThreatening()); + addCategoryScore(categories, "self-harm", categoryScores.selfHarm(), categoryFlags.selfHarm()); + addCategoryScore(categories, "violence", categoryScores.violence(), categoryFlags.violence()); + addCategoryScore(categories, "violence-graphic", categoryScores.violenceGraphic(), categoryFlags.violenceGraphic()); + addCategoryScore(categories, "sexual-minors", categoryScores.sexualMinors(), categoryFlags.sexualMinors()); + result.put("categories", categories); + return result; + } + + private void addCategoryScore(JsonArray categories, String name, Double score, Boolean flagged) { + if (score != null) { + JsonObject categoryScore = new JsonObject(); + categoryScore.put("name", name); + categoryScore.put("flagged", flagged); + categoryScore.put("score", score); + categories.add(categoryScore); + } + } + +}