From ef175ea756a45924b2e6dea75cf777911dd04338 Mon Sep 17 00:00:00 2001 From: John Oliver <1615532+johnoliver@users.noreply.github.com> Date: Thu, 20 Jul 2023 08:22:57 +0100 Subject: [PATCH] Java: Fix a number of planner issues, add blog example (#2077) - Fix the planner overwriting the result even when it is being bound to a variable. - Fix incorrect handling of default values for planner functions - Add example for blog post ### Motivation and Context ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#dev-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- .../Example_PlanWithNativeFunctions.java | 133 ++++++++++++++++++ .../orchestration/AbstractSkFunction.java | 8 +- .../planner/actionplanner/Plan.java | 22 ++- 3 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example_PlanWithNativeFunctions.java diff --git a/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example_PlanWithNativeFunctions.java b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example_PlanWithNativeFunctions.java new file mode 100644 index 000000000000..c92f79e33ccd --- /dev/null +++ b/java/samples/sample-code/src/main/java/com/microsoft/semantickernel/Example_PlanWithNativeFunctions.java @@ -0,0 +1,133 @@ +package com.microsoft.semantickernel; + +import com.azure.ai.openai.OpenAIAsyncClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.microsoft.semantickernel.builders.SKBuilders; +import com.microsoft.semantickernel.connectors.ai.openai.util.AIProviderSettings; +import com.microsoft.semantickernel.connectors.ai.openai.util.AzureOpenAISettings; +import com.microsoft.semantickernel.orchestration.SKContext; +import com.microsoft.semantickernel.planner.actionplanner.Plan; +import com.microsoft.semantickernel.planner.sequentialplanner.SequentialPlanner; +import com.microsoft.semantickernel.skilldefinition.annotations.DefineSKFunction; +import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionInputAttribute; +import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters; +import com.microsoft.semantickernel.textcompletion.TextCompletion; + +import java.io.IOException; +import java.nio.file.Paths; + +public class Example_PlanWithNativeFunctions { + + public static void main(String[] args) throws IOException { + AzureOpenAISettings settings = AIProviderSettings.getAzureOpenAISettingsFromFile( + Paths.get(System.getProperty("user.home"), ".sk", "conf.properties").toAbsolutePath().toString() + ); + + OpenAIAsyncClient client = + new OpenAIClientBuilder() + .endpoint(settings.getEndpoint()) + .credential(new AzureKeyCredential(settings.getKey())) + .buildAsyncClient(); + + TextCompletion textCompletionService = SKBuilders.textCompletionService().build(client, "text-davinci-003"); + + KernelConfig config = SKBuilders.kernelConfig() + .addTextCompletionService("davinci", + kernel -> textCompletionService) + .build(); + + Kernel kernel = SKBuilders.kernel() + .withKernelConfig(config) + .build(); + + kernel.importSkill(new StringFunctions(), "StringFunctions"); + kernel.importSkill(new Emailer(), "Emailer"); + kernel.importSkill(new Names(), "Names"); + + SequentialPlanner planner = new SequentialPlanner(kernel, null, null); + + + Plan plan = planner + .createPlanAsync("Send the input as an email to Steven and make sure I use his preferred name in the email") + .block(); + + System.out.println("\n\n=============================== Plan to execute ==============================="); + System.out.println(plan.toPlanString()); + System.out.println("==============================================================================="); + + + SKContext context = SKBuilders.context().build(); + context.setVariable("subject", "Update"); + + String message = "Hay Steven, just wanted to let you know I have finished."; + + System.out.println("\n\nExecuting plan..."); + SKContext planResult = plan.invokeAsync(message, context, null).block(); + + System.out.println("\n\nPlan Result: " + planResult.getResult()); + } + + public static class StringFunctions { + @DefineSKFunction( + name = "stringReplace", + description = "Takes a message and substitutes string 'from' to 'to'") + public String stringReplace( + @SKFunctionInputAttribute + @SKFunctionParameters(name = "input", description = "The string to perform the replacement on") + String input, + @SKFunctionParameters(name = "from", description = "The string to replace") + String from, + @SKFunctionParameters(name = "to", description = "The string to replace with") + String to + ) { + return input.replaceAll(from, to); + } + } + + public static class Names { + @DefineSKFunction(name = "getNickName", description = "Retrieves the nick name for a given user") + public String getNickName( + @SKFunctionInputAttribute + @SKFunctionParameters(name = "name", description = "The name of the person to get an nick name for") + String name) { + switch (name) { + case "Steven": + return "Code King"; + default: + throw new RuntimeException("Unknown user: " + name); + } + } + + } + + public static class Emailer { + @DefineSKFunction(name = "getEmailAddress", description = "Retrieves the email address for a given user") + public String getEmailAddress( + @SKFunctionInputAttribute + @SKFunctionParameters(name = "name", description = "The name of the person to get an email address for") + String name) { + switch (name) { + case "Steven": + return "codeking@example.com"; + default: + throw new RuntimeException("Unknown user: " + name); + } + } + + @DefineSKFunction(name = "sendEmail", description = "Sends an email") + public String sendEmail( + @SKFunctionParameters(name = "subject", description = "The email subject") String subject, + @SKFunctionParameters(name = "message", description = "The message to email") String message, + @SKFunctionParameters(name = "emailAddress", description = "The emailAddress to send the message to") String emailAddress) { + System.out.println("================= Sending Email ===================="); + System.out.printf("To: %s%n", emailAddress); + System.out.printf("Subject: %s%n", subject); + System.out.printf("Message: %s%n", message); + System.out.println("===================================================="); + return "Message sent"; + } + } + +} + diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/AbstractSkFunction.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/AbstractSkFunction.java index 6412b27c97fd..50f9e030d5d0 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/AbstractSkFunction.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/orchestration/AbstractSkFunction.java @@ -7,6 +7,7 @@ import com.microsoft.semantickernel.skilldefinition.KernelSkillsSupplier; import com.microsoft.semantickernel.skilldefinition.ParameterView; import com.microsoft.semantickernel.skilldefinition.ReadOnlySkillCollection; +import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters; import reactor.core.publisher.Mono; @@ -188,7 +189,12 @@ public String toManualString() { parameter -> { String defaultValueString; if (parameter.getDefaultValue() == null - || parameter.getDefaultValue().isEmpty()) { + || parameter.getDefaultValue().isEmpty() + || parameter + .getDefaultValue() + .equals( + SKFunctionParameters + .NO_DEFAULT_VALUE)) { defaultValueString = ""; } else { defaultValueString = diff --git a/java/semantickernel-extensions-parent/semantickernel-actionplanner-extension/src/main/java/com/microsoft/semantickernel/planner/actionplanner/Plan.java b/java/semantickernel-extensions-parent/semantickernel-actionplanner-extension/src/main/java/com/microsoft/semantickernel/planner/actionplanner/Plan.java index f3741cce4cd3..5f8d57f38dde 100644 --- a/java/semantickernel-extensions-parent/semantickernel-actionplanner-extension/src/main/java/com/microsoft/semantickernel/planner/actionplanner/Plan.java +++ b/java/semantickernel-extensions-parent/semantickernel-actionplanner-extension/src/main/java/com/microsoft/semantickernel/planner/actionplanner/Plan.java @@ -5,11 +5,16 @@ import com.microsoft.semantickernel.Verify; import com.microsoft.semantickernel.builders.SKBuilders; import com.microsoft.semantickernel.memory.SemanticTextMemory; -import com.microsoft.semantickernel.orchestration.*; +import com.microsoft.semantickernel.orchestration.AbstractSkFunction; +import com.microsoft.semantickernel.orchestration.ContextVariables; +import com.microsoft.semantickernel.orchestration.SKContext; +import com.microsoft.semantickernel.orchestration.SKFunction; +import com.microsoft.semantickernel.orchestration.WritableContextVariables; import com.microsoft.semantickernel.planner.PlanningException; import com.microsoft.semantickernel.skilldefinition.FunctionView; import com.microsoft.semantickernel.skilldefinition.KernelSkillsSupplier; import com.microsoft.semantickernel.skilldefinition.ParameterView; +import com.microsoft.semantickernel.skilldefinition.annotations.SKFunctionParameters; import com.microsoft.semantickernel.textcompletion.CompletionRequestSettings; import reactor.core.publisher.Mono; @@ -290,6 +295,17 @@ public Mono invokeNextStepAsync( } }); + // If this function produces an output, don't overwrite the current + // result + if (step.outputs.size() > 0) { + this.state = + this.state + .writableClone() + .setVariable( + ContextVariables.MAIN_KEY, + context.getResult()); + } + sink.next(this); }); } @@ -455,7 +471,9 @@ private ContextVariables getNextStepVariables(ContextVariables variables, Plan s // - Plan.Description String input = ""; - if (step.parameters != null && !Verify.isNullOrEmpty(step.parameters.getInput())) { + if (step.parameters != null + && !Verify.isNullOrEmpty(step.parameters.getInput()) + && !step.parameters.getInput().equals(SKFunctionParameters.NO_DEFAULT_VALUE)) { input = this.expandFromVariables( variables, Objects.requireNonNull(step.parameters.getInput()));