From 2fc53157f324fad95ba3c924d2998900533748d5 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Mon, 20 Apr 2026 17:54:20 +0200 Subject: [PATCH 01/12] Add support to use Ai skills Signed-off-by: Charles Moulliard --- .../010-springboot-replace-bom-quarkus.yaml | 3 + migration-cli/pom.xml | 7 +- .../mtool/commands/TransformCommand.java | 15 +- .../transform/provider/ProviderFactory.java | 2 + .../provider/SkillsEnabledProvider.java | 8 + .../provider/ai/SkillsAssistant.java | 11 ++ .../provider/impl/AiSkillsProvider.java | 142 ++++++++++++++++++ .../provider/model/ExecutionContext.java | 4 +- .../src/main/resources/application.properties | 2 + .../snowdrop/mtool/model/analyze/Rule.java | 2 +- pom.xml | 2 +- .../mtool/tests/analyze/RulesTest.java | 18 +-- 12 files changed, 198 insertions(+), 18 deletions(-) create mode 100644 migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/SkillsEnabledProvider.java create mode 100644 migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java create mode 100644 migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java diff --git a/cookbook/rules/quarkus-spring/010-springboot-replace-bom-quarkus.yaml b/cookbook/rules/quarkus-spring/010-springboot-replace-bom-quarkus.yaml index e35be3b0..12468f5e 100644 --- a/cookbook/rules/quarkus-spring/010-springboot-replace-bom-quarkus.yaml +++ b/cookbook/rules/quarkus-spring/010-springboot-replace-bom-quarkus.yaml @@ -13,6 +13,9 @@ order: 1 instructions: ai: + - skills: + - "spring-boot-to-quarkus/check-jdk" + - "spring-boot-to-quarkus/configuration" - tasks: - "Add to the pom.xml file the Quarkus BOM dependency version 3.31.3 within the dependencyManagement section and the following dependencies: quarkus-arc, quarkus-core and quarkus-smallrye-openapi" - "Remove next from the ôpm.xml the plugin org.springframework.boot:spring-boot-maven-plugin" diff --git a/migration-cli/pom.xml b/migration-cli/pom.xml index 7cf4818c..a09273da 100644 --- a/migration-cli/pom.xml +++ b/migration-cli/pom.xml @@ -81,7 +81,12 @@ io.quarkiverse.langchain4j quarkus-langchain4j-openai - 1.8.4 + ${quarkus-langchain4j.version} + + + io.quarkiverse.langchain4j + quarkus-langchain4j-skills + ${quarkus-langchain4j.version} diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/commands/TransformCommand.java b/migration-cli/src/main/java/dev/snowdrop/mtool/commands/TransformCommand.java index 29cbb2d0..c8614d6d 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/commands/TransformCommand.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/commands/TransformCommand.java @@ -6,6 +6,7 @@ import dev.snowdrop.mtool.model.transform.MigrationTasksExport; import dev.snowdrop.mtool.transform.provider.ai.Assistant; import dev.snowdrop.mtool.transform.provider.ai.FileSystemTool; +import dev.snowdrop.mtool.transform.provider.ai.SkillsAssistant; import dev.snowdrop.mtool.transform.provider.impl.OpenRewriteProvider; import dev.snowdrop.mtool.transform.provider.model.ExecutionContext; import dev.snowdrop.mtool.transform.provider.model.ExecutionResult; @@ -40,7 +41,7 @@ public class TransformCommand implements Runnable { private boolean dryRun; @CommandLine.Option(names = { "-p", - "--provider" }, description = "Migration provider to use (ai, openrewrite, manual). Default: from migration.provider property") + "--provider" }, description = "Migration provider to use (ai, ai-skill, openrewrite, manual). Default: from migration.provider property") @ConfigProperty(name = "migration.provider") private String provider; @@ -50,9 +51,15 @@ public class TransformCommand implements Runnable { @ConfigProperty(name = "openrewrite.maven-plugin.version") private String openRewriteMavenPluginVersion; + @ConfigProperty(name = "quarkus.langchain4j.skills.directories") + private String aiAgentSkillsHomeDir; + @Inject Assistant aiAssistant; + @Inject + SkillsAssistant aiSkillsAssistant; + @Inject FileSystemTool fileSystemTool; @@ -162,8 +169,8 @@ private void startTransformation() { // Configure the Context with the information used by the Provider fileSystemTool.setBasePath(projectPath); - ExecutionContext context = new ExecutionContext(projectPath, verbose, dryRun, provider, aiAssistant, - openRewriteMavenPluginVersion, compositeRecipeName); + ExecutionContext context = new ExecutionContext(projectPath, verbose, dryRun, provider, aiAssistant, aiSkillsAssistant, + aiAgentSkillsHomeDir, openRewriteMavenPluginVersion, compositeRecipeName); if ("openrewrite".equals(provider)) { // Batch all OpenRewrite tasks into a single Maven execution @@ -212,7 +219,7 @@ private void executeTaskWithProvider(String taskId, MigrationTask task, Executio var instructions = task.getRule().instructions(); boolean hasInstructions = instructions != null && switch (provider) { case "openrewrite" -> instructions.openrewrite() != null; - case "ai" -> instructions.ai() != null; + case "ai", "ai-skills" -> instructions.ai() != null; case "manual" -> instructions.manual() != null; default -> false; }; diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ProviderFactory.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ProviderFactory.java index 78500a99..bc0f9595 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ProviderFactory.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ProviderFactory.java @@ -1,6 +1,7 @@ package dev.snowdrop.mtool.transform.provider; import dev.snowdrop.mtool.transform.provider.impl.AiProvider; +import dev.snowdrop.mtool.transform.provider.impl.AiSkillsProvider; import dev.snowdrop.mtool.transform.provider.impl.ManualProvider; import dev.snowdrop.mtool.transform.provider.impl.OpenRewriteProvider; @@ -18,6 +19,7 @@ public class ProviderFactory { static { registerProvider(new OpenRewriteProvider()); registerProvider(new AiProvider()); + registerProvider(new AiSkillsProvider()); registerProvider(new ManualProvider()); } diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/SkillsEnabledProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/SkillsEnabledProvider.java new file mode 100644 index 00000000..9b6c1625 --- /dev/null +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/SkillsEnabledProvider.java @@ -0,0 +1,8 @@ +package dev.snowdrop.mtool.transform.provider; + +import dev.snowdrop.mtool.transform.provider.model.ExecutionContext; +import dev.snowdrop.mtool.transform.provider.model.ExecutionResult; + +public interface SkillsEnabledProvider extends MigrationProvider { + ExecutionResult execute(String skillPath, ExecutionContext context); +} \ No newline at end of file diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java new file mode 100644 index 00000000..57a6b4b7 --- /dev/null +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java @@ -0,0 +1,11 @@ +package dev.snowdrop.mtool.transform.provider.ai; + +import io.quarkiverse.langchain4j.RegisterAiService; +import io.quarkiverse.langchain4j.skills.SkillsSystemMessageProvider; +import jakarta.enterprise.context.ApplicationScoped; + +@RegisterAiService(systemMessageProviderSupplier = SkillsSystemMessageProvider.class) +@ApplicationScoped +public interface SkillsAssistant { + String chat(String message); +} diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java new file mode 100644 index 00000000..2c096acf --- /dev/null +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java @@ -0,0 +1,142 @@ +package dev.snowdrop.mtool.transform.provider.impl; + +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.skills.FileSystemSkillLoader; +import dev.langchain4j.skills.Skills; +import dev.snowdrop.mtool.model.analyze.MigrationTask; +import dev.snowdrop.mtool.model.analyze.Rule; +import dev.snowdrop.mtool.transform.provider.MigrationProvider; +import dev.snowdrop.mtool.transform.provider.ai.SkillsAssistant; +import dev.snowdrop.mtool.transform.provider.model.ExecutionContext; +import dev.snowdrop.mtool.transform.provider.model.ExecutionResult; +import io.quarkiverse.langchain4j.vertexai.runtime.anthropic.VertexAiAnthropicChatModel; +import org.jboss.logging.Logger; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AiSkillsProvider implements MigrationProvider { + private static final Logger logger = Logger.getLogger(AiSkillsProvider.class); + private ChatModel model; + + public AiSkillsProvider() { + String projectId = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PROJECT_ID", "dummy"); + String location = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION", "dummy"); + String modelId = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_MODEL_ID", "claude-opus-4-6"); + + validateRequired(projectId, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PROJECT_ID"); + validateRequired(location, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION"); + + model = VertexAiAnthropicChatModel.builder() + .projectId(projectId) + .location(location) + .modelId(modelId) + .maxOutputTokens(20000) + .logRequests(true) + .logResponses(true) + .build(); + } + + @Override + public String getProviderType() { + return "ai-skills"; + } + + @Override + public ExecutionResult execute(MigrationTask task, ExecutionContext context) { + + // We only process until now only one instruction/rule ! + var ai = Arrays.stream(task.getRule().instructions().ai()).findFirst().orElse(null); + + if (ai.skills() == null || ai.skills().isEmpty()) { + return ExecutionResult.failure("No tasks defined with AI instruction, skipping", null); + } + + try { + ExecutionResult result = executeAiInstruction(ai, task.getRule(), context); + + if (!result.success()) { + return ExecutionResult.failure(result.message(), result.details(), null); + } + + return ExecutionResult.success("AI SKILL execution completed successfully", result.details()); + } catch (Exception e) { + logger.errorf("Error executing AI SKILL: %s", e.getMessage()); + if (context.verbose()) { + e.printStackTrace(); + } + return ExecutionResult.failure("Error executing AI SKILL !", e); + } + } + + private ExecutionResult executeAiInstruction(Rule.Ai ai, Rule rule, ExecutionContext context) { + List details = new ArrayList<>(); + + details.add("Processing rule : " + "" + rule.ruleID()); + + var skills = ai.skills(); + + if (skills == null || skills.isEmpty()) { + return ExecutionResult.failure( + String.format("No AI SKILL defined for rule %s, skipping", rule.ruleID()), null); + } + + // Get the list of SKILL + skills.forEach(skill -> { + logger.infof("Skill: %s", skill); + details.add("- " + skill); + }); + + boolean success = execAiSkillCmd(context, skills); + details.add("AI SKILL cmd executed successfully"); + + if (success) { + return ExecutionResult.success("AI instruction executed successfully", details); + } else { + return ExecutionResult.failure("AI's chat command execution failed", details, null); + } + } + + private boolean execAiSkillCmd(ExecutionContext ctx, List skills) { + logger.info("Hello! I'm your AI SKILL migration assistant and I will help you moving your code"); + + // TODO: Have a property to set the SKILL Agent home folder + // Claude: .claude/skills + + // How could we call a specific skill ? + skills.forEach(s -> { + logger.infof("============= Sending user message to the model"); + Skills agentSkills = Skills.from(FileSystemSkillLoader.loadSkill(Path.of(ctx.aiSkillsHomeDir(), s))); + SkillsAssistant service = AiServices.builder(SkillsAssistant.class) + .chatModel(model) + .toolProvider(agentSkills.toolProvider()) + .build(); + + String response = service.chat("Migrate the application using the SKILL provided"); + logger.infof("============= Model response: %s", response); + }); + + return true; + } + + /** + * Helper to get Env Var or return a default. + */ + private static String getEnv(String name, String defaultValue) { + String val = System.getenv(name); + return (val != null && !val.isBlank()) ? val : defaultValue; + } + + /** + * Helper to enforce required fields. + */ + private static void validateRequired(String value, String envName) { + if (value == null || value.isBlank() || value.equals("dummy")) { + throw new IllegalStateException( + "CRITICAL ERROR: The environment variable '" + envName + "' is required but not set."); + } + } +} diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionContext.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionContext.java index 9102a41a..1d4c8986 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionContext.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionContext.java @@ -1,6 +1,7 @@ package dev.snowdrop.mtool.transform.provider.model; import dev.snowdrop.mtool.transform.provider.ai.Assistant; +import dev.snowdrop.mtool.transform.provider.ai.SkillsAssistant; import java.nio.file.Path; @@ -8,5 +9,6 @@ * Context information for provider execution including project settings and configuration. */ public record ExecutionContext(Path projectPath, boolean verbose, boolean dryRun, String provideType, - Assistant assistant, String openRewriteMavenPluginVersion, String compositeRecipeName) { + Assistant assistant, SkillsAssistant aiSkillsAssistant, String aiSkillsHomeDir, String openRewriteMavenPluginVersion, + String compositeRecipeName) { } \ No newline at end of file diff --git a/migration-cli/src/main/resources/application.properties b/migration-cli/src/main/resources/application.properties index a3470378..c6e29657 100644 --- a/migration-cli/src/main/resources/application.properties +++ b/migration-cli/src/main/resources/application.properties @@ -38,6 +38,8 @@ quarkus.log.category."dev.snowdrop.mtool.transform".level=INFO quarkus.langchain4j.log-requests=false quarkus.langchain4j.log-responses=false quarkus.langchain4j.timeout=30S +quarkus.langchain4j.skills.directories=/Users/cmoullia/code/quarkus/rewrite-mtool/migration-tool/.claude/skills + # Default LLM enabled quarkus.langchain4j.chat-model.provider=vertexai-anthropic diff --git a/model/src/main/java/dev/snowdrop/mtool/model/analyze/Rule.java b/model/src/main/java/dev/snowdrop/mtool/model/analyze/Rule.java index 50bd2f5a..c8b75d74 100644 --- a/model/src/main/java/dev/snowdrop/mtool/model/analyze/Rule.java +++ b/model/src/main/java/dev/snowdrop/mtool/model/analyze/Rule.java @@ -14,7 +14,7 @@ public record Rule(String category, @JsonProperty("customVariables") List tasks) { + public record Ai(@Deprecated String promptMessage, List tasks, List skills) { } public record Manual(String todo) { diff --git a/pom.xml b/pom.xml index de5822b8..6b950c7b 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 3.27.1 5.12.0 0.20.3 - 1.8.4 + 1.9.0.CR2 3.31.3 1.6.1 0.3.4 diff --git a/tests/src/test/java/dev/snowdrop/mtool/tests/analyze/RulesTest.java b/tests/src/test/java/dev/snowdrop/mtool/tests/analyze/RulesTest.java index 69cf952a..cf6bf331 100644 --- a/tests/src/test/java/dev/snowdrop/mtool/tests/analyze/RulesTest.java +++ b/tests/src/test/java/dev/snowdrop/mtool/tests/analyze/RulesTest.java @@ -20,10 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -144,7 +141,8 @@ private Rule createRule001_ReplaceBomQuarkus() { // promptMessage (deprecated en favor de tasks) Arrays.asList( "Add to the pom.xml file the Quarkus BOM dependency within the dependencyManagement section and the following dependencies: quarkus-arc, quarkus-core", - "The version of quarkus to be used and to included within the pom.xml properties is 3.31.3.")) }; + "The version of quarkus to be used and to included within the pom.xml properties is 3.31.3."), + new ArrayList<>()) }; // Manual Instructions Rule.Manual[] manualInstructions = new Rule.Manual[] { new Rule.Manual( @@ -182,7 +180,7 @@ private Rule createRule002_AddQuarkusClass() { // AI Instructions Rule.Ai[] aiInstructions = new Rule.Ai[] { new Rule.Ai(null, // promptMessage (deprecated en favor de tasks) - aiTasks) }; + aiTasks, new ArrayList<>()) }; // Manual Instructions Rule.Manual[] manualInstructions = new Rule.Manual[] { new Rule.Manual("See openrewrite instructions") }; @@ -220,7 +218,7 @@ private Rule createRule003_QuarkusMainAnnotation() { // AI Instructions Rule.Ai[] aiInstructions = new Rule.Ai[] { new Rule.Ai(null, // promptMessage (deprecated en favor de tasks) - aiTasks) }; + aiTasks, new ArrayList<>()) }; // Manual Instructions Rule.Manual[] manualInstructions = new Rule.Manual[] { new Rule.Manual("See openrewrite instructions") }; @@ -229,7 +227,7 @@ private Rule createRule003_QuarkusMainAnnotation() { Rule.Openrewrite[] openrewriteInstructions = new Rule.Openrewrite[] { new Rule.Openrewrite( "Replace the SpringBoot parent dependency with Quarkus BOM within the pom.xml file", "Replace the SpringBoot parent dependency with Quarkus BOM within the pom.xml file.", null, // preconditions - null, // recipeList (aquí podrías añadir los recipes si los necesitas) + null, // recipeList (You could add the recipes here if you need them.) new String[] { "dev.snowdrop.mtool:openrewrite-recipes:1.0.0-SNAPSHOT", "org.openrewrite:rewrite-maven:8.73.0" }) }; @@ -262,8 +260,8 @@ private Rule createRule004_RestAnnotations() { // AI Instructions Rule.Ai[] aiInstructions = new Rule.Ai[] { new Rule.Ai(null, - // promptMessage (deprecated en favor de tasks) - List.of("TODO")) }; + // promptMessage (deprecated in favor of tasks) + List.of("TODO"), new ArrayList<>()) }; // Manual Instructions Rule.Manual[] manualInstructions = new Rule.Manual[] { new Rule.Manual("See openrewrite instructions") }; From c7a60fe34633793e926e1cbb63044dcbd1d0b3ae Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 09:18:38 +0200 Subject: [PATCH 02/12] Set the missing parameter: publisher needed to access Vertex AI API Signed-off-by: Charles Moulliard --- .../mtool/transform/provider/impl/AiSkillsProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java index 2c096acf..c7296fb5 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java @@ -26,6 +26,7 @@ public AiSkillsProvider() { String projectId = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PROJECT_ID", "dummy"); String location = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION", "dummy"); String modelId = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_MODEL_ID", "claude-opus-4-6"); + String publisher = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PUBLISHER", "anthropic"); validateRequired(projectId, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PROJECT_ID"); validateRequired(location, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION"); @@ -34,6 +35,7 @@ public AiSkillsProvider() { .projectId(projectId) .location(location) .modelId(modelId) + .publisher(publisher) .maxOutputTokens(20000) .logRequests(true) .logResponses(true) @@ -106,7 +108,6 @@ private boolean execAiSkillCmd(ExecutionContext ctx, List skills) { // TODO: Have a property to set the SKILL Agent home folder // Claude: .claude/skills - // How could we call a specific skill ? skills.forEach(s -> { logger.infof("============= Sending user message to the model"); Skills agentSkills = Skills.from(FileSystemSkillLoader.loadSkill(Path.of(ctx.aiSkillsHomeDir(), s))); From eba432e4f541ac4fc3525b78596daec0e3ab6355 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 09:45:58 +0200 Subject: [PATCH 03/12] Configure properly the Skills to be executed by the agent. Add duration. Disable the logging Signed-off-by: Charles Moulliard --- .../mtool/transform/provider/ai/SkillsAssistant.java | 2 +- .../transform/provider/impl/AiSkillsProvider.java | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java index 57a6b4b7..a77f39da 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java @@ -4,7 +4,7 @@ import io.quarkiverse.langchain4j.skills.SkillsSystemMessageProvider; import jakarta.enterprise.context.ApplicationScoped; -@RegisterAiService(systemMessageProviderSupplier = SkillsSystemMessageProvider.class) +@RegisterAiService @ApplicationScoped public interface SkillsAssistant { String chat(String message); diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java index c7296fb5..620f3353 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java @@ -14,6 +14,7 @@ import org.jboss.logging.Logger; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,6 +28,7 @@ public AiSkillsProvider() { String location = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION", "dummy"); String modelId = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_MODEL_ID", "claude-opus-4-6"); String publisher = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PUBLISHER", "anthropic"); + int duration = Integer.parseInt(getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_DURATION", "30")); validateRequired(projectId, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PROJECT_ID"); validateRequired(location, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION"); @@ -37,8 +39,9 @@ public AiSkillsProvider() { .modelId(modelId) .publisher(publisher) .maxOutputTokens(20000) - .logRequests(true) - .logResponses(true) + .timeout(Duration.ofSeconds(duration)) + .logRequests(false) + .logResponses(false) .build(); } @@ -110,10 +113,10 @@ private boolean execAiSkillCmd(ExecutionContext ctx, List skills) { skills.forEach(s -> { logger.infof("============= Sending user message to the model"); - Skills agentSkills = Skills.from(FileSystemSkillLoader.loadSkill(Path.of(ctx.aiSkillsHomeDir(), s))); + Skills agentSkill = Skills.from(FileSystemSkillLoader.loadSkill(Path.of(ctx.aiSkillsHomeDir(), s))); SkillsAssistant service = AiServices.builder(SkillsAssistant.class) .chatModel(model) - .toolProvider(agentSkills.toolProvider()) + .systemMessage(agentSkill.formatAvailableSkills()) .build(); String response = service.chat("Migrate the application using the SKILL provided"); From 1317ef2406b117889ddc81a8ece26d8e175e7ae5 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 11:15:55 +0200 Subject: [PATCH 04/12] Use langchain4 bom instead of declaring individually the GAV needed. Create a new method to return warn messages when by example we cannot find skills to be executed. Review how we configure the Skills Assistant Signed-off-by: Charles Moulliard --- migration-cli/pom.xml | 5 ++-- .../transform/TransformationService.java | 4 +++ .../provider/ai/SkillsAssistant.java | 1 - .../provider/impl/AiSkillsProvider.java | 4 ++- .../provider/model/ExecutionResult.java | 26 ++++++++++++++----- pom.xml | 22 +++++++--------- 6 files changed, 39 insertions(+), 23 deletions(-) diff --git a/migration-cli/pom.xml b/migration-cli/pom.xml index a09273da..44704896 100644 --- a/migration-cli/pom.xml +++ b/migration-cli/pom.xml @@ -84,9 +84,8 @@ ${quarkus-langchain4j.version} - io.quarkiverse.langchain4j - quarkus-langchain4j-skills - ${quarkus-langchain4j.version} + dev.langchain4j + langchain4j-skills diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/TransformationService.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/TransformationService.java index eb1ae673..fcdeed0d 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/TransformationService.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/TransformationService.java @@ -27,6 +27,8 @@ public class TransformationService { public void logExecutionResult(ExecutionResult result, boolean verbose) { if (result.success()) { logger.infof("Task completed successfully: %s", result.message()); + } else if (result.warning() != null) { + logger.warn(result.warning()); } else { logger.errorf("Task failed: %s", result.message()); if (result.exception() != null && verbose) { @@ -62,6 +64,8 @@ public ExecutionResult execute(MigrationTask task, ExecutionContext ctx) { if (result.success()) { return ExecutionResult.success(String.format(" %s execution completed successfully", ctx.provideType()), allDetails); + } else if (result.warning() != null) { + return ExecutionResult.warning(String.format(" %s for provider: %s", result.warning(), ctx.provideType())); } else { return ExecutionResult.failure(String.format(" %s execution failed !", ctx.provideType())); } diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java index a77f39da..32a27db0 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java @@ -1,7 +1,6 @@ package dev.snowdrop.mtool.transform.provider.ai; import io.quarkiverse.langchain4j.RegisterAiService; -import io.quarkiverse.langchain4j.skills.SkillsSystemMessageProvider; import jakarta.enterprise.context.ApplicationScoped; @RegisterAiService diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java index 620f3353..9d8ac823 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java @@ -56,8 +56,10 @@ public ExecutionResult execute(MigrationTask task, ExecutionContext context) { // We only process until now only one instruction/rule ! var ai = Arrays.stream(task.getRule().instructions().ai()).findFirst().orElse(null); + assert ai != null; if (ai.skills() == null || ai.skills().isEmpty()) { - return ExecutionResult.failure("No tasks defined with AI instruction, skipping", null); + return ExecutionResult + .warning(String.format("No AI Skills defined part of the rule: %s. Skipping", task.getRule().ruleID())); } try { diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionResult.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionResult.java index 0d8f79d4..3d3fd1d0 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionResult.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/model/ExecutionResult.java @@ -5,40 +5,54 @@ /** * Result of a provider execution with success status and details. */ -public record ExecutionResult(boolean success, String message, List details, Exception exception) { +public record ExecutionResult(boolean success, String message, List details, String warning, Exception exception) { /** * Creates a successful execution result. */ public static ExecutionResult success(String message) { - return new ExecutionResult(true, message, List.of(), null); + return new ExecutionResult(true, message, List.of(), null, null); } /** * Creates a successful execution result with details. */ public static ExecutionResult success(String message, List details) { - return new ExecutionResult(true, message, details, null); + return new ExecutionResult(true, message, details, null, null); + } + + /** + * Creates an execution result with warning. + */ + public static ExecutionResult warning(String warning) { + return new ExecutionResult(false, null, List.of(), warning, null); + } + + /** + * Creates a successful execution result with details and a warning. + */ + public static ExecutionResult warning(List details, String warning) { + return new ExecutionResult(false, null, details, warning, null); } /** * Creates a failed execution result. */ public static ExecutionResult failure(String message) { - return new ExecutionResult(false, message, List.of(), null); + return new ExecutionResult(false, message, List.of(), null, null); } /** * Creates a failed execution result with exception. */ public static ExecutionResult failure(String message, Exception exception) { - return new ExecutionResult(false, message, List.of(), exception); + return new ExecutionResult(false, message, List.of(), null, exception); } /** * Creates a failed execution result with details and exception. */ public static ExecutionResult failure(String message, List details, Exception exception) { - return new ExecutionResult(false, message, details, exception); + return new ExecutionResult(false, message, details, null, exception); } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6b950c7b..2a5be57c 100644 --- a/pom.xml +++ b/pom.xml @@ -25,8 +25,7 @@ 2.13.2 1.46.0 2.18.0 - 1.13.0 - 1.13.0-beta23 + 1.13.0 1.18.44 0.24.0 3.27.1 @@ -100,6 +99,14 @@ import + + dev.langchain4j + langchain4j-bom + ${langchain4j-bom.version} + pom + import + + org.openrewrite.recipe rewrite-recipe-bom @@ -107,6 +114,7 @@ pom import + org.projectlombok lombok @@ -175,16 +183,6 @@ quarkus-langchain4j-agentic ${quarkus-langchain4j.version} - - dev.langchain4j - langchain4j-anthropic - ${langchain4j-anthropic.version} - - - dev.langchain4j - langchain4j-vertex-ai-anthropic - ${langchain4j-vertex-ai-anthropic.version} - io.quarkiverse.langchain4j quarkus-langchain4j-testing-internal From dfe8b1713f1bae3d5e3e02610b816ed11622a359 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 11:46:11 +0200 Subject: [PATCH 05/12] Remove the property to set the path of the skills within the migration-cli/src/main/resources/application.properties as it must be provided as env var by the user Signed-off-by: Charles Moulliard --- migration-cli/src/main/resources/application.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/migration-cli/src/main/resources/application.properties b/migration-cli/src/main/resources/application.properties index c6e29657..b94caf9a 100644 --- a/migration-cli/src/main/resources/application.properties +++ b/migration-cli/src/main/resources/application.properties @@ -38,7 +38,6 @@ quarkus.log.category."dev.snowdrop.mtool.transform".level=INFO quarkus.langchain4j.log-requests=false quarkus.langchain4j.log-responses=false quarkus.langchain4j.timeout=30S -quarkus.langchain4j.skills.directories=/Users/cmoullia/code/quarkus/rewrite-mtool/migration-tool/.claude/skills # Default LLM enabled quarkus.langchain4j.chat-model.provider=vertexai-anthropic From 5db4b3ee194c611c49153c20fb7c65d59ef56d6f Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 11:46:59 +0200 Subject: [PATCH 06/12] Use as artifact: quarkus-langchain4j instead of langchain4j to get the path of the SKILLS using Smallrye config Signed-off-by: Charles Moulliard --- migration-cli/pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/migration-cli/pom.xml b/migration-cli/pom.xml index 44704896..a09273da 100644 --- a/migration-cli/pom.xml +++ b/migration-cli/pom.xml @@ -84,8 +84,9 @@ ${quarkus-langchain4j.version} - dev.langchain4j - langchain4j-skills + io.quarkiverse.langchain4j + quarkus-langchain4j-skills + ${quarkus-langchain4j.version} From c153597719502bdf2c53394d88c83880f17ebe1c Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 11:47:23 +0200 Subject: [PATCH 07/12] Don't pass to the method the details as not used Signed-off-by: Charles Moulliard --- .../snowdrop/mtool/transform/provider/impl/AiProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiProvider.java index 90c5bed0..dbd2ed51 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiProvider.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiProvider.java @@ -74,7 +74,7 @@ private ExecutionResult executeAiInstruction(Rule.Ai ai, Rule rule, ExecutionCon details.add("- " + task); }); - boolean success = execAiCmd(context, tasks, details); + boolean success = execAiCmd(context, tasks); details.add("Ai cmd executed successfully"); if (success) { @@ -84,7 +84,7 @@ private ExecutionResult executeAiInstruction(Rule.Ai ai, Rule rule, ExecutionCon } } - private boolean execAiCmd(ExecutionContext ctx, List tasks, List details) { + private boolean execAiCmd(ExecutionContext ctx, List tasks) { logger.info("Hello! I'm your AI migration assistant and I will help you move your code"); tasks.forEach(t -> { From 4ac84869b76969de3346459caf00477da1fa61c8 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 14:35:20 +0200 Subject: [PATCH 08/12] Set a default value for quarkus.langchain4j.skills.directories Signed-off-by: Charles Moulliard --- migration-cli/src/main/resources/application.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/migration-cli/src/main/resources/application.properties b/migration-cli/src/main/resources/application.properties index b94caf9a..0fadb345 100644 --- a/migration-cli/src/main/resources/application.properties +++ b/migration-cli/src/main/resources/application.properties @@ -38,6 +38,7 @@ quarkus.log.category."dev.snowdrop.mtool.transform".level=INFO quarkus.langchain4j.log-requests=false quarkus.langchain4j.log-responses=false quarkus.langchain4j.timeout=30S +quarkus.langchain4j.skills.directories=~/.claude/skills # Default LLM enabled quarkus.langchain4j.chat-model.provider=vertexai-anthropic From cd1cff06e3fb2e0eb039b7029722b6ab805be8e3 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 16:53:32 +0200 Subject: [PATCH 09/12] Register the toolbox to read/write files locally Signed-off-by: Charles Moulliard --- .../snowdrop/mtool/transform/provider/ai/SkillsAssistant.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java index 32a27db0..d3bd6ea7 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/SkillsAssistant.java @@ -1,10 +1,13 @@ package dev.snowdrop.mtool.transform.provider.ai; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import io.quarkiverse.langchain4j.RegisterAiService; +import io.quarkiverse.langchain4j.ToolBox; import jakarta.enterprise.context.ApplicationScoped; @RegisterAiService @ApplicationScoped public interface SkillsAssistant { + @ToolBox(FileSystemTool.class) String chat(String message); } From 3b54beca8a72bf30f577b711e1ec1894f2c615d4 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Tue, 21 Apr 2026 16:54:19 +0200 Subject: [PATCH 10/12] Add new properties to set: logRequests, logResponses, maxOutputTokens Signed-off-by: Charles Moulliard --- .../provider/impl/AiSkillsProvider.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java index 9d8ac823..8520a8e9 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java @@ -1,12 +1,14 @@ package dev.snowdrop.mtool.transform.provider.impl; import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.response.*; import dev.langchain4j.service.AiServices; import dev.langchain4j.skills.FileSystemSkillLoader; import dev.langchain4j.skills.Skills; import dev.snowdrop.mtool.model.analyze.MigrationTask; import dev.snowdrop.mtool.model.analyze.Rule; import dev.snowdrop.mtool.transform.provider.MigrationProvider; +import dev.snowdrop.mtool.transform.provider.ai.FileSystemTool; import dev.snowdrop.mtool.transform.provider.ai.SkillsAssistant; import dev.snowdrop.mtool.transform.provider.model.ExecutionContext; import dev.snowdrop.mtool.transform.provider.model.ExecutionResult; @@ -19,6 +21,8 @@ import java.util.Arrays; import java.util.List; +import static dev.langchain4j.model.LambdaStreamingResponseHandler.onPartialResponse; + public class AiSkillsProvider implements MigrationProvider { private static final Logger logger = Logger.getLogger(AiSkillsProvider.class); private ChatModel model; @@ -29,6 +33,9 @@ public AiSkillsProvider() { String modelId = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_MODEL_ID", "claude-opus-4-6"); String publisher = getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PUBLISHER", "anthropic"); int duration = Integer.parseInt(getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_DURATION", "30")); + int maxTokens = Integer.parseInt(getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_MAX_TOKENS", "1000")); + Boolean logRequests = Boolean.valueOf(getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOG_REQUESTS", "false")); + Boolean logResponses = Boolean.valueOf(getEnv("QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOG_RESPONSES", "false")); validateRequired(projectId, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_PROJECT_ID"); validateRequired(location, "QUARKUS_LANGCHAIN4J_VERTEXAI_ANTHROPIC_LOCATION"); @@ -38,10 +45,10 @@ public AiSkillsProvider() { .location(location) .modelId(modelId) .publisher(publisher) - .maxOutputTokens(20000) + .maxOutputTokens(maxTokens) .timeout(Duration.ofSeconds(duration)) - .logRequests(false) - .logResponses(false) + .logRequests(logRequests) + .logResponses(logResponses) .build(); } @@ -114,15 +121,16 @@ private boolean execAiSkillCmd(ExecutionContext ctx, List skills) { // Claude: .claude/skills skills.forEach(s -> { - logger.infof("============= Sending user message to the model"); + logger.infof("=== Processing SKILL: %S", s); Skills agentSkill = Skills.from(FileSystemSkillLoader.loadSkill(Path.of(ctx.aiSkillsHomeDir(), s))); SkillsAssistant service = AiServices.builder(SkillsAssistant.class) .chatModel(model) .systemMessage(agentSkill.formatAvailableSkills()) .build(); - String response = service.chat("Migrate the application using the SKILL provided"); - logger.infof("============= Model response: %s", response); + String response = service + .chat(String.format("Migrate the application of the current directory using the SKILL: %s", s)); + logger.infof("=== Model response: %s", response); }); return true; From 0f35645cee4e366352197d4f6543816933d024cd Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Wed, 22 Apr 2026 11:34:26 +0200 Subject: [PATCH 11/12] Add listFiles tool Signed-off-by: Charles Moulliard --- .../transform/provider/ai/FileSystemTool.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/FileSystemTool.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/FileSystemTool.java index 5a71f83b..2a339736 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/FileSystemTool.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/ai/FileSystemTool.java @@ -9,6 +9,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; /** * A LangChain4j tool that exposes file system read and write operations to AI agents. @@ -63,7 +65,29 @@ public String readFile(@P("Path to the file to analyze") String path) { logger.debugf("Reading file: %s", resolved); return Files.readString(resolved); } catch (IOException e) { - return "Error reading file: " + e.getMessage(); + return String.format("Skip the path: %s as this %s", path, e.getMessage()); + } + } + + @Tool("Whenever you need to know the files part of an application to understand the java project structure") + public List listFiles(@P("The relative path from the project root. Use '.' for the root directory. " + + "Example: 'src/main/java' or 'src/test/resources'. " + + "Do not use absolute paths or leading slashes.") String path) + throws IOException { + + Path resolved = resolve(path); + + if (!resolved.startsWith(basePath)) { + return List.of("Error: Access denied. You cannot look outside the project root."); + } + + if (!Files.exists(resolved)) { + return List.of("Error: Directory does not exist: " + path); + } + + try (var stream = Files.list(resolved)) { + return stream.map(p -> basePath.relativize(p).toString()) + .collect(Collectors.toList()); } } From 69d3f0aa9041b0d7690c920955e76813c6ca79a3 Mon Sep 17 00:00:00 2001 From: Charles Moulliard Date: Wed, 22 Apr 2026 11:35:09 +0200 Subject: [PATCH 12/12] Set the sequential tools invocation to 100 Signed-off-by: Charles Moulliard --- .../snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java index 8520a8e9..3a40fb81 100644 --- a/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java +++ b/migration-cli/src/main/java/dev/snowdrop/mtool/transform/provider/impl/AiSkillsProvider.java @@ -126,6 +126,7 @@ private boolean execAiSkillCmd(ExecutionContext ctx, List skills) { SkillsAssistant service = AiServices.builder(SkillsAssistant.class) .chatModel(model) .systemMessage(agentSkill.formatAvailableSkills()) + .maxSequentialToolsInvocations(100) .build(); String response = service