From 7684c3afea62763764e9e59ac517325a8c1fab2b Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Fri, 6 Sep 2024 16:06:33 +1000 Subject: [PATCH] Added local option Signed-off-by: Phillip Kruger --- deployment/pom.xml | 3 +- .../deployment/ChappieAvailableBuildItem.java | 9 + .../chappie/deployment/ChappieConfig.java | 13 +- .../deployment/ChappieDevUIProcessor.java | 71 +++++++ .../chappie/deployment/ChappieEnabled.java | 22 -- .../deployment/ConfiguredDevUIProcessor.java | 33 --- .../chappie/deployment/ConsoleProcessor.java | 125 +++++++++++ .../UnconfiguredDevUIProcessor.java | 28 --- .../ChappieDevServiceProcessor.java | 40 ++-- .../devservice/DevServicesConfig.java | 8 +- .../chappie/deployment/devservice/LLM.java | 11 - .../devservice/ollama/JdkOllamaClient.java | 195 ++++++++++++++++++ .../devservice/ollama/OllamaBuildItem.java | 16 ++ .../devservice/ollama/OllamaClient.java | 72 +++++++ .../devservice/ollama/OllamaConfig.java | 39 ++++ .../ollama/OllamaDevServicesProcessor.java | 165 +++++++++++++++ .../exception/ConsoleProcessor.java | 109 ---------- .../exception/ExceptionDevUIProcessor.java | 157 +++++++------- .../exception/ExceptionProcessor.java | 3 +- .../ExplanationDevUIProcessor.java | 87 ++++---- .../javadoc/JavaDocDevUIProcessor.java | 139 +++++++------ .../testing/TestingDevUIProcessor.java | 154 +++++++------- .../resources/dev-ui/qwc-chappie-exception.js | 16 ++ .../dev-ui/qwc-chappie-explanation.js | 16 ++ .../resources/dev-ui/qwc-chappie-javadoc.js | 16 ++ .../resources/dev-ui/qwc-chappie-testing.js | 16 ++ .../dev-ui/qwc-chappie-unconfigured.js | 44 ++-- integration-tests/pom.xml | 4 - .../chappie/it/ChappieResource.java | 32 --- .../chappie/it/ChappieResourceTest.java | 13 -- pom.xml | 2 +- .../src/main/resources/application.properties | 1 - 32 files changed, 1112 insertions(+), 547 deletions(-) create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieAvailableBuildItem.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieDevUIProcessor.java delete mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieEnabled.java delete mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/ConfiguredDevUIProcessor.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/ConsoleProcessor.java delete mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/UnconfiguredDevUIProcessor.java delete mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/LLM.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/JdkOllamaClient.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaBuildItem.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaClient.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaConfig.java create mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaDevServicesProcessor.java delete mode 100644 deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ConsoleProcessor.java delete mode 100644 integration-tests/src/main/java/io/quarkiverse/chappie/it/ChappieResource.java diff --git a/deployment/pom.xml b/deployment/pom.xml index 2333b85..ee3ea7c 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -11,8 +11,7 @@ Quarkus Chappie - Deployment - 0.33.0 - 0.0.11 + 0.0.12 diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieAvailableBuildItem.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieAvailableBuildItem.java new file mode 100644 index 0000000..dd63f17 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieAvailableBuildItem.java @@ -0,0 +1,9 @@ +package io.quarkiverse.chappie.deployment; + +import io.quarkus.builder.item.SimpleBuildItem; + +final public class ChappieAvailableBuildItem extends SimpleBuildItem { + + public ChappieAvailableBuildItem() { + } +} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieConfig.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieConfig.java index ca30b47..bab6b0a 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieConfig.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieConfig.java @@ -1,29 +1,28 @@ package io.quarkiverse.chappie.deployment; import io.quarkiverse.chappie.deployment.devservice.DevServicesConfig; -import io.quarkiverse.chappie.deployment.devservice.LLM; import io.quarkiverse.chappie.deployment.devservice.OpenAIConfig; +import io.quarkiverse.chappie.deployment.devservice.ollama.OllamaConfig; import io.quarkus.runtime.annotations.ConfigDocSection; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; -import io.smallrye.config.WithDefault; @ConfigMapping(prefix = "quarkus.assistant") @ConfigRoot(phase = ConfigPhase.BUILD_TIME) public interface ChappieConfig { /** - * The LLM to use, for now we only support openai + * OpenAI config */ - @WithDefault("openai") - LLM llm(); + @ConfigDocSection + OpenAIConfig openai(); /** - * OpenAI config + * Ollama config */ @ConfigDocSection - OpenAIConfig openai(); + OllamaConfig ollama(); /** * Dev Services diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieDevUIProcessor.java new file mode 100644 index 0000000..50b413c --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieDevUIProcessor.java @@ -0,0 +1,71 @@ +package io.quarkiverse.chappie.deployment; + +import java.util.List; +import java.util.Optional; + +import io.quarkiverse.chappie.deployment.devservice.ollama.OllamaBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; + +@BuildSteps(onlyIf = IsDevelopment.class) +class ChappieDevUIProcessor { + + @BuildStep + void pages(List chappiePageBuildItems, + Optional ollamaBuildItem, + BuildProducer cardPageProducer, + ChappieConfig config) { + + if (config.openai().apiKey().isPresent()) { + configuredOpenAiPage(chappiePageBuildItems, cardPageProducer, config); + } else if (ollamaBuildItem.isPresent()) { + configuredOllamaPage(chappiePageBuildItems, cardPageProducer, config); + } else { + unconfiguredPage(cardPageProducer); + } + } + + private void configuredOpenAiPage(List chappiePageBuildItems, + BuildProducer cardPageProducer, + ChappieConfig config) { + configuredPage(chappiePageBuildItems, cardPageProducer, "OpenAI", config.openai().modelName()); + } + + private void configuredOllamaPage(List chappiePageBuildItems, + BuildProducer cardPageProducer, + ChappieConfig config) { + configuredPage(chappiePageBuildItems, cardPageProducer, "Ollama", config.ollama().modelName()); + } + + private void configuredPage(List chappiePageBuildItems, + BuildProducer cardPageProducer, + String llm, String modelName) { + CardPageBuildItem chappieCard = new CardPageBuildItem(); + chappieCard.setCustomCard("qwc-chappie-custom-card.js"); + + chappieCard.addBuildTimeData("llm", llm); + chappieCard.addBuildTimeData("modelName", modelName); + + for (ChappiePageBuildItem cpbi : chappiePageBuildItems) { + chappieCard.addPage(cpbi.getPageBuilder()); + } + + cardPageProducer.produce(chappieCard); + } + + private void unconfiguredPage(BuildProducer cardPageProducer) { + CardPageBuildItem chappieCard = new CardPageBuildItem(); + + chappieCard.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:circle-question") + .title("Configure assistant") + .componentLink("qwc-chappie-unconfigured.js")); + + cardPageProducer.produce(chappieCard); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieEnabled.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieEnabled.java deleted file mode 100644 index f3e9deb..0000000 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ChappieEnabled.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkiverse.chappie.deployment; - -import java.util.function.BooleanSupplier; - -import io.quarkiverse.chappie.deployment.devservice.LLM; - -public class ChappieEnabled implements BooleanSupplier { - - private final ChappieConfig chappieConfig; - - ChappieEnabled(ChappieConfig chappieConfig) { - this.chappieConfig = chappieConfig; - } - - @Override - public boolean getAsBoolean() { - if (chappieConfig.llm().equals(LLM.openai) && chappieConfig.openai().apiKey().isPresent()) { - return true; - } - return false; - } -} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ConfiguredDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ConfiguredDevUIProcessor.java deleted file mode 100644 index e91d7c2..0000000 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ConfiguredDevUIProcessor.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.quarkiverse.chappie.deployment; - -import java.util.List; - -import io.quarkiverse.chappie.deployment.devservice.LLM; -import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.BuildSteps; -import io.quarkus.devui.spi.page.CardPageBuildItem; - -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) -class ConfiguredDevUIProcessor { - - @BuildStep - void pages(List chappiePageBuildItems, - BuildProducer cardPageProducer, - ChappieConfig config) { - CardPageBuildItem chappieCard = new CardPageBuildItem(); - chappieCard.setCustomCard("qwc-chappie-custom-card.js"); - chappieCard.addBuildTimeData("llm", config.llm()); - - if (config.llm().equals(LLM.openai)) { - chappieCard.addBuildTimeData("modelName", config.openai().modelName()); - } - - for (ChappiePageBuildItem cpbi : chappiePageBuildItems) { - chappieCard.addPage(cpbi.getPageBuilder()); - } - - cardPageProducer.produce(chappieCard); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/ConsoleProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ConsoleProcessor.java new file mode 100644 index 0000000..e98da4f --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/ConsoleProcessor.java @@ -0,0 +1,125 @@ +package io.quarkiverse.chappie.deployment; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import io.quarkiverse.chappie.deployment.devservice.ChappieClient; +import io.quarkiverse.chappie.deployment.devservice.ChappieClientBuildItem; +import io.quarkiverse.chappie.deployment.devservice.ollama.OllamaBuildItem; +import io.quarkiverse.chappie.deployment.exception.LastException; +import io.quarkiverse.chappie.deployment.exception.LastExceptionBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.console.ConsoleCommand; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.ConsoleStateManager; +import io.quarkus.deployment.dev.testing.MessageFormat; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; +import io.vertx.core.Vertx; + +/** + * Main console Chappie Processor. This create a few Build Items also used by the DevUI processor + * + * @author Phillip Kruger (phillip.kruger@gmail.com) + */ +@BuildSteps(onlyIf = IsDevelopment.class) +class ConsoleProcessor { + static volatile ConsoleStateManager.ConsoleContext chappieConsoleContext; + + @BuildStep + void chappieAvailable(BuildProducer chappieAvailableProducer, + ChappieConfig config, + Optional ollamaBuildItem) { + + if (config.openai().apiKey().isPresent() || ollamaBuildItem.isPresent()) { + chappieAvailableProducer.produce(new ChappieAvailableBuildItem()); + } + } + + @Consume(ConsoleInstalledBuildItem.class) + @BuildStep + void setupConsole(BuildProducer featureProducer, + LastExceptionBuildItem lastExceptionBuildItem, + ChappieClientBuildItem chappieClientBuildItem, + LoggingDecorateBuildItem loggingDecorateBuildItem, + Optional chappieAvailable) { + + if (chappieAvailable.isPresent()) { + + Path srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); + + if (chappieConsoleContext == null) { + chappieConsoleContext = ConsoleStateManager.INSTANCE.createContext("Assistant"); + } + + Vertx vertx = Vertx.vertx(); + chappieConsoleContext.reset( + new ConsoleCommand('a', "Help with the latest exception", + new ConsoleCommand.HelpState(new Supplier() { + @Override + public String get() { + return MessageFormat.RED; + } + }, new Supplier() { + @Override + public String get() { + LastException lastException = lastExceptionBuildItem.getLastException().get(); + if (lastException == null) { + return "none"; + } + return lastException.throwable().getMessage(); + } + }), new Runnable() { + @Override + public void run() { + LastException lastException = lastExceptionBuildItem.getLastException().get(); + if (lastException == null) { + return; + } + + String sourceString = SourceCodeFinder.getSourceCode(srcMainJava, + lastException.stackTraceElement()); + + String stacktraceString = lastException.getStackTraceString(); + + System.out.println( + "===============\nAssisting with the exception, please wait"); + + long timer = vertx.setPeriodic(800, id -> System.out.print(".")); + + ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); + Object[] params = ParameterCreator.getParameters("", stacktraceString, sourceString); + + CompletableFuture futureResult = chappieClient.executeRPC("exception#suggestfix", + params); + + futureResult.thenAccept(r -> { + vertx.cancelTimer(timer); + Map result = (Map) r; + + System.out.println("\n\n" + result.get("response")); + System.out.println("\n\n" + result.get("explanation")); + System.out.println("\n------ Diff ------ "); + System.out.println("\n\n" + result.get("diff")); + System.out.println("\n------ Suggested source ------ "); + System.out.println("\n\n" + result.get("suggestedSource")); + }).exceptionally(throwable -> { + // Handle any errors + System.out.println("\n\nCould not get a response from ai due the this exception:"); + throwable.printStackTrace(); + return null; + }); + } + })); + } + + featureProducer.produce(new FeatureBuildItem(Feature.FEATURE)); + } +} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/UnconfiguredDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/UnconfiguredDevUIProcessor.java deleted file mode 100644 index 0462822..0000000 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/UnconfiguredDevUIProcessor.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.quarkiverse.chappie.deployment; - -import io.quarkiverse.chappie.deployment.devservice.LLM; -import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.BuildSteps; -import io.quarkus.devui.spi.page.CardPageBuildItem; -import io.quarkus.devui.spi.page.Page; - -@BuildSteps(onlyIf = IsDevelopment.class) -class UnconfiguredDevUIProcessor { - - @BuildStep(onlyIfNot = ChappieEnabled.class) - void notenabled(BuildProducer cardPageProducer, ChappieConfig config) { - CardPageBuildItem chappieCard = new CardPageBuildItem(); - - chappieCard.addBuildTimeData("llm", config.llm()); - if (config.llm().equals(LLM.openai)) { - chappieCard.addBuildTimeData("modelName", config.openai().modelName()); - chappieCard.addPage(Page.webComponentPageBuilder() - .icon("font-awesome-solid:circle-question") - .title("Configure assistant") - .componentLink("qwc-chappie-unconfigured.js")); - } - cardPageProducer.produce(chappieCard); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ChappieDevServiceProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ChappieDevServiceProcessor.java index 9cd3573..511db68 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ChappieDevServiceProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ChappieDevServiceProcessor.java @@ -14,14 +14,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Scanner; import org.jboss.logging.Logger; import org.yaml.snakeyaml.Yaml; import io.quarkiverse.chappie.deployment.ChappieConfig; -import io.quarkiverse.chappie.deployment.ChappieEnabled; import io.quarkiverse.chappie.deployment.Feature; +import io.quarkiverse.chappie.deployment.devservice.ollama.OllamaBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -30,7 +31,7 @@ import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; import io.quarkus.runtime.util.ClassPathUtils; -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class, GlobalDevServicesConfig.Enabled.class }) +@BuildSteps(onlyIf = { IsDevelopment.class, GlobalDevServicesConfig.Enabled.class }) public class ChappieDevServiceProcessor { private static Process process; private static ChappieClient chappieClient; @@ -40,9 +41,10 @@ public class ChappieDevServiceProcessor { public void createContainer(BuildProducer devServicesResultProducer, BuildProducer chappieClientProducer, ExtensionVersionBuildItem extensionVersionBuildItem, + Optional ollamaBuildItem, ChappieConfig config) { - if (process == null) { + if (process == null && (config.openai().apiKey().isPresent() || ollamaBuildItem.isPresent())) { Map properties = new HashMap<>(); int port = findAvailablePort(4315); @@ -51,13 +53,16 @@ public void createContainer(BuildProducer devService properties.put("quarkus.http.host", "localhost"); properties.put("quarkus.http.port", String.valueOf(port)); - if (config.llm().equals(LLM.openai)) { - properties.put("quarkus.langchain4j.chat-model.provider", "openai"); - properties.put("quarkus.langchain4j.openai.api-key", config.openai().apiKey().get()); - properties.put("quarkus.langchain4j.openai.chat-model.model-name", config.openai().modelName()); - properties.put("quarkus.langchain4j.openai.enable-integration", "true"); + + if (config.openai().apiKey().isPresent()) { + properties.put("chappie.openai.api-key", config.openai().apiKey().get()); + properties.put("chappie.openai.model-name", config.openai().modelName()); + } else if (ollamaBuildItem.isPresent()) { + properties.put("chappie.ollama.base-url", ollamaBuildItem.get().getUrl()); + properties.put("chappie.ollama.model-name", config.ollama().modelName()); + properties.put("chappie.ollama.timeout", config.ollama().timeout()); + } - // TODO: Ollama String extVersion = extensionVersionBuildItem.getVersion(); if (!chappieServerExists(extVersion) || extVersion.endsWith(SNAPSHOT)) { @@ -65,7 +70,7 @@ public void createContainer(BuildProducer devService } try { - runServer(extVersion, properties); + runServer(extVersion, properties, config.devservices().log()); Runtime.getRuntime().addShutdownHook(new Thread(() -> { if (process != null && process.isAlive()) { process.destroy(); @@ -89,7 +94,6 @@ public void createContainer(BuildProducer devService chappieClient = new ChappieClient(jsonRpcBase); chappieClient.connect(); } - // TODO: Reconnect ? chappieClientProducer.produce(new ChappieClientBuildItem(chappieClient)); @@ -192,7 +196,7 @@ private Path getChappieBaseDir(String version) { return Path.of(userHome, ".chappie/" + version); } - private void runServer(String version, Map properties) throws IOException { + private void runServer(String version, Map properties, boolean log) throws IOException { Path chappieServer = getChappieServer(version); List command = new ArrayList<>(); @@ -200,6 +204,10 @@ private void runServer(String version, Map properties) throws IO for (Map.Entry es : properties.entrySet()) { command.add("-D" + es.getKey() + "=" + es.getValue()); } + if (log) { + command.add("-Dchappie.log.request=true"); + command.add("-Dchappie.log.response=true"); + } command.add("-jar"); command.add(chappieServer.toString()); @@ -207,16 +215,18 @@ private void runServer(String version, Map properties) throws IO process = processBuilder.start(); - new Thread(() -> handleStream(process.getInputStream(), "OUTPUT")).start(); - new Thread(() -> handleStream(process.getErrorStream(), "ERROR")).start(); + new Thread(() -> handleStream(process.getInputStream(), "OUTPUT", log)).start(); + new Thread(() -> handleStream(process.getErrorStream(), "ERROR", log)).start(); } - private void handleStream(java.io.InputStream inputStream, String type) { + private void handleStream(java.io.InputStream inputStream, String type, boolean log) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { String line; while ((line = reader.readLine()) != null) { if (type.equals("ERROR")) { LOG.error(line); + } else if (log) { + LOG.info(line); } else { LOG.debug(line); } diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/DevServicesConfig.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/DevServicesConfig.java index 5c3dfe6..6462bd7 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/DevServicesConfig.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/DevServicesConfig.java @@ -9,6 +9,12 @@ @ConfigGroup public interface DevServicesConfig { + /** + * Show the log of the dev service in the application log + */ + @WithDefault("false") + boolean log(); + /** * The default port where the inference server listens for requests */ @@ -17,7 +23,7 @@ public interface DevServicesConfig { /** * The version to use */ - @WithDefault("0.0.6") + @WithDefault("0.0.12") String version(); /** diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/LLM.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/LLM.java deleted file mode 100644 index 552f78c..0000000 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/LLM.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkiverse.chappie.deployment.devservice; - -/** - * Contains the supported LLMs - * - * @author Phillip Kruger (phillip.kruger@gmail.com) - */ -public enum LLM { - openai, - ollama -} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/JdkOllamaClient.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/JdkOllamaClient.java new file mode 100644 index 0000000..bf74cc0 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/JdkOllamaClient.java @@ -0,0 +1,195 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package io.quarkiverse.chappie.deployment.devservice.ollama; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ConnectException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.concurrent.Flow; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; + +import io.smallrye.mutiny.operators.multi.builders.EmitterBasedMulti; +import io.smallrye.mutiny.subscription.BackPressureStrategy; +import io.smallrye.mutiny.subscription.MultiEmitter; + +/** + * An implementation of {@link OllamaClient} based on the JDK client. + * We can't use our REST Client here because it's not available during augmentation. + */ +public class JdkOllamaClient implements OllamaClient { + + private final ObjectMapper objectMapper; + private final Options options; + + public JdkOllamaClient(Options options) { + this.options = options; + this.objectMapper = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Override + public List localModels() { + String serverUrl = String.format("http://%s:%d/api/tags", options.host(), options.port()); + try { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(new URI(serverUrl)) + .GET() + .build(); + + HttpResponse httpResponse = HttpClient.newHttpClient().send(httpRequest, + HttpResponse.BodyHandlers.ofString()); + if (httpResponse.statusCode() != 200) { + throw new RuntimeException( + "Unexpected response code: " + httpResponse.statusCode() + " response body: " + + httpResponse.body()); + } + + return objectMapper.readValue(httpResponse.body(), ModelsResponse.class).models(); + } catch (URISyntaxException e) { + throw new IllegalStateException("Unable to convert " + serverUrl + " to URI", e); + } catch (ConnectException e) { + throw new OllamaClient.ServerUnavailableException(options.host(), options.port()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public ModelInfo modelInfo(String modelName) { + String serverUrl = String.format("http://%s:%d/api/show", options.host(), options.port()); + try { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(new URI(serverUrl)) + .POST(HttpRequest.BodyPublishers.ofString(String.format("{\"name\":\"%s\"}", modelName))) + .build(); + + HttpResponse httpResponse = HttpClient.newHttpClient().send(httpRequest, + HttpResponse.BodyHandlers.ofString()); + if (httpResponse.statusCode() != 200) { + if (httpResponse.statusCode() == 404) { + throw new OllamaClient.ModelNotFoundException(modelName); + } + throw new RuntimeException( + "Unexpected response code: " + httpResponse.statusCode() + " response body: " + httpResponse.body()); + } + + return objectMapper.readValue(httpResponse.body(), ModelInfo.class); + } catch (URISyntaxException e) { + throw new IllegalStateException("Unable to convert " + serverUrl + " to URI", e); + } catch (ConnectException e) { + throw new OllamaClient.ServerUnavailableException(options.host(), options.port()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public Flow.Publisher pullAsync(String modelName) { + String serverUrl = String.format("http://%s:%d/api/pull", options.host(), options.port()); + try { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(new URI(serverUrl)) + .POST(HttpRequest.BodyPublishers.ofString(String.format("{\"name\":\"%s\", \"stream\": true}", modelName))) + .build(); + + // can't use Multi.createFrom().emitter because it causes ClassLoader issues in the tests + return new EmitterBasedMulti<>(emitter -> { + try { + HttpClient.newHttpClient().send(httpRequest, + HttpResponse.BodyHandlers + .fromLineSubscriber(new PullAsyncLineSubscriber(emitter, objectMapper, modelName))); + } catch (ConnectException e) { + throw new OllamaClient.ServerUnavailableException(options.host(), options.port()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }, BackPressureStrategy.BUFFER); + } catch (URISyntaxException e) { + throw new IllegalStateException("Unable to convert " + serverUrl + " to URI", e); + } + } + + @Override + public void preloadChatModel(String modelName) { + String serverUrl = String.format("http://%s:%d/api/chat", options.host(), options.port()); + try { + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(new URI(serverUrl)) + .POST(HttpRequest.BodyPublishers.ofString(String.format("{\"model\": \"%s\"}", modelName))) + .build(); + + HttpResponse httpResponse = HttpClient.newHttpClient().send(httpRequest, + HttpResponse.BodyHandlers.ofString()); + if (httpResponse.statusCode() != 200) { + throw new RuntimeException( + "Unexpected response code: " + httpResponse.statusCode() + " response body: " + + httpResponse.body()); + } + } catch (URISyntaxException e) { + throw new IllegalStateException("Unable to convert " + serverUrl + " to URI", e); + } catch (ConnectException e) { + throw new OllamaClient.ServerUnavailableException(options.host(), options.port()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private record PullAsyncLineSubscriber(MultiEmitter emitter, ObjectMapper objectMapper, + String modelName) implements Flow.Subscriber { + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(String item) { + if (item.isBlank()) { + return; + } + if (item.contains("file does not exist")) { + emitter.fail(new ModelDoesNotExistException(modelName)); + } + try { + emitter.emit(objectMapper.readValue(item, PullAsyncLine.class)); + } catch (Exception e) { + emitter.fail(e); + } + } + + @Override + public void onError(Throwable throwable) { + emitter.fail(throwable); + } + + @Override + public void onComplete() { + emitter.complete(); + } + } + + private record ModelsResponse(List models) { + + } +} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaBuildItem.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaBuildItem.java new file mode 100644 index 0000000..d5ec5b0 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaBuildItem.java @@ -0,0 +1,16 @@ +package io.quarkiverse.chappie.deployment.devservice.ollama; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class OllamaBuildItem extends SimpleBuildItem { + private final String url; + + public OllamaBuildItem(String url) { + this.url = url; + } + + public String getUrl() { + return this.url; + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaClient.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaClient.java new file mode 100644 index 0000000..2f4b1df --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaClient.java @@ -0,0 +1,72 @@ +package io.quarkiverse.chappie.deployment.devservice.ollama; + +import java.util.List; +import java.util.concurrent.Flow; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public interface OllamaClient { + + static OllamaClient create(Options options) { + return new JdkOllamaClient(options); + } + + /** + * Returns a list of all models the server has + */ + List localModels(); + + /** + * Get information from a model that already exists in the Ollama server + * + * @throws ModelNotFoundException if the server has not previously pulled the model + */ + ModelInfo modelInfo(String modelName); + + /** + * Instructs Ollama to pull the specified model. The result here a Publisher in order to facilitate providing updates + * about the progress of the pull (which can take a long time). + */ + Flow.Publisher pullAsync(String modelName); + + /** + * Instructs Ollama to preload a model in order to get faster response times. + * See this + * for more information + */ + void preloadChatModel(String modelName); + + record ModelInfo(String name, @JsonProperty("modelfile") String modelFile, String parameters, Details details) { + + public record Details(String family, String parameterSize) { + + } + } + + record PullAsyncLine(String status, Long total, Long completed) { + + } + + class ModelNotFoundException extends RuntimeException { + public ModelNotFoundException(String model) { + super("Model not found: " + model); + } + } + + class ModelDoesNotExistException extends RuntimeException { + public ModelDoesNotExistException(String model) { + super("Model does not exist: " + model); + } + } + + class ServerUnavailableException extends RuntimeException { + public ServerUnavailableException(String host, int port) { + super("Ollama server at [" + host + ":" + port + "] is unreachable"); + } + } + + record Options(String host, int port) { + + } +} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaConfig.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaConfig.java new file mode 100644 index 0000000..48d311a --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaConfig.java @@ -0,0 +1,39 @@ +package io.quarkiverse.chappie.deployment.devservice.ollama; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface OllamaConfig { + + /** + * The default host where the inference server runs + */ + @WithDefault("localhost") + String host(); + + /** + * The default port where the inference server listens for requests + */ + @WithDefault("11434") + Integer port(); + + /** + * The Model to use + */ + @WithDefault("codellama") + String modelName(); + + /** + * Instructs Ollama to preload a model in order to get faster response times + */ + @WithDefault("true") + boolean preload(); + + /** + * Timeout for the request + */ + @WithDefault("PT120S") + String timeout(); + +} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaDevServicesProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaDevServicesProcessor.java new file mode 100644 index 0000000..e0ebce0 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/devservice/ollama/OllamaDevServicesProcessor.java @@ -0,0 +1,165 @@ +package io.quarkiverse.chappie.deployment.devservice.ollama; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; + +import io.quarkiverse.chappie.deployment.ChappieConfig; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.DevServicesResultBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; + +@BuildSteps(onlyIf = { IsDevelopment.class, GlobalDevServicesConfig.Enabled.class }) +public class OllamaDevServicesProcessor { + + private final static Logger LOGGER = Logger.getLogger(OllamaDevServicesProcessor.class); + + @BuildStep + private void handleModels(LoggingSetupBuildItem loggingSetupBuildItem, + Optional consoleInstalledBuildItem, + LaunchModeBuildItem launchMode, + ChappieConfig config, + BuildProducer producer, + BuildProducer ollamaProducer) { + + if (!config.openai().apiKey().isPresent()) { // Only start up if OpenAI is not used + + OllamaClient client = OllamaClient.create(new OllamaClient.Options(config.ollama().host(), config.ollama().port())); + try { + Set localModels = client.localModels().stream().map(mi -> ModelName.of(mi.name())) + .collect(Collectors.toSet()); + + Optional modelToPull; + if (localModels.contains(ModelName.of(config.ollama().modelName()))) { + LOGGER.debug("Ollama already has model " + config.ollama().modelName() + " pulled locally"); + modelToPull = Optional.empty(); + } else { + LOGGER.debug("Need to pull the following model into Ollama server: " + config.ollama().modelName()); + modelToPull = Optional.of(config.ollama().modelName()); + } + + AtomicReference clientThreadName = new AtomicReference<>(); + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Ollama model pull:", consoleInstalledBuildItem, + loggingSetupBuildItem, + // ensure that the progress logging done on the async thread is also caught by the compressor + thread -> { + String t = clientThreadName.get(); + if (t == null) { + return false; + } + return thread.getName().equals(t); + }); + if (modelToPull.isPresent()) { + String model = modelToPull.get(); + // we pull one model at a time and provide progress updates to the user via logging + LOGGER.info("Pulling model " + model); + + CompletableFuture cf = new CompletableFuture<>(); + client.pullAsync(model).subscribe(new Flow.Subscriber<>() { + + private static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(OllamaClient.PullAsyncLine line) { + clientThreadName.compareAndSet(null, Thread.currentThread().getName()); + if ((line.total() != null) && (line.completed() != null) && (line.status() != null) + && line.status().contains("pulling")) { + BigDecimal percentage = new BigDecimal(line.completed()).divide(new BigDecimal(line.total()), 4, + RoundingMode.HALF_DOWN).multiply(ONE_HUNDRED); + BigDecimal progress = percentage.setScale(2, RoundingMode.HALF_DOWN); + if (progress.compareTo(ONE_HUNDRED) >= 0) { + // avoid showing 100% for too long + LOGGER.infof("Verifying and cleaning up\n", progress); + } else { + LOGGER.infof("Progress: %s%%\n", progress); + } + } + } + + @Override + public void onError(Throwable throwable) { + cf.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + cf.complete(null); + } + }); + + // TODO: Let Quarkus start up, and just show the progress in dev ui / cli + try { + cf.get(5, TimeUnit.MINUTES); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(e.getCause()); + } + } + + // preload model - it only makes sense to load a single model + if (config.ollama().preload()) { + String modelName = config.ollama().modelName(); + LOGGER.infof("Preloading model %s", modelName); + client.preloadChatModel(modelName); + } + + compressor.close(); + + String ollamaBaseUrl = String.format("http://%s:%d", config.ollama().host(), config.ollama().port()); + + Map modelBaseUrls = Map.of("baseUrl", ollamaBaseUrl); + + DevServicesResultBuildItem buildItem = new DevServicesResultBuildItem("chappie-ollama", + "Ollama for chappie. We expect a running ollama, but will manage models automatically", + null, + modelBaseUrls); + + producer.produce(buildItem); + ollamaProducer.produce(new OllamaBuildItem(ollamaBaseUrl)); + } catch (OllamaClient.ServerUnavailableException e) { + return; + } + } + } + + private record ModelName(String model, String tag) { + + public static ModelName of(String modelName) { + Objects.requireNonNull(modelName, "modelName cannot be null"); + String[] parts = modelName.split(":"); + if (parts.length == 1) { + return new ModelName(modelName, "latest"); + } else if (parts.length == 2) { + return new ModelName(parts[0], parts[1]); + } else { + throw new IllegalArgumentException("Invalid model name: " + modelName); + } + } + + } +} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ConsoleProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ConsoleProcessor.java deleted file mode 100644 index ae98cfb..0000000 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ConsoleProcessor.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.quarkiverse.chappie.deployment.exception; - -import java.nio.file.Path; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; - -import io.quarkiverse.chappie.deployment.ChappieEnabled; -import io.quarkiverse.chappie.deployment.Feature; -import io.quarkiverse.chappie.deployment.ParameterCreator; -import io.quarkiverse.chappie.deployment.SourceCodeFinder; -import io.quarkiverse.chappie.deployment.devservice.ChappieClient; -import io.quarkiverse.chappie.deployment.devservice.ChappieClientBuildItem; -import io.quarkus.deployment.IsDevelopment; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.BuildSteps; -import io.quarkus.deployment.annotations.Consume; -import io.quarkus.deployment.builditem.FeatureBuildItem; -import io.quarkus.deployment.console.ConsoleCommand; -import io.quarkus.deployment.console.ConsoleInstalledBuildItem; -import io.quarkus.deployment.console.ConsoleStateManager; -import io.quarkus.deployment.dev.testing.MessageFormat; -import io.quarkus.deployment.logging.LoggingDecorateBuildItem; -import io.vertx.core.Vertx; - -/** - * Main Chappie Processor. This create a few Build Items also used by the DevUI processor - * - * @author Phillip Kruger (phillip.kruger@gmail.com) - */ -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) -class ConsoleProcessor { - static volatile ConsoleStateManager.ConsoleContext chappieConsoleContext; - - @Consume(ConsoleInstalledBuildItem.class) - @BuildStep - FeatureBuildItem setupConsole(LastExceptionBuildItem lastExceptionBuildItem, - ChappieClientBuildItem chappieClientBuildItem, - LoggingDecorateBuildItem loggingDecorateBuildItem) { - - Path srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); - - if (chappieConsoleContext == null) { - chappieConsoleContext = ConsoleStateManager.INSTANCE.createContext("Assistant"); - } - - Vertx vertx = Vertx.vertx(); - chappieConsoleContext.reset( - new ConsoleCommand('a', "Help with the latest exception", - new ConsoleCommand.HelpState(new Supplier() { - @Override - public String get() { - return MessageFormat.RED; - } - }, new Supplier() { - @Override - public String get() { - LastException lastException = lastExceptionBuildItem.getLastException().get(); - if (lastException == null) { - return "none"; - } - return lastException.throwable().getMessage(); - } - }), new Runnable() { - @Override - public void run() { - LastException lastException = lastExceptionBuildItem.getLastException().get(); - if (lastException == null) { - return; - } - - String sourceString = SourceCodeFinder.getSourceCode(srcMainJava, - lastException.stackTraceElement()); - - String stacktraceString = lastException.getStackTraceString(); - - System.out.println( - "===============\nAssisting with the exception, please wait"); - - long timer = vertx.setPeriodic(800, id -> System.out.print(".")); - - ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); - Object[] params = ParameterCreator.getParameters("", stacktraceString, sourceString); - - CompletableFuture futureResult = chappieClient.executeRPC("exception#suggestfix", - params); - - futureResult.thenAccept(r -> { - vertx.cancelTimer(timer); - Map result = (Map) r; - - System.out.println("\n\n" + result.get("response")); - System.out.println("\n\n" + result.get("explanation")); - System.out.println("\n------ Diff ------ "); - System.out.println("\n\n" + result.get("diff")); - System.out.println("\n------ Suggested source ------ "); - System.out.println("\n\n" + result.get("suggestedSource")); - }).exceptionally(throwable -> { - // Handle any errors - System.out.println("\n\nCould not get a response from ai due the this exception:"); - throwable.printStackTrace(); - return null; - }); - } - })); - - return new FeatureBuildItem(Feature.FEATURE); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionDevUIProcessor.java index 18aee10..e1c6b78 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionDevUIProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionDevUIProcessor.java @@ -6,10 +6,11 @@ import java.nio.file.StandardOpenOption; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; -import io.quarkiverse.chappie.deployment.ChappieEnabled; +import io.quarkiverse.chappie.deployment.ChappieAvailableBuildItem; import io.quarkiverse.chappie.deployment.ChappiePageBuildItem; import io.quarkiverse.chappie.deployment.ParameterCreator; import io.quarkiverse.chappie.deployment.SourceCodeFinder; @@ -30,7 +31,7 @@ import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) +@BuildSteps(onlyIf = IsDevelopment.class) class ExceptionDevUIProcessor { private static final String EXCEPTION_TITLE = "Help with the latest exception"; @@ -62,96 +63,102 @@ public void accept(Throwable throwable, StackTraceElement stackTraceElement) { } @BuildStep - void addActionToErrorPage(BuildProducer errorPageActionsProducer, + void addActionToErrorPage(Optional chappieAvailable, + BuildProducer errorPageActionsProducer, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { - - String url = nonApplicationRootPathBuildItem.resolvePath( - "dev-ui/io.quarkiverse.chappie.quarkus-chappie/" + EXCEPTION_TITLE.replace(" ", "-").toLowerCase()); - errorPageActionsProducer.produce(new ErrorPageActionsBuildItem("Get help with this", url + "?autoSuggest=true")); - + if (chappieAvailable.isPresent()) { + String url = nonApplicationRootPathBuildItem.resolvePath( + "dev-ui/io.quarkiverse.chappie.quarkus-chappie/" + EXCEPTION_TITLE.replace(" ", "-").toLowerCase()); + errorPageActionsProducer.produce(new ErrorPageActionsBuildItem("Get help with this", url + "?autoSuggest=true")); + } } @BuildStep - void exceptionPage(BuildProducer chappiePageBuildItem) { - - chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() - .icon("font-awesome-solid:bug") - .title(EXCEPTION_TITLE) - .componentLink("qwc-chappie-exception.js"))); + void exceptionPage(Optional chappieAvailable, + BuildProducer chappiePageBuildItem) { + if (chappieAvailable.isPresent()) { + chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() + .icon("font-awesome-solid:bug") + .title(EXCEPTION_TITLE) + .componentLink("qwc-chappie-exception.js"))); + } } @BuildStep - void createBuildTimeActions(BuildProducer buildTimeActionProducer, + void createBuildTimeActions(Optional chappieAvailable, + BuildProducer buildTimeActionProducer, LoggingDecorateBuildItem loggingDecorateBuildItem, ChappieClientBuildItem chappieClientBuildItem, BroadcastsBuildItem broadcastsBuildItem, LastExceptionBuildItem lastExceptionBuildItem, LastSolutionBuildItem lastSolutionBuildItem) { - if (srcMainJava == null) { - srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); - } - if (knownClasses == null) { - knownClasses = loggingDecorateBuildItem.getKnowClasses(); - } - BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); - - // This gets the last know exception. For initial load. - buildItemActions.addAction("getLastException", ignored -> { - LastException lastException = lastExceptionBuildItem.getLastException().get(); - if (lastException != null) { - return lastException; + if (chappieAvailable.isPresent()) { + if (srcMainJava == null) { + srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); } - return null; - }); - - // This streams exceptions as they happen - buildItemActions.addSubscription("streamException", ignored -> { - return broadcastsBuildItem.getLastExceptionPublisher(); - }); - - // This suggest fix based on exception and code - buildItemActions.addAction("suggestFix", ignored -> { - LastException lastException = lastExceptionBuildItem.getLastException().get(); - if (lastException != null) { - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, lastException.stackTraceElement()); - String sourceString = SourceCodeFinder.getSourceCode(srcMainJava, lastException.stackTraceElement()); - String stacktraceString = lastException.getStackTraceString(); - - ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); - Object[] params = ParameterCreator.getParameters("", stacktraceString, sourceString); - CompletableFuture result = chappieClient.executeRPC("exception#suggestfix", params); - - result.thenApply(suggestedFix -> { - lastSolutionBuildItem.getLastSolution().set(suggestedFix); - lastSolutionBuildItem.getPath().set(sourcePath); - return suggestedFix; - }); - - return result; + if (knownClasses == null) { + knownClasses = loggingDecorateBuildItem.getKnowClasses(); } - return null; - }); - - // This suggest fix based on exception and code - buildItemActions.addAction("applyFix", code -> { - Object lastSolution = lastSolutionBuildItem.getLastSolution().get(); - Path path = lastSolutionBuildItem.getPath().get(); - if (lastSolution != null && path != null) { - Map jsonRPCResponse = (Map) lastSolution; - if (jsonRPCResponse.containsKey("suggestedSource")) { - String newCode = (String) jsonRPCResponse.get("suggestedSource"); - try { - Files.writeString(path, newCode, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); - return path.toString(); - } catch (IOException ex) { - ex.printStackTrace(); + BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); + + // This gets the last know exception. For initial load. + buildItemActions.addAction("getLastException", ignored -> { + LastException lastException = lastExceptionBuildItem.getLastException().get(); + if (lastException != null) { + return lastException; + } + return null; + }); + + // This streams exceptions as they happen + buildItemActions.addSubscription("streamException", ignored -> { + return broadcastsBuildItem.getLastExceptionPublisher(); + }); + + // This suggest fix based on exception and code + buildItemActions.addAction("suggestFix", ignored -> { + LastException lastException = lastExceptionBuildItem.getLastException().get(); + if (lastException != null) { + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, lastException.stackTraceElement()); + String sourceString = SourceCodeFinder.getSourceCode(srcMainJava, lastException.stackTraceElement()); + String stacktraceString = lastException.getStackTraceString(); + + ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); + Object[] params = ParameterCreator.getParameters("", stacktraceString, sourceString); + CompletableFuture result = chappieClient.executeRPC("exception#suggestfix", params); + + result.thenApply(suggestedFix -> { + lastSolutionBuildItem.getLastSolution().set(suggestedFix); + lastSolutionBuildItem.getPath().set(sourcePath); + return suggestedFix; + }); + + return result; + } + return null; + }); + + // This suggest fix based on exception and code + buildItemActions.addAction("applyFix", code -> { + Object lastSolution = lastSolutionBuildItem.getLastSolution().get(); + Path path = lastSolutionBuildItem.getPath().get(); + if (lastSolution != null && path != null) { + Map jsonRPCResponse = (Map) lastSolution; + if (jsonRPCResponse.containsKey("suggestedSource")) { + String newCode = (String) jsonRPCResponse.get("suggestedSource"); + try { + Files.writeString(path, newCode, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + return path.toString(); + } catch (IOException ex) { + ex.printStackTrace(); + } } } - } - return null; - }); + return null; + }); - buildTimeActionProducer.produce(buildItemActions); + buildTimeActionProducer.produce(buildItemActions); + } } } diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionProcessor.java index a85a55b..75edbd9 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/exception/ExceptionProcessor.java @@ -3,7 +3,6 @@ import java.nio.file.Path; import java.util.concurrent.atomic.AtomicReference; -import io.quarkiverse.chappie.deployment.ChappieEnabled; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -13,7 +12,7 @@ * * @author Phillip Kruger (phillip.kruger@gmail.com) */ -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) +@BuildSteps(onlyIf = IsDevelopment.class) class ExceptionProcessor { @BuildStep diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/explanation/ExplanationDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/explanation/ExplanationDevUIProcessor.java index 82de70e..83c680a 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/explanation/ExplanationDevUIProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/explanation/ExplanationDevUIProcessor.java @@ -3,10 +3,11 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import io.quarkiverse.chappie.deployment.ChappieAvailableBuildItem; import io.quarkiverse.chappie.deployment.ChappieConfig; -import io.quarkiverse.chappie.deployment.ChappieEnabled; import io.quarkiverse.chappie.deployment.ChappiePageBuildItem; import io.quarkiverse.chappie.deployment.ParameterCreator; import io.quarkiverse.chappie.deployment.SourceCodeFinder; @@ -20,7 +21,7 @@ import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.devui.spi.page.Page; -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) +@BuildSteps(onlyIf = IsDevelopment.class) class ExplanationDevUIProcessor { private static final String EXPLANATION_TITLE = "Explain your source"; @@ -28,59 +29,65 @@ class ExplanationDevUIProcessor { static volatile List knownClasses; @BuildStep - void explanationPage(BuildProducer chappiePageBuildItem) { + void explanationPage(Optional chappieAvailable, + BuildProducer chappiePageBuildItem) { - chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() - .icon("font-awesome-solid:file-circle-question") - .title(EXPLANATION_TITLE) - .componentLink("qwc-chappie-explanation.js"))); + if (chappieAvailable.isPresent()) { + chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() + .icon("font-awesome-solid:file-circle-question") + .title(EXPLANATION_TITLE) + .componentLink("qwc-chappie-explanation.js"))); + } } @BuildStep - void createBuildTimeActions(BuildProducer buildTimeActionProducer, + void createBuildTimeActions(Optional chappieAvailable, + BuildProducer buildTimeActionProducer, LoggingDecorateBuildItem loggingDecorateBuildItem, ChappieClientBuildItem chappieClientBuildItem, ChappieConfig chappieConfig) { - if (srcMainJava == null) { - srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); - } - if (knownClasses == null) { - knownClasses = loggingDecorateBuildItem.getKnowClasses(); - } - BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); + if (chappieAvailable.isPresent()) { + if (srcMainJava == null) { + srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); + } + if (knownClasses == null) { + knownClasses = loggingDecorateBuildItem.getKnowClasses(); + } + BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); - buildItemActions.addAction("getKnownClasses", ignored -> { - return knownClasses; - }); + buildItemActions.addAction("getKnownClasses", ignored -> { + return knownClasses; + }); - buildItemActions.addAction("getSourceCode", (Map param) -> { - if (param.containsKey("className")) { - String className = param.get("className"); - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); - return SourceCodeFinder.getSourceCode(sourcePath); - } - return null; - }); + buildItemActions.addAction("getSourceCode", (Map param) -> { + if (param.containsKey("className")) { + String className = param.get("className"); + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); + return SourceCodeFinder.getSourceCode(sourcePath); + } + return null; + }); - buildItemActions.addAction("explainClass", (Map param) -> { - if (param.containsKey("className")) { - String className = param.get("className"); + buildItemActions.addAction("explainClass", (Map param) -> { + if (param.containsKey("className")) { + String className = param.get("className"); - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); - String sourceCode = SourceCodeFinder.getSourceCode(sourcePath); + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); + String sourceCode = SourceCodeFinder.getSourceCode(sourcePath); - if (sourceCode != null) { + if (sourceCode != null) { - ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); - Object[] params = ParameterCreator.getParameters("", sourceCode); - CompletableFuture result = chappieClient.executeRPC("explanation#explain", params); - return result; + ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); + Object[] params = ParameterCreator.getParameters("", sourceCode); + CompletableFuture result = chappieClient.executeRPC("explanation#explain", params); + return result; + } } - } - return null; - }); + return null; + }); - buildTimeActionProducer.produce(buildItemActions); + buildTimeActionProducer.produce(buildItemActions); + } } } diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/javadoc/JavaDocDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/javadoc/JavaDocDevUIProcessor.java index fb55bb0..b945a6b 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/javadoc/JavaDocDevUIProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/javadoc/JavaDocDevUIProcessor.java @@ -6,10 +6,11 @@ import java.nio.file.StandardOpenOption; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -import io.quarkiverse.chappie.deployment.ChappieEnabled; +import io.quarkiverse.chappie.deployment.ChappieAvailableBuildItem; import io.quarkiverse.chappie.deployment.ChappiePageBuildItem; import io.quarkiverse.chappie.deployment.ParameterCreator; import io.quarkiverse.chappie.deployment.SourceCodeFinder; @@ -23,7 +24,7 @@ import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.devui.spi.page.Page; -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) +@BuildSteps(onlyIf = IsDevelopment.class) class JavaDocDevUIProcessor { private static final String JAVADOC_TITLE = "Create javadoc for your source"; @@ -31,88 +32,96 @@ class JavaDocDevUIProcessor { static volatile List knownClasses; @BuildStep - LastJavaDocBuildItem createLastJavaDocReference() { - final AtomicReference lastResponse = new AtomicReference<>(); - final AtomicReference path = new AtomicReference<>(); - return new LastJavaDocBuildItem(lastResponse, path); + void createLastJavaDocReference(Optional chappieAvailable, + BuildProducer lastJavaDocProducer) { + if (chappieAvailable.isPresent()) { + final AtomicReference lastResponse = new AtomicReference<>(); + final AtomicReference path = new AtomicReference<>(); + lastJavaDocProducer.produce(new LastJavaDocBuildItem(lastResponse, path)); + } } @BuildStep - void javaDocPage(BuildProducer chappiePageBuildItem) { - chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() - .icon("font-awesome-solid:book") - .title(JAVADOC_TITLE) - .componentLink("qwc-chappie-javadoc.js"))); + void javaDocPage(Optional chappieAvailable, + BuildProducer chappiePageBuildItem) { + if (chappieAvailable.isPresent()) { + chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() + .icon("font-awesome-solid:book") + .title(JAVADOC_TITLE) + .componentLink("qwc-chappie-javadoc.js"))); + } } @BuildStep - void createBuildTimeActions(BuildProducer buildTimeActionProducer, + void createBuildTimeActions(Optional chappieAvailable, + BuildProducer buildTimeActionProducer, LoggingDecorateBuildItem loggingDecorateBuildItem, LastJavaDocBuildItem lastJavaDocBuildItem, ChappieClientBuildItem chappieClientBuildItem) { + if (chappieAvailable.isPresent()) { + if (srcMainJava == null) { + srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); + } + if (knownClasses == null) { + knownClasses = loggingDecorateBuildItem.getKnowClasses(); + } + BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); - if (srcMainJava == null) { - srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); - } - if (knownClasses == null) { - knownClasses = loggingDecorateBuildItem.getKnowClasses(); - } - BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); - - buildItemActions.addAction("getKnownClasses", ignored -> { - return knownClasses; - }); + buildItemActions.addAction("getKnownClasses", ignored -> { + return knownClasses; + }); - buildItemActions.addAction("getSourceCode", (Map param) -> { - if (param.containsKey("className")) { - String className = param.get("className"); - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); - return SourceCodeFinder.getSourceCode(sourcePath); - } - return null; - }); + buildItemActions.addAction("getSourceCode", (Map param) -> { + if (param.containsKey("className")) { + String className = param.get("className"); + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); + return SourceCodeFinder.getSourceCode(sourcePath); + } + return null; + }); - buildItemActions.addAction("addJavaDoc", (Map param) -> { - if (param.containsKey("className")) { - String className = param.get("className"); + buildItemActions.addAction("addJavaDoc", (Map param) -> { + if (param.containsKey("className")) { + String className = param.get("className"); - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); - String sourceCode = SourceCodeFinder.getSourceCode(sourcePath); + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); + String sourceCode = SourceCodeFinder.getSourceCode(sourcePath); - if (sourceCode != null) { + if (sourceCode != null) { - ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); - Object[] params = ParameterCreator.getParameters("", "JavaDoc", sourceCode); - CompletableFuture result = chappieClient.executeRPC("doc#addDoc", params); + ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); + Object[] params = ParameterCreator.getParameters("", "JavaDoc", sourceCode); + CompletableFuture result = chappieClient.executeRPC("doc#addDoc", params); - result.thenApply(classWithJavaDoc -> { - lastJavaDocBuildItem.getLastResponse().set(classWithJavaDoc); - lastJavaDocBuildItem.getPath().set(sourcePath); - return classWithJavaDoc; - }); - return result; + result.thenApply(classWithJavaDoc -> { + lastJavaDocBuildItem.getLastResponse().set(classWithJavaDoc); + lastJavaDocBuildItem.getPath().set(sourcePath); + return classWithJavaDoc; + }); + return result; + } + } + return null; + }); + + buildItemActions.addAction("save", ignored -> { + String sourceCode = (String) lastJavaDocBuildItem.getLastResponse().get(); + Path srcPath = lastJavaDocBuildItem.getPath().get(); + + try { + Files.createDirectories(srcPath.getParent()); + if (!Files.exists(srcPath)) + Files.createFile(srcPath); + Files.writeString(srcPath, sourceCode, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException ex) { + ex.printStackTrace(); } - } - return null; - }); - - buildItemActions.addAction("save", ignored -> { - String sourceCode = (String) lastJavaDocBuildItem.getLastResponse().get(); - Path srcPath = lastJavaDocBuildItem.getPath().get(); - - try { - Files.createDirectories(srcPath.getParent()); - if (!Files.exists(srcPath)) - Files.createFile(srcPath); - Files.writeString(srcPath, sourceCode, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); - } catch (IOException ex) { - ex.printStackTrace(); - } - return srcPath; - }); + return srcPath; + }); - buildTimeActionProducer.produce(buildItemActions); + buildTimeActionProducer.produce(buildItemActions); + } } } diff --git a/deployment/src/main/java/io/quarkiverse/chappie/deployment/testing/TestingDevUIProcessor.java b/deployment/src/main/java/io/quarkiverse/chappie/deployment/testing/TestingDevUIProcessor.java index acbfd15..def2a2d 100644 --- a/deployment/src/main/java/io/quarkiverse/chappie/deployment/testing/TestingDevUIProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/chappie/deployment/testing/TestingDevUIProcessor.java @@ -6,10 +6,11 @@ import java.nio.file.StandardOpenOption; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -import io.quarkiverse.chappie.deployment.ChappieEnabled; +import io.quarkiverse.chappie.deployment.ChappieAvailableBuildItem; import io.quarkiverse.chappie.deployment.ChappiePageBuildItem; import io.quarkiverse.chappie.deployment.ParameterCreator; import io.quarkiverse.chappie.deployment.SourceCodeFinder; @@ -23,7 +24,7 @@ import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.devui.spi.page.Page; -@BuildSteps(onlyIf = { IsDevelopment.class, ChappieEnabled.class }) +@BuildSteps(onlyIf = IsDevelopment.class) class TestingDevUIProcessor { private static final String TESTING_TITLE = "Create tests for your source"; @@ -31,92 +32,101 @@ class TestingDevUIProcessor { static volatile List knownClasses; @BuildStep - LastTestClassBuildItem createLastTestClassReference() { - final AtomicReference lastResponse = new AtomicReference<>(); - final AtomicReference path = new AtomicReference<>(); - return new LastTestClassBuildItem(lastResponse, path); + void createLastTestClassReference(Optional chappieAvailable, + BuildProducer lastTestClassProducer) { + if (chappieAvailable.isPresent()) { + final AtomicReference lastResponse = new AtomicReference<>(); + final AtomicReference path = new AtomicReference<>(); + lastTestClassProducer.produce(new LastTestClassBuildItem(lastResponse, path)); + } } @BuildStep - void testingPage(BuildProducer chappiePageBuildItem) { - chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() - .icon("font-awesome-solid:flask-vial") - .title(TESTING_TITLE) - .componentLink("qwc-chappie-testing.js"))); + void testingPage(Optional chappieAvailable, + BuildProducer chappiePageBuildItem) { + if (chappieAvailable.isPresent()) { + chappiePageBuildItem.produce(new ChappiePageBuildItem(Page.webComponentPageBuilder() + .icon("font-awesome-solid:flask-vial") + .title(TESTING_TITLE) + .componentLink("qwc-chappie-testing.js"))); + } } @BuildStep - void createBuildTimeActions(BuildProducer buildTimeActionProducer, + void createBuildTimeActions(Optional chappieAvailable, + BuildProducer buildTimeActionProducer, LoggingDecorateBuildItem loggingDecorateBuildItem, LastTestClassBuildItem lastTestClassBuildItem, ChappieClientBuildItem chappieClientBuildItem) { - if (srcMainJava == null) { - srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); - } - if (knownClasses == null) { - knownClasses = loggingDecorateBuildItem.getKnowClasses(); - } - BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); - - buildItemActions.addAction("getKnownClasses", ignored -> { - return knownClasses; - }); - - buildItemActions.addAction("getSourceCode", (Map param) -> { - if (param.containsKey("className")) { - String className = param.get("className"); - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); - return SourceCodeFinder.getSourceCode(sourcePath); + if (chappieAvailable.isPresent()) { + if (srcMainJava == null) { + srcMainJava = loggingDecorateBuildItem.getSrcMainJava(); } - return null; - }); - - buildItemActions.addAction("suggestTestClass", (Map param) -> { - if (param.containsKey("className")) { - String className = param.get("className"); - - Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); - String sourceCode = SourceCodeFinder.getSourceCode(sourcePath); - - if (sourceCode != null) { + if (knownClasses == null) { + knownClasses = loggingDecorateBuildItem.getKnowClasses(); + } + BuildTimeActionBuildItem buildItemActions = new BuildTimeActionBuildItem(); - ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); - Object[] params = ParameterCreator.getParameters( - "Make sure to NOT create a NativeTest, but a normal Quarkus Unit test", sourceCode); - CompletableFuture result = chappieClient.executeRPC("testing#suggesttest", params); + buildItemActions.addAction("getKnownClasses", ignored -> { + return knownClasses; + }); - result.thenApply(suggestedTestClass -> { - lastTestClassBuildItem.getLastResponse().set(suggestedTestClass); - lastTestClassBuildItem.getPath().set(sourcePath); - return suggestedTestClass; - }); - return result; + buildItemActions.addAction("getSourceCode", (Map param) -> { + if (param.containsKey("className")) { + String className = param.get("className"); + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); + return SourceCodeFinder.getSourceCode(sourcePath); + } + return null; + }); + + buildItemActions.addAction("suggestTestClass", (Map param) -> { + if (param.containsKey("className")) { + String className = param.get("className"); + + Path sourcePath = SourceCodeFinder.getSourceCodePath(srcMainJava, className); + String sourceCode = SourceCodeFinder.getSourceCode(sourcePath); + + if (sourceCode != null) { + + ChappieClient chappieClient = chappieClientBuildItem.getChappieClient(); + Object[] params = ParameterCreator.getParameters( + "Make sure to NOT create a NativeTest, but a normal Quarkus Unit test", sourceCode); + CompletableFuture result = chappieClient.executeRPC("testing#suggesttest", params); + + result.thenApply(suggestedTestClass -> { + lastTestClassBuildItem.getLastResponse().set(suggestedTestClass); + lastTestClassBuildItem.getPath().set(sourcePath); + return suggestedTestClass; + }); + return result; + } + } + return null; + }); + + buildItemActions.addAction("saveSuggestion", ignored -> { + Map m = (Map) lastTestClassBuildItem.getLastResponse().get(); + Path srcPath = lastTestClassBuildItem.getPath().get(); + + String sourceCode = (String) m.get("suggestedTestSource"); + Path testPath = createTestPath(srcPath); + + try { + Files.createDirectories(testPath.getParent()); + if (!Files.exists(testPath)) + Files.createFile(testPath); + Files.writeString(testPath, sourceCode, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException ex) { + ex.printStackTrace(); } - } - return null; - }); - - buildItemActions.addAction("saveSuggestion", ignored -> { - Map m = (Map) lastTestClassBuildItem.getLastResponse().get(); - Path srcPath = lastTestClassBuildItem.getPath().get(); - - String sourceCode = (String) m.get("suggestedTestSource"); - Path testPath = createTestPath(srcPath); - - try { - Files.createDirectories(testPath.getParent()); - if (!Files.exists(testPath)) - Files.createFile(testPath); - Files.writeString(testPath, sourceCode, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); - } catch (IOException ex) { - ex.printStackTrace(); - } - return testPath; - }); + return testPath; + }); - buildTimeActionProducer.produce(buildItemActions); + buildTimeActionProducer.produce(buildItemActions); + } } private Path createTestPath(Path srcPath) { diff --git a/deployment/src/main/resources/dev-ui/qwc-chappie-exception.js b/deployment/src/main/resources/dev-ui/qwc-chappie-exception.js index e2aa164..fe39bcb 100644 --- a/deployment/src/main/resources/dev-ui/qwc-chappie-exception.js +++ b/deployment/src/main/resources/dev-ui/qwc-chappie-exception.js @@ -218,6 +218,7 @@ export class QwcChappieException extends observeState(LitElement) { this._showProgressBar = false; this._suggestedFix = jsonRpcResponse.result; }); + this._scrollToBottom(); } _applyFix(){ @@ -227,5 +228,20 @@ export class QwcChappieException extends observeState(LitElement) { }); } + async _scrollToBottom(){ + + await this.updateComplete; + + const last = Array.from( + this.shadowRoot.querySelectorAll('.fix') + ).pop(); + + if(last){ + last.scrollIntoView({ + behavior: "smooth", + block: "end" + }); + } + } } customElements.define('qwc-chappie-exception', QwcChappieException); diff --git a/deployment/src/main/resources/dev-ui/qwc-chappie-explanation.js b/deployment/src/main/resources/dev-ui/qwc-chappie-explanation.js index dd1750c..eb98547 100644 --- a/deployment/src/main/resources/dev-ui/qwc-chappie-explanation.js +++ b/deployment/src/main/resources/dev-ui/qwc-chappie-explanation.js @@ -193,7 +193,23 @@ export class QwcChappieExplanation extends observeState(LitElement) { this._showProgressBar = false; this._explanation = jsonRpcResponse.result; }); + this._scrollToBottom(); } + async _scrollToBottom(){ + + await this.updateComplete; + + const last = Array.from( + this.shadowRoot.querySelectorAll('.fix') + ).pop(); + + if(last){ + last.scrollIntoView({ + behavior: "smooth", + block: "end" + }); + } + } } customElements.define('qwc-chappie-explanation', QwcChappieExplanation); diff --git a/deployment/src/main/resources/dev-ui/qwc-chappie-javadoc.js b/deployment/src/main/resources/dev-ui/qwc-chappie-javadoc.js index 42d310f..0e94696 100644 --- a/deployment/src/main/resources/dev-ui/qwc-chappie-javadoc.js +++ b/deployment/src/main/resources/dev-ui/qwc-chappie-javadoc.js @@ -211,6 +211,7 @@ export class QwcChappieJavaDoc extends observeState(LitElement) { this._showProgressBar = false; this._sourceWithJavaDoc = jsonRpcResponse.result; }); + this._scrollToBottom(); } _saveNewSource(){ @@ -220,5 +221,20 @@ export class QwcChappieJavaDoc extends observeState(LitElement) { }); } + async _scrollToBottom(){ + + await this.updateComplete; + + const last = Array.from( + this.shadowRoot.querySelectorAll('.fix') + ).pop(); + + if(last){ + last.scrollIntoView({ + behavior: "smooth", + block: "end" + }); + } + } } customElements.define('qwc-chappie-javadoc', QwcChappieJavaDoc); diff --git a/deployment/src/main/resources/dev-ui/qwc-chappie-testing.js b/deployment/src/main/resources/dev-ui/qwc-chappie-testing.js index 8eb99b4..9a53bd3 100644 --- a/deployment/src/main/resources/dev-ui/qwc-chappie-testing.js +++ b/deployment/src/main/resources/dev-ui/qwc-chappie-testing.js @@ -212,6 +212,7 @@ export class QwcChappieTesting extends observeState(LitElement) { this._showProgressBar = false; this._suggestedTestSource = jsonRpcResponse.result; }); + this._scrollToBottom(); } _saveTestSource(){ @@ -221,5 +222,20 @@ export class QwcChappieTesting extends observeState(LitElement) { }); } + async _scrollToBottom(){ + + await this.updateComplete; + + const last = Array.from( + this.shadowRoot.querySelectorAll('.fix') + ).pop(); + + if(last){ + last.scrollIntoView({ + behavior: "smooth", + block: "end" + }); + } + } } customElements.define('qwc-chappie-testing', QwcChappieTesting); diff --git a/deployment/src/main/resources/dev-ui/qwc-chappie-unconfigured.js b/deployment/src/main/resources/dev-ui/qwc-chappie-unconfigured.js index 0288ef6..7fabcba 100644 --- a/deployment/src/main/resources/dev-ui/qwc-chappie-unconfigured.js +++ b/deployment/src/main/resources/dev-ui/qwc-chappie-unconfigured.js @@ -1,6 +1,6 @@ import { LitElement, html, css} from 'lit'; import { JsonRpc } from 'jsonrpc'; -import '@vaadin/button'; +import '@qomponent/qui-code-block'; /** * This component shows how to configure Chappie @@ -15,26 +15,42 @@ export class QwcChappieUnconfigured extends LitElement { height: 100%; padding: 10px; } + a { + color: var(--lumo-body-text-color); + } + .code { + color: var(--lumo-primary-text-color); + font-family: Arial; + } `; static properties = { }; - constructor() { - super(); - } - - connectedCallback() { - super.connectedCallback(); - } - - disconnectedCallback() { - super.disconnectedCallback(); - } - render() { - return html`You need to add an API Key`; + return html`To use the AI Assistant in Quarkus Dev Mode, you need to either provide an OpenAI Api Key OR install Ollama +
+
+ +

Using OpenAI

+

+ To use OpenAI you need to provide an OpenAI Api Key + in the quarkus.assistant.openai.api-key property OR set a QUARKUS_ASSISTANT_OPENAI_API_KEY environment variable. +

+ Example:
+ + mvn quarkus:dev -Dquarkus.assistant.openai.api-key=sk.... + +
+

+ +

Using Ollama

+

+ To use Ollama you need to install and run ollama. See ollama.com/download +

+ + `; } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 65e93d5..651cc23 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -15,10 +15,6 @@ - - io.quarkus - quarkus-resteasy-reactive - io.quarkiverse.chappie quarkus-chappie diff --git a/integration-tests/src/main/java/io/quarkiverse/chappie/it/ChappieResource.java b/integration-tests/src/main/java/io/quarkiverse/chappie/it/ChappieResource.java deleted file mode 100644 index 038f120..0000000 --- a/integration-tests/src/main/java/io/quarkiverse/chappie/it/ChappieResource.java +++ /dev/null @@ -1,32 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package io.quarkiverse.chappie.it; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; - -@Path("/chappie") -@ApplicationScoped -public class ChappieResource { - // add some rest methods here - - @GET - public String hello() { - return "Hello chappie"; - } -} diff --git a/integration-tests/src/test/java/io/quarkiverse/chappie/it/ChappieResourceTest.java b/integration-tests/src/test/java/io/quarkiverse/chappie/it/ChappieResourceTest.java index 0752420..58b3b4a 100644 --- a/integration-tests/src/test/java/io/quarkiverse/chappie/it/ChappieResourceTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/chappie/it/ChappieResourceTest.java @@ -1,21 +1,8 @@ package io.quarkiverse.chappie.it; -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.is; - -import org.junit.jupiter.api.Test; - import io.quarkus.test.junit.QuarkusTest; @QuarkusTest public class ChappieResourceTest { - @Test - public void testHelloEndpoint() { - given() - .when().get("/chappie") - .then() - .statusCode(200) - .body(is("Hello chappie")); - } } diff --git a/pom.xml b/pom.xml index 61c4696..2d6cda4 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 17 UTF-8 UTF-8 - 3.14.1 + 999-SNAPSHOT diff --git a/sample/src/main/resources/application.properties b/sample/src/main/resources/application.properties index a5e632c..e69de29 100644 --- a/sample/src/main/resources/application.properties +++ b/sample/src/main/resources/application.properties @@ -1 +0,0 @@ -#%dev.quarkus.assistant.llm=ollama