diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index d60c3e3f023c..26bb8e881632 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -70,6 +70,10 @@ + + + + diff --git a/dotnet/SK-dotnet.slnx b/dotnet/SK-dotnet.slnx index 63b2caf15c85..b24a49cbd285 100644 --- a/dotnet/SK-dotnet.slnx +++ b/dotnet/SK-dotnet.slnx @@ -88,11 +88,11 @@ + + - - @@ -139,8 +139,8 @@ - + @@ -284,11 +284,6 @@ - - - - - @@ -300,32 +295,33 @@ + - + - - + - + - + - + - + - + - + - + + - - - + + + diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 9e657dc5e881..18c9411c171e 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -88,8 +88,6 @@ - - diff --git a/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs b/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs deleted file mode 100644 index 63f62e9d77be..000000000000 --- a/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs +++ /dev/null @@ -1,386 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Concurrent; -using System.ComponentModel; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Planning; -using OpenAI.Chat; - -using ChatMessageContent = Microsoft.SemanticKernel.ChatMessageContent; - -namespace Planners; - -/// -/// This example shows how to implement plan generation and execution with Auto Function Calling and -/// enable telemetry, filters and caching. -/// object is used for plan manipulation and execution. -/// -public class AutoFunctionCallingPlanning(ITestOutputHelper output) : BaseTest(output) -{ - /// Test goal which is used in all examples in this file for comparison purposes. - private const string Goal = "Check current UTC time and return current weather in Boston city."; - - /// JSON serialization configuration for readable output. - private readonly JsonSerializerOptions _jsonSerializerOptions = new() { WriteIndented = true }; - - /// - /// This method contains side by side comparison of Auto Function Calling with FunctionCallingStepwisePlanner. - /// Both approaches allow to generate and execute a plan by using object. - /// - [Fact] - public async Task SideBySideComparisonWithStepwisePlannerAsync() - { - var kernel = GetKernel(); - - // 1.1 Plan execution using FunctionCallingStepwisePlanner. - var planner = new FunctionCallingStepwisePlanner(); - var plannerResult = await planner.ExecuteAsync(kernel, Goal); - - Console.WriteLine($"Planner execution result: {plannerResult.FinalAnswer}"); - Console.WriteLine($"Chat history containing the planning process: {JsonSerializer.Serialize(plannerResult.ChatHistory, _jsonSerializerOptions)}"); - Console.WriteLine($"Planner execution tokens: {GetChatHistoryTokens(plannerResult.ChatHistory)}"); - - // Output: - // Planner execution result: The current UTC time is Sat, 06 Jul 2024 02:11:10 GMT and the weather in Boston is 61 and rainy. - // Planner execution tokens: 1380 - - // 1.2 Plan execution using Auto Function Calling. - var functionCallingChatHistory = new ChatHistory(); - var chatCompletionService = kernel.GetRequiredService(); - var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - - functionCallingChatHistory.AddUserMessage(Goal); - - var functionCallingResult = await chatCompletionService.GetChatMessageContentAsync(functionCallingChatHistory, executionSettings, kernel); - - Console.WriteLine($"Auto Function Calling execution result: {functionCallingResult.Content}"); - Console.WriteLine($"Chat history containing the planning process: {JsonSerializer.Serialize(functionCallingChatHistory, _jsonSerializerOptions)}"); - Console.WriteLine($"Auto Function Calling execution tokens: {GetChatHistoryTokens(functionCallingChatHistory)}"); - - // Output: - // Auto Function Calling execution result: The current UTC time is Sat, 06 Jul 2024 02:11:16 GMT.The weather right now in Boston is 61 degrees and rainy. - // Auto Function Calling execution tokens: 243 - - // 2.1 Plan re-execution using FunctionCallingStepwisePlanner. - // ChatHistory (plan) should be passed without 2 last messages from previously generated ChatHistory. - plannerResult = await planner.ExecuteAsync(kernel, Goal, new ChatHistory(plannerResult.ChatHistory!.Take(..^2))); - Console.WriteLine($"Planner re-execution result: {plannerResult.FinalAnswer}"); - - // 2.2. Plan re-execution using Auto Function Calling. - functionCallingResult = await chatCompletionService.GetChatMessageContentAsync(functionCallingChatHistory, executionSettings, kernel); - Console.WriteLine($"Auto Function Calling re-execution result: {functionCallingResult.Content}"); - } - - /// - /// This method shows different plan execution options. - /// If generated plan is not important and only result is needed - it's possible to use object directly to generate and execute a plan. - /// If generated plan is important, then an access to is required. It's possible to get it by using . - /// - [Fact] - public async Task PlanExecutionOptionsAsync() - { - var kernel = GetKernel(); - - var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - - // If result is the only thing that is needed without generated plan, it's possible to create and execute a plan using Kernel object. - var kernelResult = await kernel.InvokePromptAsync(Goal, new(executionSettings)); - - Console.WriteLine($"Kernel result: {kernelResult}"); - // Output: Kernel result: The current UTC time is Tue, 02 Jul 2024 01:15:28 GMT. The weather in Boston city is 61 degrees and rainy. - - // If result is needed together with generated plan, chat completion service should be used to get an access to the chat history object (generated plan). - var chatCompletionService = kernel.GetRequiredService(); - var chatHistory = new ChatHistory(); - - chatHistory.AddUserMessage(Goal); - - var chatCompletionServiceResult = await chatCompletionService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel); - - Console.WriteLine($"Chat completion service result: {chatCompletionServiceResult.Content}"); - Console.WriteLine($"Chat history containing the planning process: {JsonSerializer.Serialize(chatHistory, _jsonSerializerOptions)}"); - // Output: Chat completion service result: The current UTC time is Tue, 02 Jul 2024 01:15:32 GMT. The weather in Boston city is 61 degrees and rainy. - } - - /// - /// This method shows the telemetry which is produced when using Auto Function Calling to generate and execute a plan. - /// The example contains produced logs, but metering and tracing are also supported. - /// More information here: https://github.com/microsoft/semantic-kernel/blob/main/dotnet/docs/TELEMETRY.md. - /// - [Fact] - public async Task TelemetryForPlanGenerationAndExecutionAsync() - { - var kernel = GetKernel(enableLogging: true); - - var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - - var result = await kernel.InvokePromptAsync(Goal, new(executionSettings)); - - Console.WriteLine($"Kernel result: {result}"); - - // Output: - // Function InvokePromptAsync_Id invoking. - // Function arguments: {} - // Rendered prompt: Check current UTC time and return current weather in Boston city. - // ChatHistory: [{"Role":{"Label":"user"... - // Prompt tokens: 86. Completion tokens: 11. Total tokens: 97. - // Tool requests: 1 - // Function call requests: HelperFunctions-GetCurrentUtcTime({}) - // Function GetCurrentUtcTime invoking. - // Function arguments: {} - // Function GetCurrentUtcTime succeeded. - // Function result: Tue, 02 Jul 2024 01:20:07 GMT - // Function completed. Duration: 0.0015557s - // Prompt tokens: 124. Completion tokens: 21. Total tokens: 145. - // Tool requests: 1 - // Function call requests: HelperFunctions-GetWeatherForCity({"cityName": "Boston"}) - // Function GetWeatherForCity invoking. - // Function arguments: {"cityName":"Boston"} - // Function GetWeatherForCity succeeded. - // Function result: 61 and rainy - // Function completed. Duration: 0.0019822s - // Prompt tokens: 161. Completion tokens: 34. Total tokens: 195. - // Function InvokePromptAsync_Id succeeded. - // Function result: The current time in UTC is Tue, 02 Jul 2024 01:20:07 GMT. The weather in Boston is 61 degrees and rainy. - // Function completed. Duration: 5.1014667s - // Kernel result: The current time in UTC is Tue, 02 Jul 2024 01:20:07 GMT. The weather in Boston is 61 degrees and rainy. - } - - /// - /// This method shows how to cache object (generated plan) in order to re-use it later for the same goal. - /// is used as a caching decorator, which is backed by in-memory cache for demonstration purposes. - /// - [Fact] - public async Task PlanCachingForReusabilityAsync() - { - var kernel = GetKernel(); - var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - - // Wrap chat completion service from Kernel in caching decorator. - var chatCompletionService = new CachedChatCompletionService(kernel.GetRequiredService()); - - Console.WriteLine("First run:"); - - var firstChatHistory = new ChatHistory([new ChatMessageContent(AuthorRole.User, Goal)]); - var chatCompletionServiceResult = await ExecuteWithStopwatchAsync(() - => chatCompletionService.GetChatMessageContentAsync(firstChatHistory, executionSettings, kernel)); - - Console.WriteLine($"Plan execution result: {chatCompletionServiceResult.Content}"); - - Console.WriteLine("Second run:"); - - // New chat history is used without responses from previous run to demonstrate that previous chat history is stored in cache - // and can be accessed by the same goal. - var secondChatHistory = new ChatHistory([new ChatMessageContent(AuthorRole.User, Goal)]); - chatCompletionServiceResult = await ExecuteWithStopwatchAsync(() - => chatCompletionService.GetChatMessageContentAsync(secondChatHistory, executionSettings, kernel)); - - Console.WriteLine($"Plan execution result: {chatCompletionServiceResult.Content}"); - - // Output: - // First run: - // Elapsed Time: 00:00:04.211 - // Plan execution result: The current UTC time is Tue, 02 Jul 2024 02:23:08 GMT and the weather in Boston is 61 degrees and rainy. - // Second run: - // Elapsed Time: 00:00:01.615 - // Plan execution result: The current UTC time is Tue, 02 Jul 2024 02:23:08 GMT and the current weather in Boston is 61°F and rainy. - } - - /// - /// This method shows how to get more control over plan execution using Filters. - /// is used to override the result of specific plan step (function). - /// - [Fact] - public async Task UsingFiltersToControlPlanExecutionAsync() - { - var kernel = GetKernel(); - - kernel.FunctionInvocationFilters.Add(new PlanExecutionFilter()); - - var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; - - var result = await kernel.InvokePromptAsync(Goal, new(executionSettings)); - - Console.WriteLine($"Kernel result: {result}"); - // Output: Kernel result: The current UTC time is Tue, 02 Jul 2024 01:38:18 GMT and the current weather in Boston city is 70 and sunny. - } - - /// - /// Filter to control plan execution and each step (function). - /// With filters it's possible to observe which step is going to be executed and its arguments, handle exceptions, override step result. - /// - private sealed class PlanExecutionFilter : IFunctionInvocationFilter - { - public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - await next(context); - - // For GetWeatherForCity step, when cityName argument is Boston - return "70 and sunny" result. - if (context.Function.Name.Equals(nameof(WeatherPlugin.GetWeatherForCity), StringComparison.OrdinalIgnoreCase) && - context.Arguments.TryGetValue("cityName", out object? cityName) && - cityName!.ToString()!.Equals("Boston", StringComparison.OrdinalIgnoreCase)) - { - // Override step result. - context.Result = new FunctionResult(context.Result, "70 and sunny"); - } - } - } - - /// - /// Caching decorator to re-use previously generated plan and execute it. - /// This allows to skip plan generation process for the same goal. - /// - private sealed class CachedChatCompletionService(IChatCompletionService innerChatCompletionService) : IChatCompletionService - { - /// In-memory cache for demonstration purposes. - private readonly ConcurrentDictionary _inMemoryCache = new(); - - public IReadOnlyDictionary Attributes => innerChatCompletionService.Attributes; - - public async Task> GetChatMessageContentsAsync( - ChatHistory chatHistory, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - { - // Generate cache key. - var key = GetCacheKey(chatHistory); - - // Get chat history from cache or use original one. - var chatHistoryToUse = this._inMemoryCache.TryGetValue(key, out string? cachedChatHistory) ? - JsonSerializer.Deserialize(cachedChatHistory) : - chatHistory; - - // Execute a request. - var result = await innerChatCompletionService.GetChatMessageContentsAsync(chatHistoryToUse!, executionSettings, kernel, cancellationToken); - - // Store generated chat history in cache for future usage. - this._inMemoryCache[key] = JsonSerializer.Serialize(chatHistoryToUse); - - return result; - } - - public async IAsyncEnumerable GetStreamingChatMessageContentsAsync( - ChatHistory chatHistory, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (var item in innerChatCompletionService.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken)) - { - yield return item; - } - } - - /// - /// Hashing is used for a cache key generation for demonstration purposes. - /// Cache key generation should be implemented based on specific scenario and requirements. - /// - private static string GetCacheKey(ChatHistory chatHistory) - { - var goal = chatHistory.First(l => l.Role == AuthorRole.User).Content!; - - byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(goal)); - - return Convert.ToHexString(bytes).Replace("-", "").ToUpperInvariant(); - } - } - - #region Helper methods - - private Kernel GetKernel(bool enableLogging = false) - { - var builder = Kernel - .CreateBuilder() - .AddOpenAIChatCompletion("gpt-4", TestConfiguration.OpenAI.ApiKey); - - if (enableLogging) - { - builder.Services.AddSingleton(this.LoggerFactory); - } - - var kernel = builder.Build(); - - // Import sample plugins. - kernel.ImportPluginFromType(); - kernel.ImportPluginFromType(); - - return kernel; - } - - private int GetChatHistoryTokens(ChatHistory? chatHistory) - { - var tokens = 0; - - if (chatHistory is null) - { - return tokens; - } - - foreach (var message in chatHistory) - { - if (message.Metadata is not null && - message.Metadata.TryGetValue("Usage", out object? usage) && - usage is ChatTokenUsage completionsUsage && - completionsUsage is not null) - { - tokens += completionsUsage.TotalTokenCount; - } - } - - return tokens; - } - - private async Task ExecuteWithStopwatchAsync(Func> action) - { - var stopwatch = Stopwatch.StartNew(); - - var result = await action(); - - stopwatch.Stop(); - - Console.WriteLine($@"Elapsed Time: {stopwatch.Elapsed:hh\:mm\:ss\.FFF}"); - - return result; - } - - #endregion - - #region Sample plugins - - private sealed class TimePlugin - { - [KernelFunction] - [Description("Retrieves the current time in UTC")] - public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); - } - - private sealed class WeatherPlugin - { - [KernelFunction] - [Description("Gets the current weather for the specified city")] - public string GetWeatherForCity(string cityName) => - cityName switch - { - "Boston" => "61 and rainy", - "London" => "55 and cloudy", - "Miami" => "80 and sunny", - "Paris" => "60 and rainy", - "Tokyo" => "50 and sunny", - "Sydney" => "75 and sunny", - "Tel Aviv" => "80 and sunny", - _ => "31 and snowing", - }; - } - - #endregion -} diff --git a/dotnet/samples/Concepts/Planners/FunctionCallStepwisePlanning.cs b/dotnet/samples/Concepts/Planners/FunctionCallStepwisePlanning.cs deleted file mode 100644 index f07b890b86c9..000000000000 --- a/dotnet/samples/Concepts/Planners/FunctionCallStepwisePlanning.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Plugins.Core; - -namespace Planners; - -public class FunctionCallStepwisePlanning(ITestOutputHelper output) : BaseTest(output) -{ - [Fact] - public async Task RunAsync() - { - string[] questions = - [ - "What is the current hour number, plus 5?", - "What is 387 minus 22? Email the solution to John and Mary.", - "Write a limerick, translate it to Spanish, and send it to Jane", - ]; - - var kernel = InitializeKernel(); - - var options = new FunctionCallingStepwisePlannerOptions - { - MaxIterations = 15, - MaxTokens = 4000, - }; - var planner = new Microsoft.SemanticKernel.Planning.FunctionCallingStepwisePlanner(options); - - foreach (var question in questions) - { - FunctionCallingStepwisePlannerResult result = await planner.ExecuteAsync(kernel, question); - Console.WriteLine($"Q: {question}\nA: {result.FinalAnswer}"); - - // You can uncomment the line below to see the planner's process for completing the request. - // Console.WriteLine($"Chat history:\n{System.Text.Json.JsonSerializer.Serialize(result.ChatHistory)}"); - } - } - - /// - /// Initialize the kernel and load plugins. - /// - /// A kernel instance - private static Kernel InitializeKernel() - { - Kernel kernel = Kernel.CreateBuilder() - .AddOpenAIChatCompletion( - apiKey: TestConfiguration.OpenAI.ApiKey, - modelId: "gpt-3.5-turbo-1106") - .Build(); - - kernel.ImportPluginFromType(); - kernel.ImportPluginFromType(); - kernel.ImportPluginFromType(); - - return kernel; - } - - private sealed class MathPlugin - { - [KernelFunction, Description("Adds an amount to a value")] - [return: Description("The sum")] - public int Add( - [Description("The value to add")] int value, - [Description("Amount to add")] int amount) => - value + amount; - - [KernelFunction, Description("Subtracts an amount from a value")] - [return: Description("The difference")] - public int Subtract( - [Description("The value to subtract")] int value, - [Description("Amount to subtract")] int amount) => - value - amount; - } -} diff --git a/dotnet/samples/Concepts/Planners/HandlebarsPlanning.cs b/dotnet/samples/Concepts/Planners/HandlebarsPlanning.cs deleted file mode 100644 index a9225404419a..000000000000 --- a/dotnet/samples/Concepts/Planners/HandlebarsPlanning.cs +++ /dev/null @@ -1,450 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Planning.Handlebars; -using Plugins.DictionaryPlugin; -using Resources; -using xRetry; - -namespace Planners; - -// This example shows how to use the Handlebars sequential planner. -public class HandlebarsPlanning(ITestOutputHelper output) : BaseTest(output) -{ - private static int s_sampleIndex; - - private const string CourseraPluginName = "CourseraPlugin"; - - private void WriteSampleHeading(string name) - { - Console.WriteLine($"======== [Handlebars Planner] Sample {s_sampleIndex++} - Create and Execute Plan with: {name} ========"); - } - - private async Task SetupKernelAsync(params string[] pluginDirectoryNames) - { - string apiKey = TestConfiguration.AzureOpenAI.ApiKey; - string chatDeploymentName = TestConfiguration.AzureOpenAI.ChatDeploymentName; - string chatModelId = TestConfiguration.AzureOpenAI.ChatModelId; - string endpoint = TestConfiguration.AzureOpenAI.Endpoint; - - if (apiKey is null || chatDeploymentName is null || chatModelId is null || endpoint is null) - { - Console.WriteLine("Azure endpoint, apiKey, deploymentName, or modelId not found. Skipping example."); - return null; - } - - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion( - deploymentName: chatDeploymentName, - endpoint: endpoint, - serviceId: "AzureOpenAIChat", - apiKey: apiKey, - modelId: chatModelId) - .Build(); - - if (pluginDirectoryNames.Length > 0) - { - if (pluginDirectoryNames[0] == StringParamsDictionaryPlugin.PluginName) - { - kernel.ImportPluginFromType(StringParamsDictionaryPlugin.PluginName); - } - else if (pluginDirectoryNames[0] == ComplexParamsDictionaryPlugin.PluginName) - { - kernel.ImportPluginFromType(ComplexParamsDictionaryPlugin.PluginName); - } - else if (pluginDirectoryNames[0] == CourseraPluginName) - { - await kernel.ImportPluginFromOpenApiAsync( - CourseraPluginName, - new Uri("https://www.coursera.org/api/rest/v1/search/openapi.yaml") - ); - } - else - { - string folder = RepoFiles.SamplePluginsPath(); - - foreach (var pluginDirectoryName in pluginDirectoryNames) - { - kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, pluginDirectoryName)); - } - } - } - - return kernel; - } - - private void PrintPlannerDetails(string goal, HandlebarsPlan plan, string result, bool shouldPrintPrompt) - { - Console.WriteLine($"Goal: {goal}"); - Console.WriteLine($"\nOriginal plan:\n{plan}"); - Console.WriteLine($"\nResult:\n{result}\n"); - - // Print the prompt template - if (shouldPrintPrompt && plan.Prompt is not null) - { - Console.WriteLine("\n======== CreatePlan Prompt ========"); - Console.WriteLine(plan.Prompt); - } - } - - private async Task RunSampleAsync( - string goal, - HandlebarsPlannerOptions? plannerOptions = null, - KernelArguments? initialContext = null, - bool shouldPrintPrompt = false, - bool shouldInvokePlan = true, - params string[] pluginDirectoryNames) - { - var kernel = await SetupKernelAsync(pluginDirectoryNames); - if (kernel is null) - { - return; - } - - // Set the planner options - plannerOptions ??= new HandlebarsPlannerOptions() - { - // When using OpenAI models, we recommend using low values for temperature and top_p to minimize planner hallucinations. - ExecutionSettings = new OpenAIPromptExecutionSettings() - { - Temperature = 0.0, - TopP = 0.1, - }, - }; - - // Use gpt-4 or newer models if you want to test with loops. - // Older models like gpt-35-turbo are less recommended. They do handle loops but are more prone to syntax errors. - plannerOptions.AllowLoops = TestConfiguration.AzureOpenAI.ChatDeploymentName.Contains("gpt-4", StringComparison.OrdinalIgnoreCase); - - // Instantiate the planner and create the plan - var planner = new HandlebarsPlanner(plannerOptions); - var plan = await planner.CreatePlanAsync(kernel, goal, initialContext); - - // Execute the plan - var result = shouldInvokePlan ? await plan.InvokeAsync(kernel, initialContext) : string.Empty; - - PrintPlannerDetails(goal, plan, result, shouldPrintPrompt); - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(false)] - public async Task PlanNotPossibleSampleAsync(bool shouldPrintPrompt) - { - try - { - WriteSampleHeading("Plan Not Possible"); - - // Load additional plugins to enable planner but not enough for the given goal. - await RunSampleAsync("Send Mary an email with the list of meetings I have scheduled today.", null, null, shouldPrintPrompt, true, "SummarizePlugin"); - /* - [InsufficientFunctionsForGoal] Unable to create plan for goal with available functions. - Goal: Send Mary an email with the list of meetings I have scheduled today. - Available Functions: SummarizePlugin-MakeAbstractReadable, SummarizePlugin-Notegen, SummarizePlugin-Summarize, SummarizePlugin-Topics - Planner output: - As the available helpers do not contain any functionality to send an email or interact with meeting scheduling data, I cannot create a template to achieve the stated goal. - Additional helpers or information may be required. - */ - } - catch (Exception e) - { - Console.WriteLine(e.InnerException?.Message); - } - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(true)] - - public Task RunCourseraSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("Coursera OpenAPI Plugin"); - return RunSampleAsync("Show me courses about Artificial Intelligence.", null, null, shouldPrintPrompt, true, CourseraPluginName); - /* - Original plan: - {{!-- Step 0: Extract key values --}} - {{set "query" "Artificial Intelligence"}} - - {{!-- Step 1: Call CourseraPlugin-search with the query --}} - {{set "searchResults" (CourseraPlugin-search query=query)}} - - {{!-- Step 2: Loop through the search results and display course information --}} - {{#each searchResults.hits}} - {{json (concat "Course Name: " this.name ", URL: " this.objectUrl)}} - {{/each}} - - Result: - Course Name: Introduction to Artificial Intelligence (AI), URL: https://www.coursera.org/learn/introduction-to-ai?utm_source=rest_api - Course Name: IBM Applied AI, URL: https://www.coursera.org/professional-certificates/applied-artifical-intelligence-ibm-watson-ai?utm_source=rest_api - Course Name: AI For Everyone, URL: https://www.coursera.org/learn/ai-for-everyone?utm_source=rest_api - Course Name: Python for Data Science, AI & Development, URL: https://www.coursera.org/learn/python-for-applied-data-science-ai?utm_source=rest_api - Course Name: Introduction to Generative AI, URL: https://www.coursera.org/learn/introduction-to-generative-ai?utm_source=rest_api - Course Name: Deep Learning, URL: https://www.coursera.org/specializations/deep-learning?utm_source=rest_api - Course Name: Machine Learning, URL: https://www.coursera.org/specializations/machine-learning-introduction?utm_source=rest_api - Course Name: IBM AI Engineering, URL: https://www.coursera.org/professional-certificates/ai-engineer?utm_source=rest_api - - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(false)] - public Task RunDictionaryWithBasicTypesSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("Basic Types using Local Dictionary Plugin"); - return RunSampleAsync("Get a random word and its definition.", null, null, shouldPrintPrompt, true, StringParamsDictionaryPlugin.PluginName); - /* - Original plan: - {{!-- Step 1: Get a random word --}} - {{set "randomWord" (DictionaryPlugin-GetRandomWord)}} - - {{!-- Step 2: Get the definition of the random word --}} - {{set "definition" (DictionaryPlugin-GetDefinition word=(get "randomWord"))}} - - {{!-- Step 3: Output the random word and its definition --}} - {{json (array (get "randomWord") (get "definition"))}} - - Result: - ["book","a set of printed or written pages bound together along one edge"] - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(true)] - public Task RunLocalDictionaryWithComplexTypesSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("Complex Types using Local Dictionary Plugin"); - return RunSampleAsync("Teach me two random words and their definition.", null, null, shouldPrintPrompt, true, ComplexParamsDictionaryPlugin.PluginName); - /* - Original Plan: - {{!-- Step 1: Get two random dictionary entries --}} - {{set "entry1" (DictionaryPlugin-GetRandomEntry)}} - {{set "entry2" (DictionaryPlugin-GetRandomEntry)}} - - {{!-- Step 2: Extract words from the entries --}} - {{set "word1" (DictionaryPlugin-GetWord entry=(get "entry1"))}} - {{set "word2" (DictionaryPlugin-GetWord entry=(get "entry2"))}} - - {{!-- Step 3: Extract definitions for the words --}} - {{set "definition1" (DictionaryPlugin-GetDefinition word=(get "word1"))}} - {{set "definition2" (DictionaryPlugin-GetDefinition word=(get "word2"))}} - - {{!-- Step 4: Display the words and their definitions --}} - Word 1: {{json (get "word1")}} - Definition: {{json (get "definition1")}} - - Word 2: {{json (get "word2")}} - Definition: {{json (get "definition2")}} - - Result: - Word 1: apple - Definition 1: a round fruit with red, green, or yellow skin and a white flesh - - Word 2: dog - Definition 2: a domesticated animal with four legs, a tail, and a keen sense of smell that is often used for hunting or companionship - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(false)] - public Task RunPoetrySampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("Multiple Plugins"); - return RunSampleAsync("Write a poem about John Doe, then translate it into Italian.", null, null, shouldPrintPrompt, true, "SummarizePlugin", "WriterPlugin"); - /* - Original plan: - {{!-- Step 1: Initialize the scenario for the poem --}} - {{set "scenario" "John Doe, a mysterious and kind-hearted person"}} - - {{!-- Step 2: Generate a short poem about John Doe --}} - {{set "poem" (WriterPlugin-ShortPoem input=(get "scenario"))}} - - {{!-- Step 3: Translate the poem into Italian --}} - {{set "translatedPoem" (WriterPlugin-Translate input=(get "poem") language="Italian")}} - - {{!-- Step 4: Output the translated poem --}} - {{json (get "translatedPoem")}} - - Result: - C'era una volta un uomo di nome John Doe, - La cui gentilezza si mostrava costantemente, - Aiutava con un sorriso, - E non si arrendeva mai, - Al mistero che lo faceva brillare. - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(false)] - public Task RunBookSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("Loops and Conditionals"); - return RunSampleAsync("Create a book with 3 chapters about a group of kids in a club called 'The Thinking Caps.'", null, null, shouldPrintPrompt, true, "WriterPlugin", "MiscPlugin"); - /* - Original plan: - {{!-- Step 1: Initialize the book title and chapter count --}} - {{set "bookTitle" "The Thinking Caps"}} - {{set "chapterCount" 3}} - - {{!-- Step 2: Generate the novel outline with the given chapter count --}} - {{set "novelOutline" (WriterPlugin-NovelOutline input=(get "bookTitle") chapterCount=(get "chapterCount"))}} - - {{!-- Step 3: Loop through the chapters and generate the content for each chapter --}} - {{#each (range 1 (get "chapterCount"))}} - {{set "chapterIndex" this}} - {{set "chapterSynopsis" (MiscPlugin-ElementAtIndex input=(get "novelOutline") index=(get "chapterIndex"))}} - {{set "previousChapterSynopsis" (MiscPlugin-ElementAtIndex input=(get "novelOutline") index=(get "chapterIndex" - 1))}} - - {{!-- Step 4: Write the chapter content using the WriterPlugin-NovelChapter helper --}} - {{set "chapterContent" (WriterPlugin-NovelChapter input=(get "chapterSynopsis") theme=(get "bookTitle") previousChapter=(get "previousChapterSynopsis") chapterIndex=(get "chapterIndex"))}} - - {{!-- Step 5: Output the chapter content --}} - {{json (get "chapterContent")}} - {{/each}} - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(true)] - public Task RunPredefinedVariablesSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("CreatePlan Prompt With Predefined Variables"); - - // When using predefined variables, you must pass these arguments to both the CreatePlanAsync and InvokeAsync methods. - var initialArguments = new KernelArguments() - { - { "greetings", new List(){ "hey", "bye" } }, - { "someNumber", 1 }, - { "person", new Dictionary() - { - {"name", "John Doe" }, - { "language", "Italian" }, - } } - }; - - return RunSampleAsync("Write a poem about the given person, then translate it into French.", null, initialArguments, shouldPrintPrompt, true, "WriterPlugin", "MiscPlugin"); - /* - Original plan: - {{!-- Step 0: Extract key values --}} - {{set "personName" @root.person.name}} - - {{!-- Step 1: Generate a short poem about the person --}} - {{set "poem" (WriterPlugin-ShortPoem input=personName)}} - - {{!-- Step 2: Translate the poem into French --}} - {{set "translatedPoem" (WriterPlugin-Translate input=poem language="French")}} - - {{!-- Step 3: Output the translated poem --}} - {{json translatedPoem}} - - Result: - Il était une fois un gars nommé Doe, - Dont la vie était un spectacle comique, - Il trébuchait et tombait, - Mais riait à travers tout cela, - Alors qu'il dansait dans la vie, de-ci de-là. - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(true)] - public Task RunPromptWithAdditionalContextSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("Prompt With Additional Context"); - - // Pulling the raw content from SK's README file as domain context. - static async Task getDomainContext() - { - // For demonstration purposes only, beware of token count. - var repositoryUrl = "https://github.com/microsoft/semantic-kernel"; - var readmeUrl = $"{repositoryUrl}/main/README.md".Replace("github.com", "raw.githubusercontent.com", StringComparison.CurrentCultureIgnoreCase); - try - { - var httpClient = new HttpClient(); - // Send a GET request to the specified URL - var response = await httpClient.GetAsync(new Uri(readmeUrl)); - response.EnsureSuccessStatusCode(); // Throw an exception if not successful - - // Read the response content as a string - var content = await response.Content.ReadAsStringAsync(); - httpClient.Dispose(); - return "Content imported from the README of https://github.com/microsoft/semantic-kernel:\n" + content; - } - catch (HttpRequestException e) - { - System.Console.WriteLine("\nException Caught!"); - System.Console.WriteLine("Message :{0} ", e.Message); - return ""; - } - } - - var goal = "Help me onboard to the Semantic Kernel SDK by creating a quick guide that includes a brief overview of the SDK for C# developers and detailed set-up steps. Include relevant links where possible. Then, draft an email with this guide, so I can share it with my team."; - var plannerOptions = new HandlebarsPlannerOptions() - { - // Context to be used in the prompt template. - GetAdditionalPromptContext = getDomainContext, - }; - - return RunSampleAsync(goal, plannerOptions, null, shouldPrintPrompt, true, "WriterPlugin"); - /* - {{!-- Step 0: Extract Key Values --}} - {{set "sdkLink" "https://learn.microsoft.com/en-us/semantic-kernel/overview/"}} - {{set "nugetPackageLink" "https://www.nuget.org/packages/Microsoft.SemanticKernel/"}} - {{set "csharpGetStartedLink" "dotnet/README.md"}} - {{set "emailSubject" "Semantic Kernel SDK: Quick Guide for C# Developers"}} - - {{!-- Step 1: Create a concise guide and store it in a variable --}} - {{set "guide" (concat "The Semantic Kernel SDK provides seamless integration between large language models (LLMs) and programming languages such as C#. " "To get started with the C# SDK, please follow these steps:\n\n" "1. Read the SDK Overview for a brief introduction here: " sdkLink "\n" "2. Install the Nuget package in your project: " nugetPackageLink "\n" "3. Follow the detailed set-up steps in the C# 'Getting Started' guide: " csharpGetStartedLink "\n\n" "Feel free to share this quick guide with your team members to help them onboard quickly with the Semantic Kernel SDK. ")}} - - {{!-- Step 2: Generate a draft email with the guide --}} - {{set "emailBody" (concat "Hi Team,\n\n" "I have put together a quick guide to help you onboard to the Semantic Kernel SDK for C# developers. " "This guide includes a brief overview and detailed set-up steps:\n\n" guide "\n\n" "I have attached a more comprehensive guide as a document. Please review it and let me know if you have any questions. " "Let's start integrating the Semantic Kernel SDK into our projects!\n\n" "Best Regards,\n" "Your Name ")}} - - {{json (concat "Subject: " emailSubject "\n\nBody:\n" emailBody)}} - - Result: - Subject: Semantic Kernel SDK: Quick Guide for C# Developers - - Body: - Hi Team, - I have put together a quick guide to help you onboard to the Semantic Kernel SDK for C# developers. This guide includes a brief overview and detailed set-up steps: - - The Semantic Kernel SDK provides seamless integration between large language models (LLMs) and programming languages such as C#. To get started with the C# SDK, please follow these steps: - 1. Read the SDK Overview for a brief introduction here: https://learn.microsoft.com/en-us/semantic-kernel/overview/ - 2. Install the Nuget package in your project: https://www.nuget.org/packages/Microsoft.SemanticKernel/ - 3. Follow the detailed set-up steps in the C# 'Getting Started' guide: dotnet/README.md - - Feel free to share this quick guide with your team members to help them onboard quickly with the Semantic Kernel SDK. - - I have attached a more comprehensive guide as a document. Please review it and let me know if you have any questions. Let's start integrating the Semantic Kernel SDK into our projects! - - Best Regards, - Your Name - */ - } - - [RetryTheory(typeof(HttpOperationException))] - [InlineData(true)] - public Task RunOverrideCreatePlanPromptSampleAsync(bool shouldPrintPrompt) - { - WriteSampleHeading("CreatePlan Prompt Override"); - - static string OverridePlanPrompt() - { - // Load a custom CreatePlan prompt template from an embedded resource. - var ResourceFileName = "65-prompt-override.handlebars"; - var fileContent = EmbeddedResource.ReadStream(ResourceFileName); - return new StreamReader(fileContent!).ReadToEnd(); - } - - var plannerOptions = new HandlebarsPlannerOptions() - { - // Callback to override the default prompt template. - CreatePlanPromptHandler = OverridePlanPrompt, - }; - - var goal = "I just watched the movie 'Inception' and I loved it! I want to leave a 5 star review. Can you help me?"; - - // Note that since the custom prompt inputs a unique Helpers section with helpers not actually registered with the kernel, - // any plan created using this prompt will fail execution; thus, we will skip the InvokePlan call in this example. - // For a simpler example, see `ItOverridesPromptAsync` in the dotnet\src\Planners\Planners.Handlebars.UnitTests\Handlebars\HandlebarsPlannerTests.cs file. - return RunSampleAsync(goal, plannerOptions, null, shouldPrintPrompt, shouldInvokePlan: false, "WriterPlugin"); - } -} diff --git a/dotnet/samples/Concepts/Plugins/GroundednessChecks.cs b/dotnet/samples/Concepts/Plugins/GroundednessChecks.cs index 384fe63c34ce..c2aeb0d6a68f 100644 --- a/dotnet/samples/Concepts/Plugins/GroundednessChecks.cs +++ b/dotnet/samples/Concepts/Plugins/GroundednessChecks.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Planning.Handlebars; -using Microsoft.SemanticKernel.Plugins.Core; using xRetry; namespace Plugins; @@ -67,59 +64,6 @@ her a beggar. My father came to her aid and two years later they married. Console.WriteLine(excisionResult.GetValue()); } - [Fact] - public async Task PlanningWithGroundednessAsync() - { - var targetTopic = "people and places"; - var samples = "John, Jane, mother, brother, Paris, Rome"; - var ask = @$"Make a summary of the following text. Then make a list of entities -related to {targetTopic} (such as {samples}) which are present in the summary. -Take this list of entities, and from it make another list of those which are not -grounded in the original input text. Finally, rewrite your summary to remove the entities -which are not grounded in the original."; - - Console.WriteLine("\n======== Planning - Groundedness Checks ========"); - - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion( - deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, - endpoint: TestConfiguration.AzureOpenAI.Endpoint, - apiKey: TestConfiguration.AzureOpenAI.ApiKey, - modelId: TestConfiguration.AzureOpenAI.ChatModelId) - .Build(); - - string folder = RepoFiles.SamplePluginsPath(); - kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin")); - kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "GroundingPlugin")); - - kernel.ImportPluginFromType(); - - var planner = new HandlebarsPlanner( - new HandlebarsPlannerOptions() - { - // When using OpenAI models, we recommend using low values for temperature and top_p to minimize planner hallucinations. - ExecutionSettings = new OpenAIPromptExecutionSettings() - { - Temperature = 0.0, - TopP = 0.1, - } - }); - - var initialArguments = new KernelArguments() - { - { "groundingText", GroundingText} - }; - var plan = await planner.CreatePlanAsync(kernel, ask, initialArguments); - - Console.WriteLine($"======== Goal: ========\n{ask}"); - Console.WriteLine($"======== Plan ========\n{plan}"); - - var result = await plan.InvokeAsync(kernel, initialArguments); - - Console.WriteLine("======== Result ========"); - Console.WriteLine(result); - } - private const string GroundingText = """ "I am by birth a Genevese, and my family is one of the most distinguished of that republic. My ancestors had been for many years counsellors and syndics, and my father had filled several public situations @@ -183,32 +127,4 @@ after this event Caroline became his wife." finding him in a mean street. Beaufort had saved a small sum of money, but it was not enough to support him and his daughter. The daughter procured work to eek out a living, but after ten months her father died, leaving her a beggar. My father came to her aid and two years later they married. - -======== Planning - Groundedness Checks ======== -======== Goal: ======== -Make a summary of the following text. Then make a list of entities -related to people and places (such as John, Jane, mother, brother, Paris, Rome) which are present in the summary. -Take this list of entities, and from it make another list of those which are not -grounded in the original input text. Finally, rewrite your summary to remove the entities -which are not grounded in the original. -======== Plan ======== -{{!-- Step 0: Extract key values --}} -{{set "inputText" @root.groundingText}} - -{{!-- Step 1: Summarize the input text --}} -{{set "summary" (SummarizePlugin-Summarize input=inputText)}} - -{{!-- Step 2: Extract entities related to people and places from the summary --}} -{{set "extractedEntities" (GroundingPlugin-ExtractEntities input=summary topic="people and places" example_entities="John, Jane, mother, brother, Paris, Rome")}} - -{{!-- Step 3: Check if extracted entities are grounded in the original input text --}} -{{set "notGroundedEntities" (GroundingPlugin-ReferenceCheckEntities input=extractedEntities reference_context=inputText)}} - -{{!-- Step 4: Remove the not grounded entities from the summary --}} -{{set "finalSummary" (GroundingPlugin-ExciseEntities input=summary ungrounded_entities=notGroundedEntities)}} - -{{!-- Step 5: Output the final summary --}} -{{json finalSummary}} -======== Result ======== -Born in Geneva to a distinguished family, the narrator's father held various honorable public positions. He married late in life after helping his impoverished friend Beaufort and his daughter Caroline. Beaufort, once wealthy, fell into poverty and moved to another location, where the narrator's father found him after ten months. Beaufort eventually fell ill and died, leaving his daughter Caroline an orphan. The narrator's father took her in, and two years later, they married. */ diff --git a/dotnet/samples/Concepts/RAG/WithFunctionCallingStepwisePlanner.cs b/dotnet/samples/Concepts/RAG/WithFunctionCallingStepwisePlanner.cs deleted file mode 100644 index 1f0d0c3bce2a..000000000000 --- a/dotnet/samples/Concepts/RAG/WithFunctionCallingStepwisePlanner.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning; - -namespace RAG; - -public class WithFunctionCallingStepwisePlanner(ITestOutputHelper output) : BaseTest(output) -{ - [Fact] - public async Task RunAsync() - { - string[] questions = - [ - "When should I use the name Bob?", - "When should I use the name Tom?", - "When should I use the name Alice?", - "When should I use the name Harry?", - ]; - - var kernel = InitializeKernel(); - - var options = new FunctionCallingStepwisePlannerOptions - { - MaxIterations = 15, - MaxTokens = 4000, - }; - var planner = new Microsoft.SemanticKernel.Planning.FunctionCallingStepwisePlanner(options); - - foreach (var question in questions) - { - FunctionCallingStepwisePlannerResult result = await planner.ExecuteAsync(kernel, question); - Console.WriteLine($"Q: {question}\nA: {result.FinalAnswer}"); - - // You can uncomment the line below to see the planner's process for completing the request. - // Console.WriteLine($"Chat history:\n{System.Text.Json.JsonSerializer.Serialize(result.ChatHistory)}"); - } - } - - /// - /// Initialize the kernel and load plugins. - /// - /// A kernel instance - private static Kernel InitializeKernel() - { - Kernel kernel = Kernel.CreateBuilder() - .AddOpenAIChatCompletion( - apiKey: TestConfiguration.OpenAI.ApiKey, - modelId: "gpt-3.5-turbo-1106") - .Build(); - - kernel.ImportPluginFromType(); - - return kernel; - } - - internal sealed class RetrievePlugin - { - [KernelFunction, Description("Given a query retrieve relevant information")] - public string Retrieve( - [Description("The input query.")] string query, - Kernel kernel) - { - if (query.Contains("Bob", System.StringComparison.OrdinalIgnoreCase) || - query.Contains("Alice", System.StringComparison.OrdinalIgnoreCase)) - { - return "Alice and Bob are fictional characters commonly used as placeholders in discussions about cryptographic systems and protocols,[1] and in other science and engineering literature where there are several participants in a thought experiment."; - } - if (query.Contains("Tom", System.StringComparison.OrdinalIgnoreCase) || - query.Contains("Dick", System.StringComparison.OrdinalIgnoreCase) || - query.Contains("Harry", System.StringComparison.OrdinalIgnoreCase)) - { - return "The phrase \"Tom, Dick, and Harry\" is a placeholder for unspecified people.[1][2] The phrase most commonly occurs as \"every Tom, Dick, and Harry\", meaning everyone, and \"any Tom, Dick, or Harry\", meaning anyone."; - } - - return string.Empty; - } - } -} diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs index 096ce4795fb3..471bad0ccc98 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs @@ -16,7 +16,7 @@ namespace StepwisePlannerMigration.Controllers; /// -/// This controller shows the old way how to use planning capability by using . +/// This controller shows the old way how to use planning capability by using FunctionCallingStepwisePlanner. /// A new recommended approach is demonstrated in . /// [ApiController] diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj index abd289077625..be2d1b1fc180 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj +++ b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,9 +8,10 @@ - - - + + + + diff --git a/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj b/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj index e76cf615ff91..ee804dbc02dc 100644 --- a/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj +++ b/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj @@ -24,7 +24,6 @@ - diff --git a/dotnet/samples/LearnResources/LearnResources.csproj b/dotnet/samples/LearnResources/LearnResources.csproj index 49fcb12a2e50..2ebd04438328 100644 --- a/dotnet/samples/LearnResources/LearnResources.csproj +++ b/dotnet/samples/LearnResources/LearnResources.csproj @@ -54,8 +54,8 @@ - - + + diff --git a/dotnet/samples/LearnResources/MicrosoftLearn/Planner.cs b/dotnet/samples/LearnResources/MicrosoftLearn/Planner.cs deleted file mode 100644 index 3c6b3f6bcf17..000000000000 --- a/dotnet/samples/LearnResources/MicrosoftLearn/Planner.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Plugins; - -namespace Examples; - -/// -/// This example demonstrates how to create native functions for AI to call as described at -/// https://learn.microsoft.com/semantic-kernel/agents/plugins/using-the-KernelFunction-decorator -/// -public class Planner(ITestOutputHelper output) : LearnBaseTest(output) -{ - [Fact] - public async Task RunAsync() - { - Console.WriteLine("======== Planner ========"); - - string? endpoint = TestConfiguration.AzureOpenAI.Endpoint; - string? modelId = TestConfiguration.AzureOpenAI.ChatModelId; - string? apiKey = TestConfiguration.AzureOpenAI.ApiKey; - - if (endpoint is null || modelId is null || apiKey is null) - { - Console.WriteLine("Azure OpenAI credentials not found. Skipping example."); - - return; - } - - // - var builder = Kernel.CreateBuilder() - .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); - builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(LogLevel.Trace)); - builder.Plugins.AddFromType(); - Kernel kernel = builder.Build(); - - // Get chat completion service - var chatCompletionService = kernel.GetRequiredService(); - - // Create chat history - ChatHistory history = []; - - // Start the conversation - Console.Write("User > "); - string? userInput; - while ((userInput = Console.ReadLine()) is not null) - { - // Get user input - Console.Write("User > "); - history.AddUserMessage(userInput!); - - // Enable auto function calling - OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() - { - FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() - }; - - // Get the response from the AI - var result = chatCompletionService.GetStreamingChatMessageContentsAsync( - history, - executionSettings: openAIPromptExecutionSettings, - kernel: kernel); - - // Stream the results - string fullMessage = ""; - var first = true; - await foreach (var content in result) - { - if (content.Role.HasValue && first) - { - Console.Write("Assistant > "); - first = false; - } - Console.Write(content.Content); - fullMessage += content.Content; - } - Console.WriteLine(); - - // Add the message from the agent to the chat history - history.AddAssistantMessage(fullMessage); - - // Get user input again - Console.Write("User > "); - } - } -} diff --git a/dotnet/samples/LearnResources/Plugins/MathSolver.cs b/dotnet/samples/LearnResources/Plugins/MathSolver.cs deleted file mode 100644 index eb305c3f1928..000000000000 --- a/dotnet/samples/LearnResources/Plugins/MathSolver.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning.Handlebars; - -namespace Plugins; - -public class MathSolver(ILoggerFactory loggerFactory) -{ - private readonly ILogger _logger = loggerFactory.CreateLogger(); - - [KernelFunction] - [Description("Solves a math problem.")] - [return: Description("The solution to the math problem.")] - public async Task SolveAsync( - Kernel kernel, - [Description("The math problem to solve; describe it in 2-3 sentences to ensure full context is provided")] string problem - ) - { - var kernelWithMath = kernel.Clone(); - - // Remove the math solver plugin so that we don't get into an infinite loop - kernelWithMath.Plugins.Remove(kernelWithMath.Plugins["MathSolver"]); - - // Add the math plugin so the LLM can solve the problem - kernelWithMath.Plugins.AddFromType(); - - var planner = new HandlebarsPlanner(new HandlebarsPlannerOptions() { AllowLoops = true }); - - // Create a plan - var plan = await planner.CreatePlanAsync(kernelWithMath, problem); - this._logger.LogInformation("Plan: {Plan}", plan); - - // Execute the plan - var result = (await plan.InvokeAsync(kernelWithMath)).Trim(); - this._logger.LogInformation("Results: {Result}", result); - - return result; - } -} diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index e41b79b146f4..8702d149b4ff 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -8,18 +8,6 @@ $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0080,SKEXP0110,SKEXP0130,OPENAI001,MEVD9000 b7762d10-e29b-4bb1-8b74-b6d69a667dd4 - - - - - - - - - - - - @@ -99,7 +87,6 @@ - @@ -196,10 +183,6 @@ - - - - Always diff --git a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlanTests.cs b/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlanTests.cs deleted file mode 100644 index f775282c69b0..000000000000 --- a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlanTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Globalization; -using System.Threading.Tasks; -using HandlebarsDotNet; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning.Handlebars; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Planners.Handlebars; - -public sealed class HandlebarsPlanTests -{ - public HandlebarsPlanTests() - { - this._kernel = new(); - this._arguments = new() { ["input"] = Guid.NewGuid().ToString("X") }; - } - - private const string PlanTemplate = """ - {{!-- Step 1: Call Bar function --}} - {{set "barResult" (Foo-Bar)}} - - {{!-- Step 2: Call BazAsync function --}} - {{set "bazAsyncResult" (Foo-Baz)}} - - {{!-- Step 3: Call Combine function with two words --}} - {{set "combinedWords" (Foo-Combine x="Hello" y="World")}} - - {{!-- Step 4: Call StringifyInt function with an integer --}} - {{set "stringifiedInt" (Foo-StringifyInt x=42)}} - - {{!-- Step 5: Output the results --}} - {{concat barResult bazAsyncResult combinedWords stringifiedInt}} - """; - - [Fact] - public async Task InvokeValidPlanAsync() - { - // Arrange & Act - var result = await this.InvokePlanAsync(PlanTemplate1); - - // Assert - Assert.Equal("BarBazWorldHello42", result); - } - - [Fact] - public async Task InvokePlanWithHallucinatedFunctionAsync() - { - // Arrange - var planWithInvalidHelper = PlanTemplate1.Replace("Foo-Combine", "Foo-HallucinatedHelper", StringComparison.CurrentCulture); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await this.InvokePlanAsync(planWithInvalidHelper)); - Assert.IsType(exception.InnerException); - Assert.Contains("Template references a helper that cannot be resolved.", exception.InnerException.Message, StringComparison.CurrentCultureIgnoreCase); - } - - #region private - - private readonly Kernel _kernel; - private readonly KernelArguments _arguments; - - public static string PlanTemplate1 => PlanTemplate; - - private async Task InvokePlanAsync(string planTemplate) - { - // Arrange - this._kernel.ImportPluginFromObject(new Foo()); - var plan = new HandlebarsPlan(planTemplate); - - // Act - return await plan.InvokeAsync(this._kernel, this._arguments); - } - - private sealed class Foo - { - [KernelFunction, Description("Return Bar")] - public string Bar() => "Bar"; - - [KernelFunction, Description("Return Baz")] - public async Task BazAsync() - { - await Task.Delay(1000); - return await Task.FromResult("Baz"); - } - - [KernelFunction, Description("Return words concatenated")] - public string Combine([Description("First word")] string x, [Description("Second word")] string y) => y + x; - - [KernelFunction, Description("Return number as string")] - public string StringifyInt([Description("Number to stringify")] int x) => x.ToString(CultureInfo.InvariantCulture); - } - - #endregion -} diff --git a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs b/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs deleted file mode 100644 index 91496a8311fd..000000000000 --- a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Threading.Tasks; -using Azure.Identity; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Planning.Handlebars; -using SemanticKernel.IntegrationTests.Fakes; -using SemanticKernel.IntegrationTests.TestSettings; -using xRetry; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Planners.Handlebars; - -public sealed class HandlebarsPlannerTests -{ - [Theory(Skip = "This test is for manual verification.")] - [InlineData("Write a joke and send it in an e-mail to Kai.", "SendEmail", "test")] - public async Task CreatePlanFunctionFlowAsync(string goal, string expectedFunction, string expectedPlugin) - { - // Arrange - bool useEmbeddings = false; - var kernel = this.InitializeKernel(useEmbeddings); - kernel.ImportPluginFromType(expectedPlugin); - TestHelpers.ImportSamplePlugins(kernel, "FunPlugin"); - - // Act - var plan = await new HandlebarsPlanner(s_defaultPlannerOptions).CreatePlanAsync(kernel, goal); - - // Assert expected function - Assert.Contains( - $"{expectedPlugin}-{expectedFunction}", - plan.ToString(), - StringComparison.CurrentCulture - ); - } - - [RetryTheory(Skip = "This test is for manual verification.")] - [InlineData("Write a novel about software development that is 3 chapters long.", "NovelChapter", "WriterPlugin")] - public async Task CreatePlanWithDefaultsAsync(string goal, string expectedFunction, string expectedPlugin) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - TestHelpers.ImportSamplePlugins(kernel, "WriterPlugin", "MiscPlugin"); - - // Act - var plan = await new HandlebarsPlanner(s_defaultPlannerOptions).CreatePlanAsync(kernel, goal); - - // Assert - Assert.Contains( - $"{expectedPlugin}-{expectedFunction}", - plan.ToString(), - StringComparison.CurrentCulture - ); - } - - [Theory(Skip = "This test is for manual verification.")] - [InlineData("List each property of the default Qux object.", "## Complex types", """ - ### Qux: - { - "type": "Object", - "properties": { - "Bar": { - "type": "String", - }, - "Baz": { - "type": "Int32", - }, - } - } - """, "GetDefaultQux", "Foo")] - public async Task CreatePlanWithComplexTypesDefinitionsAsync(string goal, string expectedSectionHeader, string expectedTypeHeader, string expectedFunction, string expectedPlugin) - { - // Arrange - bool useEmbeddings = false; - var kernel = this.InitializeKernel(useEmbeddings); - kernel.ImportPluginFromObject(new Foo()); - - // Act - var plan = await new HandlebarsPlanner(s_defaultPlannerOptions).CreatePlanAsync(kernel, goal); - - // Assert expected section header for Complex Types in prompt - Assert.Contains( - expectedSectionHeader, - plan.Prompt, - StringComparison.CurrentCulture - ); - - // Assert expected complex parameter type in prompt - Assert.Contains( - expectedTypeHeader, - plan.Prompt, - StringComparison.CurrentCulture - ); - - // Assert expected function in plan - Assert.Contains( - $"{expectedPlugin}-{expectedFunction}", - plan.ToString(), - StringComparison.CurrentCulture - ); - } - - private Kernel InitializeKernel(bool useEmbeddings = false) - { - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - - IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.Services.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, - modelId: azureOpenAIConfiguration.ChatModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - - if (useEmbeddings) - { - builder.Services.AddAzureOpenAIEmbeddingGenerator( - deploymentName: azureOpenAIEmbeddingsConfiguration.DeploymentName, - modelId: azureOpenAIEmbeddingsConfiguration.EmbeddingModelId, - endpoint: azureOpenAIEmbeddingsConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - - return builder.Build(); - } - - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - private static readonly HandlebarsPlannerOptions s_defaultPlannerOptions = new() - { - ExecutionSettings = new OpenAIPromptExecutionSettings() - { - Temperature = 0.0, - TopP = 0.1, - } - }; - - private sealed class Foo - { - public sealed class Qux(string bar, int baz) - { - public string Bar { get; set; } = bar; - public int Baz { get; set; } = baz; - } - - [KernelFunction, Description("Returns default Qux object.")] - public Qux GetDefaultQux() => new("bar", 42); - } -} diff --git a/dotnet/src/IntegrationTests/Planners/PlanTests.cs b/dotnet/src/IntegrationTests/Planners/PlanTests.cs deleted file mode 100644 index c496b3488a78..000000000000 --- a/dotnet/src/IntegrationTests/Planners/PlanTests.cs +++ /dev/null @@ -1,606 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI; -using Microsoft.SemanticKernel.Events; -using Microsoft.SemanticKernel.Planning; -using SemanticKernel.IntegrationTests.Fakes; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Planning; - -public sealed class PlanTests : IDisposable -{ - public PlanTests(ITestOutputHelper output) - { - this._testOutputHelper = new RedirectOutput(output); - - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - } - - [Theory] - [InlineData("Write a poem or joke and send it in an e-mail to Kai.")] - public void CreatePlan(string prompt) - { - // Arrange - - // Act - var plan = new Plan(prompt); - - // Assert - Assert.Equal(prompt, plan.Description); - Assert.NotEmpty(plan.Name); - Assert.Equal(nameof(Plan), plan.PluginName); - Assert.Empty(plan.Steps); - } - - [Theory] - [InlineData("This is a story about a dog.", "kai@email.com")] - public async Task CanExecuteRunSimpleAsync(string inputToEmail, string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - var expectedBody = $"Sent email to: {expectedEmail}. Body: {inputToEmail}".Trim(); - - var plan = new Plan(emailFunctions["SendEmail"]); - - // Act - var cv = new ContextVariables(); - cv.Update(inputToEmail); - cv.Set("email_address", expectedEmail); - var result = await plan.InvokeAsync(kernel, cv); - - // Assert - Assert.Equal(expectedBody, result.GetValue()); - } - - [Theory] - [InlineData("This is a story about a dog.", "kai@email.com")] - public async Task CanExecuteAsChatAsync(string inputToEmail, string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(false, true); - - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - var expectedBody = $"Sent email to: {expectedEmail}. Body: {inputToEmail}".Trim(); - - var plan = new Plan(emailFunctions["SendEmail"]); - - // Act - var cv = new ContextVariables(); - cv.Update(inputToEmail); - cv.Set("email_address", expectedEmail); - var result = await plan.InvokeAsync(kernel, cv); - - // Assert - Assert.Equal(expectedBody, result.GetValue()); - } - - [Theory] - [InlineData("Send a story to kai.", "This is a story about a dog.", "French", "kai@email.com")] - public async Task CanExecuteRunSimpleStepsAsync(string goal, string inputToTranslate, string language, string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var emailPlugin = kernel.Plugins[nameof(EmailPluginFake)]; - var writerPlugin = kernel.Plugins["WriterPlugin"]; - var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim(); - - var plan = new Plan(goal); - plan.AddSteps(writerPlugin["Translate"], emailPlugin["SendEmail"]); - - // Act - var cv = new ContextVariables(); - cv.Update(inputToTranslate); - cv.Set("email_address", expectedEmail); - cv.Set("language", language); - var result = (await plan.InvokeAsync(kernel, cv)).GetValue(); - - // Assert - Assert.NotNull(result); - Assert.Contains(expectedBody, result, StringComparison.OrdinalIgnoreCase); - Assert.True(expectedBody.Length < result.Length); - } - - [Fact] - public async Task CanExecutePlanWithTreeStepsAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var goal = "Write a poem or joke and send it in an e-mail to Kai."; - var plan = new Plan(goal); - var subPlan = new Plan("Write a poem or joke"); - - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - // Arrange - subPlan.AddSteps(emailFunctions["WritePoem"], emailFunctions["WritePoem"], emailFunctions["WritePoem"]); - plan.AddSteps(subPlan, new Plan(emailFunctions["SendEmail"])); - plan.State.Set("email_address", "something@email.com"); - - // Act - var result = await plan.InvokeAsync(kernel, "PlanInput"); - - // Assert - Assert.NotNull(result); - Assert.Equal( - "Sent email to: something@email.com. Body: Roses are red, violets are blue, Roses are red, violets are blue, Roses are red, violets are blue, PlanInput is hard, so is this test. is hard, so is this test. is hard, so is this test.", - result.GetValue()); - } - - [Fact] - public async Task ConPlanStepsTriggerKernelEventsAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var goal = "Write a poem or joke and send it in an e-mail to Kai."; - var plan = new Plan(goal); - var subPlan = new Plan("Write a poem or joke"); - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - var expectedInvocations = 4; - // 1 - Outer Plan - Write poem and send email goal - // 2 - Inner Plan - Write poem or joke goal - // 3 - Inner Plan - Step 1 - WritePoem - // 4 - Inner Plan - Step 2 - WritePoem - // 5 - Inner Plan - Step 3 - WritePoem - // 6 - Outer Plan - Step 1 - SendEmail - - subPlan.AddSteps(emailFunctions["WritePoem"], emailFunctions["WritePoem"], emailFunctions["WritePoem"]); - plan.AddSteps(subPlan, new Plan(emailFunctions["SendEmail"])); - plan.State.Set("email_address", "something@email.com"); - - var invokingCalls = 0; - var invokedCalls = 0; - var invokingListFunctions = new List(); - var invokedListFunctions = new List(); - void FunctionInvoking(object? sender, FunctionInvokingEventArgs e) - { - invokingListFunctions.Add(e.Function.Metadata); - invokingCalls++; - } - - void FunctionInvoked(object? sender, FunctionInvokedEventArgs e) - { - invokedListFunctions.Add(e.Function.Metadata); - invokedCalls++; - } - - kernel.FunctionInvoking += FunctionInvoking; - kernel.FunctionInvoked += FunctionInvoked; - - // Act - var result = await plan.InvokeAsync(kernel, "PlanInput"); - - // Assert - Assert.NotNull(result); - Assert.Equal(expectedInvocations, invokingCalls); - Assert.Equal(expectedInvocations, invokedCalls); - - // Expected invoking sequence - Assert.Equal(invokingListFunctions[0].Name, emailFunctions["WritePoem"].Name); - Assert.Equal(invokingListFunctions[1].Name, emailFunctions["WritePoem"].Name); - Assert.Equal(invokingListFunctions[2].Name, emailFunctions["WritePoem"].Name); - Assert.Equal(invokingListFunctions[3].Name, emailFunctions["SendEmail"].Name); - - // Expected invoked sequence - Assert.Equal(invokedListFunctions[0].Name, emailFunctions["WritePoem"].Name); - Assert.Equal(invokedListFunctions[1].Name, emailFunctions["WritePoem"].Name); - Assert.Equal(invokedListFunctions[2].Name, emailFunctions["WritePoem"].Name); - Assert.Equal(invokedListFunctions[3].Name, emailFunctions["SendEmail"].Name); - } - - [Theory] - [InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")] - [InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "some_email@email.com")] - public async Task CanExecuteRunPlanSimpleManualStateAsync(string input, string goal, string email) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - // Create the input mapping from parent (plan) plan state to child plan (sendEmailPlan) state. - var cv = new ContextVariables(); - cv.Set("email_address", "$TheEmailFromState"); - var sendEmailPlan = new Plan(emailFunctions["SendEmail"]) - { - Parameters = cv, - }; - - var plan = new Plan(goal); - plan.AddSteps(sendEmailPlan); - plan.State.Set("TheEmailFromState", email); // manually prepare the state - - // Act - var result = await kernel.StepAsync(input, plan); - - // Assert - var expectedBody = string.IsNullOrEmpty(input) ? goal : input; - Assert.Single(result.Steps); - Assert.Equal(1, result.NextStepIndex); - Assert.False(result.HasNextStep); - Assert.Equal(goal, plan.Description); - Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString()); - } - - [Theory] - [InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")] - [InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "some_email@email.com")] - public async Task CanExecuteRunPlanSimpleManualStateNoVariableAsync(string input, string goal, string email) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - // Create the input mapping from parent (plan) plan state to child plan (sendEmailPlan) state. - var cv = new ContextVariables(); - cv.Set("email_address", string.Empty); - var sendEmailPlan = new Plan(emailFunctions["SendEmail"]) - { - Parameters = cv, - }; - - var plan = new Plan(goal); - plan.AddSteps(sendEmailPlan); - plan.State.Set("email_address", email); // manually prepare the state - - // Act - var result = await kernel.StepAsync(input, plan); - - // Assert - var expectedBody = string.IsNullOrEmpty(input) ? goal : input; - Assert.Single(result.Steps); - Assert.Equal(1, result.NextStepIndex); - Assert.False(result.HasNextStep); - Assert.Equal(goal, plan.Description); - Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString()); - } - - [Theory] - [InlineData("", "Write a poem or joke and send it in an e-mail to Kai.", "")] - [InlineData("Hello World!", "Write a poem or joke and send it in an e-mail to Kai.", "some_email@email.com")] - public async Task CanExecuteRunPlanManualStateAsync(string input, string goal, string email) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - // Create the input mapping from parent (plan) plan state to child plan (sendEmailPlan) state. - var cv = new ContextVariables(); - cv.Set("email_address", "$TheEmailFromState"); - var sendEmailPlan = new Plan(emailFunctions["SendEmail"]) - { - Parameters = cv - }; - - var plan = new Plan(goal); - plan.AddSteps(sendEmailPlan); - plan.State.Set("TheEmailFromState", email); // manually prepare the state - - // Act - var result = await kernel.StepAsync(input, plan); - - // Assert - var expectedBody = string.IsNullOrEmpty(input) ? goal : input; - Assert.False(plan.HasNextStep); - Assert.Equal(goal, plan.Description); - Assert.Equal($"Sent email to: {email}. Body: {expectedBody}".Trim(), plan.State.ToString()); - } - - [Theory] - [InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "Kai", "Kai@example.com")] - public async Task CanExecuteRunPlanAsync(string goal, string inputToSummarize, string inputLanguage, string inputName, string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var summarizePlugin = kernel.Plugins["SummarizePlugin"]; - var writerPlugin = kernel.Plugins["WriterPlugin"]; - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim(); - - var summarizePlan = new Plan(summarizePlugin["Summarize"]); - - var cv = new ContextVariables(); - cv.Set("language", inputLanguage); - var outputs = new List - { - "TRANSLATED_SUMMARY" - }; - var translatePlan = new Plan(writerPlugin["Translate"]) - { - Parameters = cv, - Outputs = outputs, - }; - - cv = new ContextVariables(); - cv.Update(inputName); - outputs = new List - { - "TheEmailFromState" - }; - var getEmailPlan = new Plan(emailFunctions["GetEmailAddress"]) - { - Parameters = cv, - Outputs = outputs, - }; - - cv = new ContextVariables(); - cv.Set("email_address", "$TheEmailFromState"); - cv.Set("input", "$TRANSLATED_SUMMARY"); - var sendEmailPlan = new Plan(emailFunctions["SendEmail"]) - { - Parameters = cv - }; - - var plan = new Plan(goal); - plan.AddSteps(summarizePlan, translatePlan, getEmailPlan, sendEmailPlan); - - // Act - var result = await kernel.StepAsync(inputToSummarize, plan); - Assert.Equal(4, result.Steps.Count); - Assert.Equal(1, result.NextStepIndex); - Assert.True(result.HasNextStep); - result = await kernel.StepAsync(result); - Assert.Equal(4, result.Steps.Count); - Assert.Equal(2, result.NextStepIndex); - Assert.True(result.HasNextStep); - result = await kernel.StepAsync(result); - Assert.Equal(4, result.Steps.Count); - Assert.Equal(3, result.NextStepIndex); - Assert.True(result.HasNextStep); - result = await kernel.StepAsync(result); - - // Assert - Assert.Equal(4, result.Steps.Count); - Assert.Equal(4, result.NextStepIndex); - Assert.False(result.HasNextStep); - Assert.Equal(goal, plan.Description); - Assert.Contains(expectedBody, plan.State.ToString(), StringComparison.OrdinalIgnoreCase); - Assert.True(expectedBody.Length < plan.State.ToString().Length); - } - - [Theory] - [InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "Kai", "Kai@example.com")] - public async Task CanExecuteRunSequentialAsync(string goal, string inputToSummarize, string inputLanguage, string inputName, string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var summarizePlugin = kernel.Plugins["SummarizePlugin"]; - var writerPlugin = kernel.Plugins["WriterPlugin"]; - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim(); - - var summarizePlan = new Plan(summarizePlugin["Summarize"]); - - var cv = new ContextVariables(); - cv.Set("language", inputLanguage); - var outputs = new List - { - "TRANSLATED_SUMMARY" - }; - - var translatePlan = new Plan(writerPlugin["Translate"]) - { - Parameters = cv, - Outputs = outputs, - }; - - cv = new ContextVariables(); - cv.Update(inputName); - outputs = new List - { - "TheEmailFromState" - }; - var getEmailPlan = new Plan(emailFunctions["GetEmailAddress"]) - { - Parameters = cv, - Outputs = outputs, - }; - - cv = new ContextVariables(); - cv.Set("email_address", "$TheEmailFromState"); - cv.Set("input", "$TRANSLATED_SUMMARY"); - var sendEmailPlan = new Plan(emailFunctions["SendEmail"]) - { - Parameters = cv - }; - - var plan = new Plan(goal); - plan.AddSteps(summarizePlan, translatePlan, getEmailPlan, sendEmailPlan); - - // Act - var result = (await plan.InvokeAsync(kernel, inputToSummarize)).GetValue(); - - // Assert - Assert.NotNull(result); - Assert.Contains(expectedBody, result, StringComparison.OrdinalIgnoreCase); - Assert.True(expectedBody.Length < result.Length); - } - - [Theory] - [InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "Kai", "Kai@example.com")] - public async Task CanExecuteRunSequentialOnDeserializedPlanAsync(string goal, string inputToSummarize, string inputLanguage, string inputName, - string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var summarizePlugins = kernel.Plugins["SummarizePlugin"]; - var writerPlugin = kernel.Plugins["WriterPlugin"]; - var emailFunction = kernel.Plugins[nameof(EmailPluginFake)]; - - var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim(); - - var summarizePlan = new Plan(summarizePlugins["Summarize"]); - - var cv = new ContextVariables(); - cv.Set("language", inputLanguage); - var outputs = new List - { - "TRANSLATED_SUMMARY" - }; - - var translatePlan = new Plan(writerPlugin["Translate"]) - { - Parameters = cv, - Outputs = outputs, - }; - - cv = new ContextVariables(); - cv.Update(inputName); - outputs = new List - { - "TheEmailFromState" - }; - var getEmailPlan = new Plan(emailFunction["GetEmailAddress"]) - { - Parameters = cv, - Outputs = outputs, - }; - - cv = new ContextVariables(); - cv.Set("email_address", "$TheEmailFromState"); - cv.Set("input", "$TRANSLATED_SUMMARY"); - var sendEmailPlan = new Plan(emailFunction["SendEmail"]) - { - Parameters = cv - }; - - var plan = new Plan(goal); - plan.AddSteps(summarizePlan, translatePlan, getEmailPlan, sendEmailPlan); - - // Act - var serializedPlan = plan.ToJson(); - var deserializedPlan = Plan.FromJson(serializedPlan, kernel.Plugins); - var result = (await deserializedPlan.InvokeAsync(kernel, inputToSummarize)).GetValue(); - - // Assert - Assert.NotNull(result); - Assert.Contains(expectedBody, result, StringComparison.OrdinalIgnoreCase); - Assert.True(expectedBody.Length < result.Length); - } - - [Theory] - [InlineData("Summarize an input, translate to french, and e-mail to Kai", "This is a story about a dog.", "French", "kai@email.com")] - public async Task CanExecuteRunSequentialFunctionsAsync(string goal, string inputToSummarize, string inputLanguage, string expectedEmail) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var summarizePlugin = kernel.Plugins["SummarizePlugin"]; - var writerPlugin = kernel.Plugins["WriterPlugin"]; - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - var expectedBody = $"Sent email to: {expectedEmail}. Body:".Trim(); - - var summarizePlan = new Plan(summarizePlugin["Summarize"]); - var translatePlan = new Plan(writerPlugin["Translate"]); - var sendEmailPlan = new Plan(emailFunctions["SendEmail"]); - - var plan = new Plan(goal); - plan.AddSteps(summarizePlan, translatePlan, sendEmailPlan); - - // Act - var cv = new ContextVariables(); - cv.Update(inputToSummarize); - cv.Set("email_address", expectedEmail); - cv.Set("language", inputLanguage); - var result = await plan.InvokeAsync(kernel, cv); - - // Assert - Assert.Contains(expectedBody, result.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Theory] - [InlineData("computers")] - public async Task CanRunPlanAsync(string input) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var emailFunctions = kernel.Plugins[nameof(EmailPluginFake)]; - - var plan = new Plan("Write a poem about a topic and send in an email."); - - var writePoem = new Plan(emailFunctions["WritePoem"]); - // fileStep.Parameters["input"] = "$INPUT"; - writePoem.Outputs.Add("POEM"); - - var sendEmail = new Plan(emailFunctions["SendEmail"]); - sendEmail.Parameters["input"] = "$POEM"; - sendEmail.Outputs.Add("EMAIL_RESULT"); - - plan.AddSteps(writePoem, sendEmail); - plan.Outputs.Add("EMAIL_RESULT"); - - //Act - var result = await plan.InvokeAsync(kernel, input); - - // Assert - Assert.NotNull(result); - Assert.Equal($"Sent email to: default@email.com. Body: Roses are red, violets are blue, {input} is hard, so is this test.", result.GetValue()); - } - - private Kernel InitializeKernel(bool useEmbeddings = false, bool useChatModel = false) - { - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - - IKernelBuilder builder = Kernel.CreateBuilder(); - - if (useChatModel) - { - c.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - else - { - c.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - - if (useEmbeddings) - { - c.AddAzureOpenAITextEmbeddingGeneration( - deploymentName: azureOpenAIEmbeddingsConfiguration.DeploymentName, - endpoint: azureOpenAIEmbeddingsConfiguration.Endpoint, - apiKey: azureOpenAIEmbeddingsConfiguration.ApiKey); - } - - Kernel kernel = builder.Build(); - - // Import all sample plugins available for demonstration purposes. - TestHelpers.ImportAllSamplePlugins(kernel); - - kernel.ImportPluginFromType(); - return kernel; - } - - private readonly RedirectOutput _testOutputHelper; - private readonly IConfigurationRoot _configuration; - - public void Dispose() - { - this._testOutputHelper.Dispose(); - } -} diff --git a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs b/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs deleted file mode 100644 index 679df8afe3c1..000000000000 --- a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlanParserTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning; -using SemanticKernel.IntegrationTests.Fakes; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Planners.Sequential; - -public class SequentialPlanParserTests -{ - public SequentialPlanParserTests(ITestOutputHelper output) - { - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - } - - [Fact] - public void CanCallToPlanFromXml() - { - // Arrange - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - Kernel kernel = Kernel.CreateBuilder() - .WithAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential(), - serviceId: azureOpenAIConfiguration.ServiceId) - .Build(); - kernel.ImportPluginFromType("email"); - TestHelpers.ImportSamplePlugins(kernel, "SummarizePlugin", "WriterPlugin"); - - var planString = - @" - - - - -"; - var goal = "Summarize an input, translate to french, and e-mail to John Doe"; - - // Act - var plan = planString.ToPlanFromXml(goal, kernel.Plugins.GetFunctionCallback()); - - // Assert - Assert.NotNull(plan); - Assert.Equal((string?)"Summarize an input, translate to french, and e-mail to John Doe", (string?)plan.Description); - - Assert.Equal(4, plan.Steps.Count); - Assert.Collection(plan.Steps, - step => - { - Assert.Equal("SummarizePlugin", step.PluginName); - Assert.Equal("Summarize", step.Name); - }, - step => - { - Assert.Equal("WriterPlugin", step.PluginName); - Assert.Equal("Translate", step.Name); - Assert.Equal("French", step.Parameters["language"]); - Assert.True(step.Outputs.Contains("TRANSLATED_SUMMARY")); - }, - step => - { - Assert.Equal("email", step.PluginName); - Assert.Equal("GetEmailAddress", step.Name); - Assert.Equal("John Doe", step.Parameters["input"]); - Assert.True(step.Outputs.Contains("EMAIL_ADDRESS")); - }, - step => - { - Assert.Equal("email", step.PluginName); - Assert.Equal("SendEmail", step.Name); - Assert.Equal("$TRANSLATED_SUMMARY", step.Parameters["input"]); - Assert.Equal("$EMAIL_ADDRESS", step.Parameters["email_address"]); - } - ); - } - - private readonly IConfigurationRoot _configuration; -} diff --git a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs b/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs deleted file mode 100644 index ecddc781a049..000000000000 --- a/dotnet/src/IntegrationTests/Planners/SequentialPlanner/SequentialPlannerTests.cs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AI.Embeddings; -using Microsoft.SemanticKernel.Memory; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Plugins.Memory; -using SemanticKernel.IntegrationTests.Fakes; -using SemanticKernel.IntegrationTests.TestSettings; -using xRetry; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Planners.Sequential; - -public sealed class SequentialPlannerTests : IDisposable -{ - public SequentialPlannerTests(ITestOutputHelper output) - { - this._testOutputHelper = new RedirectOutput(output); - - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - } - - [Theory] - [InlineData(false, "Write a joke and send it in an e-mail to Kai.", "SendEmail", "EmailPluginFake")] - [InlineData(true, "Write a joke and send it in an e-mail to Kai.", "SendEmail", "EmailPluginFake")] - public async Task CreatePlanFunctionFlowAsync(bool useChatModel, string prompt, string expectedFunction, string expectedPlugin) - { - // Arrange - bool useEmbeddings = false; - Kernel kernel = this.InitializeKernel(useEmbeddings, useChatModel); - kernel.ImportPluginFromType(); - TestHelpers.ImportSamplePlugins(kernel, "FunPlugin"); - - var planner = new SequentialPlanner(kernel); - - // Act - var plan = await planner.CreatePlanAsync(prompt); - - // Assert - Assert.Contains( - plan.Steps, - step => - step.Name.Equals(expectedFunction, StringComparison.OrdinalIgnoreCase) && - step.PluginName.Equals(expectedPlugin, StringComparison.OrdinalIgnoreCase)); - } - - [RetryTheory] - [InlineData("Write a novel about software development that is 3 chapters long.", "NovelOutline", "WriterPlugin", "")] - public async Task CreatePlanWithDefaultsAsync(string prompt, string expectedFunction, string expectedPlugin, string expectedDefault) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - TestHelpers.ImportSamplePlugins(kernel, "WriterPlugin", "MiscPlugin"); - - var planner = new SequentialPlanner(kernel); - - // Act - var plan = await planner.CreatePlanAsync(prompt); - - // Assert - Assert.Contains( - plan.Steps, - step => - step.Name.Equals(expectedFunction, StringComparison.OrdinalIgnoreCase) && - step.PluginName.Equals(expectedPlugin, StringComparison.OrdinalIgnoreCase) && - step.Parameters["endMarker"].Equals(expectedDefault, StringComparison.OrdinalIgnoreCase)); - } - - [RetryTheory] - [InlineData("Write a poem and a joke and send it in an e-mail to Kai.", "SendEmail", "EmailPluginFake")] - public async Task CreatePlanGoalRelevantAsync(string prompt, string expectedFunction, string expectedPlugin) - { - // Arrange - bool useEmbeddings = true; - - Kernel kernel = this.InitializeKernel(useEmbeddings); - ISemanticTextMemory memory = this.InitializeMemory(kernel.GetService()); - - kernel.ImportPluginFromType(); - - // Import all sample plugins available for demonstration purposes. - TestHelpers.ImportAllSamplePlugins(kernel); - - var planner = new SequentialPlanner(kernel, - new() { SemanticMemoryConfig = new() { RelevancyThreshold = 0.65, MaxRelevantFunctions = 30, Memory = memory } }); - - // Act - var plan = await planner.CreatePlanAsync(prompt); - - // Assert - Assert.Contains( - plan.Steps, - step => - step.Name.Equals(expectedFunction, StringComparison.OrdinalIgnoreCase) && - step.PluginName.Equals(expectedPlugin, StringComparison.OrdinalIgnoreCase)); - } - - private Kernel InitializeKernel(bool useEmbeddings = false, bool useChatModel = false) - { - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - - IKernelBuilder builder = Kernel.CreateBuilder(); - - if (useChatModel) - { - builder.Services.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - else - { - builder.Services.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - - if (useEmbeddings) - { - builder.Services.AddAzureOpenAITextEmbeddingGeneration( - deploymentName: azureOpenAIEmbeddingsConfiguration.DeploymentName, - endpoint: azureOpenAIEmbeddingsConfiguration.Endpoint, - apiKey: azureOpenAIEmbeddingsConfiguration.ApiKey); - } - - return builder.Build(); - } - - private ISemanticTextMemory InitializeMemory(ITextEmbeddingGeneration textEmbeddingGeneration) - { - var builder = new MemoryBuilder(); - - builder.WithMemoryStore(new VolatileMemoryStore()); - builder.WithTextEmbeddingGeneration(textEmbeddingGeneration); - - return builder.Build(); - } - - private readonly RedirectOutput _testOutputHelper; - private readonly IConfigurationRoot _configuration; - - public void Dispose() - { - this._testOutputHelper.Dispose(); - } -} diff --git a/dotnet/src/IntegrationTests/Planners/Stepwise/FunctionCallingStepwisePlannerTests.cs b/dotnet/src/IntegrationTests/Planners/Stepwise/FunctionCallingStepwisePlannerTests.cs deleted file mode 100644 index 3d26a8bc4b5f..000000000000 --- a/dotnet/src/IntegrationTests/Planners/Stepwise/FunctionCallingStepwisePlannerTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Plugins.Core; -using Microsoft.SemanticKernel.Plugins.Web; -using Microsoft.SemanticKernel.Plugins.Web.Bing; -using SemanticKernel.IntegrationTests.Fakes; -using SemanticKernel.IntegrationTests.TestSettings; -using xRetry; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Planners.Stepwise; -public sealed class FunctionCallingStepwisePlannerTests : BaseIntegrationTest, IDisposable -{ - private readonly string _bingApiKey; - - public FunctionCallingStepwisePlannerTests(ITestOutputHelper output) - { - this._logger = new XunitLogger(output); - - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - string? bingApiKeyCandidate = this._configuration["Bing:ApiKey"]; - Assert.NotNull(bingApiKeyCandidate); - this._bingApiKey = bingApiKeyCandidate; - } - - [Theory(Skip = "OpenAI is throttling requests. Switch this test to use Azure OpenAI.")] - [InlineData("What is the tallest mountain on Earth? How tall is it?", new string[] { "WebSearch-Search" })] - [InlineData("What is the weather in Seattle?", new string[] { "WebSearch-Search" })] - [InlineData("What is the current hour number, plus 5?", new string[] { "Time-HourNumber", "Math-Add" })] - [InlineData("What is 387 minus 22? Email the solution to John and Mary.", new string[] { "Math-Subtract", "Email-GetEmailAddress", "Email-SendEmail" })] - public async Task CanExecuteStepwisePlanAsync(string prompt, string[] expectedFunctions) - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var bingConnector = new BingConnector(this._bingApiKey); - var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); - kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); - kernel.ImportPluginFromType("Time"); - kernel.ImportPluginFromType("Math"); - kernel.ImportPluginFromType("Email"); - - var planner = new FunctionCallingStepwisePlanner( - new FunctionCallingStepwisePlannerOptions() { MaxIterations = 10 }); - - // Act - var planResult = await planner.ExecuteAsync(kernel, prompt); - - // Assert - should contain the expected answer & function calls within the maximum iterations - Assert.NotNull(planResult); - Assert.NotEqual(string.Empty, planResult.FinalAnswer); - Assert.True(planResult.Iterations > 0); - Assert.True(planResult.Iterations <= 10); - Assert.NotEmpty(planResult.FinalAnswer); - - string serializedChatHistory = JsonSerializer.Serialize(planResult.ChatHistory); - foreach (string expectedFunction in expectedFunctions) - { - Assert.Contains(expectedFunction, serializedChatHistory, StringComparison.InvariantCultureIgnoreCase); - } - } - - [RetryFact(typeof(HttpOperationException))] - public async Task DoesNotThrowWhenPluginFunctionThrowsNonCriticalExceptionAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var emailPluginFake = new ThrowingEmailPluginFake(); - kernel.Plugins.Add( - KernelPluginFactory.CreateFromFunctions( - "Email", - [ - KernelFunctionFactory.CreateFromMethod(emailPluginFake.WritePoemAsync), - KernelFunctionFactory.CreateFromMethod(emailPluginFake.SendEmailAsync), - ])); - - var planner = new FunctionCallingStepwisePlanner( - new FunctionCallingStepwisePlannerOptions() { MaxIterations = 5 }); - - // Act - var planResult = await planner.ExecuteAsync(kernel, "Email a poem about cats to test@example.com"); - - // Assert - should contain the expected answer & function calls within the maximum iterations - Assert.NotNull(planResult); - Assert.True(planResult.Iterations > 0); - Assert.True(planResult.Iterations <= 5); - - string serializedChatHistory = JsonSerializer.Serialize(planResult.ChatHistory); - Assert.Contains("Email-WritePoem", serializedChatHistory, StringComparison.InvariantCultureIgnoreCase); - Assert.Contains("Email-SendEmail", serializedChatHistory, StringComparison.InvariantCultureIgnoreCase); - } - - [RetryFact(typeof(HttpOperationException))] - public async Task ThrowsWhenPluginFunctionThrowsCriticalExceptionAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var emailPluginFake = new ThrowingEmailPluginFake(); - kernel.Plugins.Add( - KernelPluginFactory.CreateFromFunctions( - "Email", - [ - KernelFunctionFactory.CreateFromMethod(emailPluginFake.WriteJokeAsync), - KernelFunctionFactory.CreateFromMethod(emailPluginFake.SendEmailAsync), - ])); - - var planner = new FunctionCallingStepwisePlanner( - new FunctionCallingStepwisePlannerOptions() { MaxIterations = 5 }); - - // Act & Assert - // Planner should call ThrowingEmailPluginFake.WriteJokeAsync, which throws InvalidProgramException - await Assert.ThrowsAsync(async () => await planner.ExecuteAsync(kernel, "Email a joke to test@example.com")); - } - - [Fact] - public async Task CanExecutePromptFunctionAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var promptFunction = KernelFunctionFactory.CreateFromPrompt( - "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", - functionName: "FindLatestNews", - description: "Searches for the latest news."); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( - "NewsProvider", - "Delivers up-to-date news content.", - [promptFunction])); - - var planner = new FunctionCallingStepwisePlanner( - new FunctionCallingStepwisePlannerOptions() { MaxIterations = 2 }); - - // Act - var planResult = await planner.ExecuteAsync(kernel, "Show me the latest news as they are."); - - // Assert - Assert.NotNull(planResult); - Assert.Contains("Transportation", planResult.FinalAnswer, StringComparison.InvariantCultureIgnoreCase); - } - - private Kernel InitializeKernel() - { - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - IKernelBuilder builder = this.CreateKernelBuilder(); - builder.Services.AddSingleton(this._logger); - builder.AddOpenAIChatCompletion( - modelId: openAIConfiguration.ModelId, - apiKey: openAIConfiguration.ApiKey); - - var kernel = builder.Build(); - - return kernel; - } - - private readonly IConfigurationRoot _configuration; - private readonly XunitLogger _logger; - - public void Dispose() - { - this._logger.Dispose(); - } -} diff --git a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/FunctionCallingStepwisePlannerTests.cs b/dotnet/src/IntegrationTests/Planners/StepwisePlanner/FunctionCallingStepwisePlannerTests.cs deleted file mode 100644 index 9854e63b28ce..000000000000 --- a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/FunctionCallingStepwisePlannerTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Plugins.Core; -using Microsoft.SemanticKernel.Plugins.Web; -using Microsoft.SemanticKernel.Plugins.Web.Bing; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Planners.Stepwise; - -public sealed class FunctionCallingStepwisePlannerTests : IDisposable -{ - private readonly string _bingApiKey; - - public FunctionCallingStepwisePlannerTests(ITestOutputHelper output) - { - this._testOutputHelper = new RedirectOutput(output); - - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - string? bingApiKeyCandidate = this._configuration["Bing:ApiKey"]; - Assert.NotNull(bingApiKeyCandidate); - this._bingApiKey = bingApiKeyCandidate; - } - - [Theory(Skip = "Requires model deployment that supports function calling.")] - [InlineData("What is the tallest mountain on Earth? How tall is it?", "Everest")] - [InlineData("What is the weather in Seattle?", "Seattle")] - public async Task CanExecuteStepwisePlanAsync(string prompt, string partialExpectedAnswer) - { - // Arrange - bool useEmbeddings = false; - Kernel kernel = this.InitializeKernel(useEmbeddings); - var bingConnector = new BingConnector(this._bingApiKey); - var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); - kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); - kernel.ImportPluginFromType("time"); - - var planner = new FunctionCallingStepwisePlanner( - kernel, - new FunctionCallingStepwisePlannerConfig() { MaxIterations = 10 }); - - // Act - var planResult = await planner.ExecuteAsync(prompt); - - // Assert - should contain the expected answer - Assert.NotNull(planResult); - Assert.NotEqual(string.Empty, planResult.FinalAnswer); - Assert.Contains(partialExpectedAnswer, planResult.FinalAnswer, StringComparison.InvariantCultureIgnoreCase); - Assert.True(planResult.Iterations > 0); - Assert.True(planResult.Iterations <= 10); - } - - private Kernel InitializeKernel(bool useEmbeddings = false) - { - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - - var builder = Kernel.CreateBuilder() - .WithAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - if (useEmbeddings) - { - builder.WithAzureOpenAITextEmbeddingGeneration( - deploymentName: azureOpenAIEmbeddingsConfiguration.DeploymentName, - endpoint: azureOpenAIEmbeddingsConfiguration.Endpoint, - apiKey: azureOpenAIEmbeddingsConfiguration.ApiKey); - } - - return builder.Build(); - } - - private readonly RedirectOutput _testOutputHelper; - private readonly IConfigurationRoot _configuration; - - public void Dispose() - { - this._testOutputHelper.Dispose(); - } -} diff --git a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs b/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs deleted file mode 100644 index 99a79dab7577..000000000000 --- a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Plugins.OpenApi.OpenAI; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Plugins.Core; -using Microsoft.SemanticKernel.Plugins.Web; -using Microsoft.SemanticKernel.Plugins.Web.Bing; -using SemanticKernel.IntegrationTests.TestSettings; -using xRetry; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Planners.Stepwise; - -public sealed class StepwisePlannerTests : IDisposable -{ - private readonly string _bingApiKey; - - public StepwisePlannerTests(ITestOutputHelper output) - { - this._testOutputHelper = new RedirectOutput(output); - - // Load configuration - this._configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - string? bingApiKeyCandidate = this._configuration["Bing:ApiKey"]; - Assert.NotNull(bingApiKeyCandidate); - this._bingApiKey = bingApiKeyCandidate; - } - - [Theory] - [InlineData(false, "Who is the current president of the United States? What is his current age divided by 2", "ExecutePlan", "StepwisePlanner")] - [InlineData(true, "Who is the current president of the United States? What is his current age divided by 2", "ExecutePlan", "StepwisePlanner")] - public void CanCreateStepwisePlanAsync(bool useChatModel, string prompt, string expectedFunction, string expectedPlugin) - { - // Arrange - bool useEmbeddings = false; - Kernel kernel = this.InitializeKernel(useEmbeddings, useChatModel); - var bingConnector = new BingConnector(this._bingApiKey); - var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); - kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); - kernel.ImportPluginFromType("time"); - - var planner = new StepwisePlanner(kernel, new() { MaxIterations = 10 }); - - // Act - var plan = planner.CreatePlan(prompt); - - // Assert - Assert.Empty(plan.Steps); - Assert.Equal(expectedFunction, plan.Name); - Assert.Contains(expectedPlugin, plan.PluginName, StringComparison.OrdinalIgnoreCase); - } - - [RetryTheory(maxRetries: 3)] - [InlineData(false, "What is the tallest mountain on Earth? How tall is it divided by 2", "Everest")] - [InlineData(true, "What is the tallest mountain on Earth? How tall is it divided by 2", "Everest")] - [InlineData(false, "What is the weather in Seattle?", "Seattle")] - [InlineData(true, "What is the weather in Seattle?", "Seattle")] - public async Task CanExecuteStepwisePlanAsync(bool useChatModel, string prompt, string partialExpectedAnswer) - { - // Arrange - bool useEmbeddings = false; - Kernel kernel = this.InitializeKernel(useEmbeddings, useChatModel); - var bingConnector = new BingConnector(this._bingApiKey); - var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); - kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); - kernel.ImportPluginFromType("time"); - - var planner = new StepwisePlanner(kernel, new() { MaxIterations = 10 }); - - // Act - var plan = planner.CreatePlan(prompt); - var planResult = await plan.InvokeAsync(kernel); - var result = planResult.GetValue(); - - // Assert - should contain the expected answer - Assert.NotNull(result); - Assert.Contains(partialExpectedAnswer, result, StringComparison.InvariantCultureIgnoreCase); - Assert.True(planResult.TryGetMetadataValue("iterations", out string iterations)); - Assert.True(int.Parse(iterations, System.Globalization.CultureInfo.InvariantCulture) > 0); - Assert.True(int.Parse(iterations, System.Globalization.CultureInfo.InvariantCulture) <= 10); - } - - [Fact] - public async Task ExecutePlanFailsWithTooManyFunctionsAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var bingConnector = new BingConnector(this._bingApiKey); - var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector); - kernel.ImportPluginFromObject(webSearchEnginePlugin, "WebSearch"); - kernel.ImportPluginFromType("text"); - kernel.ImportPluginFromType("ConversationSummary"); - kernel.ImportPluginFromType("Math"); - kernel.ImportPluginFromType("FileIO"); - kernel.ImportPluginFromType("Http"); - - var planner = new StepwisePlanner(kernel, new() { MaxTokens = 1000 }); - - // Act - var plan = planner.CreatePlan("I need to buy a new brush for my cat. Can you show me options?"); - - // Assert - var ex = await Assert.ThrowsAsync(async () => await plan.InvokeAsync(kernel)); - Assert.Equal("ChatHistory is too long to get a completion. Try reducing the available functions.", ex.Message); - } - - [Fact] - public async Task ExecutePlanSucceedsWithAlmostTooManyFunctionsAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - _ = await kernel.ImportPluginFromOpenAIAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenAIFunctionExecutionParameters(enableDynamicOperationPayload: true)); - - var planner = new StepwisePlanner(kernel); - - // Act - var plan = planner.CreatePlan("I need to buy a new brush for my cat. Can you show me options?"); - var functionResult = await plan.InvokeAsync(kernel); - var result = functionResult.GetValue(); - - // Assert - should contain results, for now just verify it didn't fail - Assert.NotNull(result); - Assert.DoesNotContain("Result not found, review 'stepsTaken' to see what happened", result, StringComparison.OrdinalIgnoreCase); - } - - private Kernel InitializeKernel(bool useEmbeddings = false, bool useChatModel = false) - { - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIEmbeddingsConfiguration); - - IKernelBuilder builder = Kernel.CreateBuilder(); - - if (useChatModel) - { - builder.Services.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - else - { - builder.Services.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - endpoint: azureOpenAIConfiguration.Endpoint, - credentials: new AzureCliCredential()); - } - - if (useEmbeddings) - { - builder.Services.AddAzureOpenAITextEmbeddingGeneration( - deploymentName: azureOpenAIEmbeddingsConfiguration.DeploymentName, - endpoint: azureOpenAIEmbeddingsConfiguration.Endpoint, - apiKey: azureOpenAIEmbeddingsConfiguration.ApiKey); - } - - return builder.Build(); - } - - private readonly RedirectOutput _testOutputHelper; - private readonly IConfigurationRoot _configuration; - - public void Dispose() - { - this._testOutputHelper.Dispose(); - } -} diff --git a/dotnet/src/Planners/Planners.Handlebars.UnitTests/.editorconfig b/dotnet/src/Planners/Planners.Handlebars.UnitTests/.editorconfig deleted file mode 100644 index 394eef685f21..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars.UnitTests/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -# Suppressing errors for Test projects under dotnet folder -[*.cs] -dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task -dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave -dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member -dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations diff --git a/dotnet/src/Planners/Planners.Handlebars.UnitTests/Handlebars/HandlebarsPlannerTests.cs b/dotnet/src/Planners/Planners.Handlebars.UnitTests/Handlebars/HandlebarsPlannerTests.cs deleted file mode 100644 index 6e9d3b8aace1..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars.UnitTests/Handlebars/HandlebarsPlannerTests.cs +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Reflection; -using System.Text.Json; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Planning; -using Microsoft.SemanticKernel.Planning.Handlebars; -using Microsoft.SemanticKernel.Text; -using Moq; -using Xunit; - -namespace Microsoft.SemanticKernel.Planners.UnitTests.Handlebars; - -public sealed class HandlebarsPlannerTests -{ - private const string PlanString = """ - ```handlebars - {{!-- Step 1: Call Summarize function --}} - {{set "summary" (SummarizePlugin-Summarize)}} - - {{!-- Step 2: Call Translate function with the language set to French --}} - {{set "translatedSummary" (WriterPlugin-Translate language="French" input=(get "summary"))}} - - {{!-- Step 3: Call GetEmailAddress function with input set to John Doe --}} - {{set "emailAddress" (email-GetEmailAddress input="John Doe")}} - - {{!-- Step 4: Call SendEmail function with input set to the translated summary and email_address set to the retrieved email address --}} - {{email-SendEmail input=(get "translatedSummary") email_address=(get "emailAddress")}} - ``` - """; - - [Theory] - [InlineData("Summarize this text, translate it to French and send it to John Doe.")] - public async Task ItCanCreatePlanAsync(string goal) - { - // Arrange - var plugins = this.CreatePluginCollection(); - var kernel = this.CreateKernelWithMockCompletionResult(PlanString, plugins); - var planner = new HandlebarsPlanner(); - - // Act - HandlebarsPlan plan = await planner.CreatePlanAsync(kernel, goal); - - // Assert - Assert.False(string.IsNullOrEmpty(plan.Prompt)); - Assert.False(string.IsNullOrEmpty(plan.ToString())); - } - - [Fact] - public async Task EmptyGoalThrowsAsync() - { - // Arrange - var kernel = this.CreateKernelWithMockCompletionResult(PlanString); - - var planner = new HandlebarsPlanner(); - - // Act & Assert - await Assert.ThrowsAsync(async () => await planner.CreatePlanAsync(kernel, string.Empty)); - } - - [Fact] - public async Task InvalidHandlebarsTemplateThrowsAsync() - { - // Arrange - var invalidPlan = "notvalid<"; - var kernel = this.CreateKernelWithMockCompletionResult(invalidPlan); - - var planner = new HandlebarsPlanner(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await planner.CreatePlanAsync(kernel, "goal")); - - Assert.True(exception?.Message?.Contains("CreatePlan failed. See inner exception for details.", StringComparison.InvariantCulture)); - Assert.True(exception?.InnerException?.Message?.Contains("Could not find the plan in the results", StringComparison.InvariantCulture)); - Assert.Equal(exception?.ModelResults?.Content, invalidPlan); - Assert.NotNull(exception?.CreatePlanPrompt); - } - - [Fact] - public void ItDefinesAllPartialsInlinePrompt() - { - // Arrange - var assemply = Assembly.GetExecutingAssembly(); - var planner = new HandlebarsPlanner(); - - var promptName = "CreatePlan"; - var actualPartialsNamespace = $"{planner.GetType().Namespace}.{promptName}PromptPartials"; - var resourceNames = assemply.GetManifestResourceNames() - .Where(name => name.Contains($"{promptName}PromptPartials", StringComparison.CurrentCulture)); - - // Act - var actualContent = planner.ReadAllPromptPartials(promptName); - - // Assert - foreach (var resourceName in resourceNames) - { - var expectedInlinePartialHeader = $"{{{{#*inline \"{resourceName}\"}}}}"; - Assert.Contains(expectedInlinePartialHeader, actualContent, StringComparison.CurrentCulture); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ItInjectsPredefinedVariablesAsync(bool containsPredefinedVariables) - { - // Arrange - var kernel = this.CreateKernelWithMockCompletionResult(PlanString); - - var planner = new HandlebarsPlanner(); - - KernelArguments? mockArguments = containsPredefinedVariables ? new(){ - { "test", new List(){ "test", "test1" } }, - { "testNumber", 1 }, - { "testObject", new Dictionary() - { - {"test", "John Doe" }, - { "testInfo", "testing" }, - } - } - } : null; - - // Act - var plan = await planner.CreatePlanAsync(kernel, "goal", mockArguments); - - // Assert - var sectionHeader = "### Predefined Variables"; - if (containsPredefinedVariables) - { - Assert.Contains(sectionHeader, plan.Prompt, StringComparison.CurrentCulture); - foreach (var variable in mockArguments!) - { - Assert.Contains($"- \"{variable.Key}\" ({variable.Value?.GetType().GetFriendlyTypeName()})", plan.Prompt, StringComparison.CurrentCulture); - Assert.Contains(JsonSerializer.Serialize(variable.Value, JsonOptionsCache.WriteIndented), plan.Prompt, StringComparison.InvariantCulture); - } - } - else - { - Assert.DoesNotContain(sectionHeader, plan.Prompt, StringComparison.CurrentCulture); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ItInjectsAdditionalContextAsync(bool hasAdditionalContext) - { - // Arrange - var kernel = this.CreateKernelWithMockCompletionResult(PlanString); - var mockContext = "Mock context"; - - var planner = new HandlebarsPlanner( - new HandlebarsPlannerOptions() - { - GetAdditionalPromptContext = hasAdditionalContext ? () => Task.FromResult(mockContext) : null - }); - - // Act - var plan = await planner.CreatePlanAsync(kernel, "goal"); - - // Assert - var sectionHeader = "### Additional Context"; - if (hasAdditionalContext) - { - Assert.Contains("### Additional Context", plan.Prompt, StringComparison.CurrentCulture); - Assert.Contains(mockContext, plan.Prompt, StringComparison.CurrentCulture); - } - else - { - Assert.DoesNotContain(sectionHeader, plan.Prompt, StringComparison.CurrentCulture); - } - } - - [Fact] - public async Task ItOverridesPromptAsync() - { - // Arrange - var kernel = this.CreateKernelWithMockCompletionResult(PlanString); - var mockPromptOverride = "Help me fulfill my goal!"; - - var planner = new HandlebarsPlanner( - new HandlebarsPlannerOptions() - { - CreatePlanPromptHandler = () => $"{mockPromptOverride} {{{{> UserGoal }}}}" - }); - - // Act - var plan = await planner.CreatePlanAsync(kernel, "goal"); - - // Assert - Assert.Contains(mockPromptOverride, plan.Prompt, StringComparison.CurrentCulture); - Assert.Contains("## Goal", plan.Prompt, StringComparison.CurrentCulture); - Assert.DoesNotContain("## Tips and reminders", plan.Prompt, StringComparison.CurrentCulture); - } - - [Fact] - public async Task ItThrowsIfStrictlyOnePlanCantBeIdentifiedAsync() - { - // Arrange - var ResponseWithMultipleHbTemplates = """ - ```handlebars - {{!-- Step 1: Call Summarize function --}} - {{set "summary" (SummarizePlugin-Summarize)}} - ``` - - ```handlebars - {{!-- Step 2: Call Translate function with the language set to French --}} - {{set "translatedSummary" (WriterPlugin-Translate language="French" input=(get "summary"))}} - ``` - - ```handlebars - {{!-- Step 3: Call GetEmailAddress function with input set to John Doe --}} - {{set "emailAddress" (email-GetEmailAddress input="John Doe")}} - - {{!-- Step 4: Call SendEmail function with input set to the translated summary and email_address set to the retrieved email address --}} - {{email-SendEmail input=(get "translatedSummary") email_address=(get "emailAddress")}} - ``` - - ```handlebars - {{!-- Step 4: Call SendEmail function with input set to the translated summary and email_address set to the retrieved email address --}} - {{email-SendEmail input=(get "translatedSummary") email_address=(get "emailAddress")}} - ``` - """; - var kernel = this.CreateKernelWithMockCompletionResult(ResponseWithMultipleHbTemplates); - var planner = new HandlebarsPlanner(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await planner.CreatePlanAsync(kernel, "goal")); - Assert.True(exception?.InnerException?.Message?.Contains("Identified multiple Handlebars templates in model response", StringComparison.InvariantCulture)); - } - - private Kernel CreateKernelWithMockCompletionResult(string testPlanString, KernelPluginCollection? plugins = null) - { - plugins ??= []; - - var chatMessage = new ChatMessageContent(AuthorRole.Assistant, testPlanString); - - var chatCompletion = new Mock(); - chatCompletion - .Setup(cc => cc.GetChatMessageContentsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync([chatMessage]); - - var serviceSelector = new Mock(); - IChatCompletionService resultService = chatCompletion.Object; - PromptExecutionSettings? resultSettings = new(); - serviceSelector - .Setup(ss => ss.TrySelectAIService(It.IsAny(), It.IsAny(), It.IsAny(), out resultService!, out resultSettings)) - .Returns(true); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(serviceSelector.Object); - serviceCollection.AddSingleton(chatCompletion.Object); - - return new Kernel(serviceCollection.BuildServiceProvider(), plugins); - } - - private KernelPluginCollection CreatePluginCollection() => - [ - KernelPluginFactory.CreateFromFunctions("email", "Email functions", - [ - KernelFunctionFactory.CreateFromMethod(() => "MOCK FUNCTION CALLED", "SendEmail", "Send an e-mail"), - KernelFunctionFactory.CreateFromMethod(() => "MOCK FUNCTION CALLED", "GetEmailAddress", "Get an e-mail address") - ]), - KernelPluginFactory.CreateFromFunctions("WriterPlugin", "Writer functions", - [ - KernelFunctionFactory.CreateFromMethod(() => "MOCK FUNCTION CALLED", "Translate", "Translate something"), - ]), - KernelPluginFactory.CreateFromFunctions("SummarizePlugin", "Summarize functions", - [ - KernelFunctionFactory.CreateFromMethod(() => "MOCK FUNCTION CALLED", "Summarize", "Summarize something"), - ]) - ]; -} diff --git a/dotnet/src/Planners/Planners.Handlebars.UnitTests/Handlebars/KernelParameterMetadataExtensionsTests.cs b/dotnet/src/Planners/Planners.Handlebars.UnitTests/Handlebars/KernelParameterMetadataExtensionsTests.cs deleted file mode 100644 index b5386e0ac1dc..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars.UnitTests/Handlebars/KernelParameterMetadataExtensionsTests.cs +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel.Planning.Handlebars; -using Xunit; - -namespace Microsoft.SemanticKernel.Planners.UnitTests.Handlebars; - -public class KernelParameterMetadataExtensionsTests -{ - [Fact] - public void ReturnsTrueForPrimitiveOrStringTypes() - { - // Arrange - var primitiveTypes = new Type[] { typeof(int), typeof(double), typeof(bool), typeof(char) }; - var stringType = typeof(string); - - // Act and Assert - foreach (var type in primitiveTypes) - { - Assert.True(KernelParameterMetadataExtensions.IsPrimitiveOrStringType(type)); - } - - Assert.True(KernelParameterMetadataExtensions.IsPrimitiveOrStringType(stringType)); - } - - [Fact] - public void ReturnsFalseForNonPrimitiveOrStringTypes() - { - // Arrange - var nonPrimitiveTypes = new Type[] { typeof(object), typeof(DateTime), typeof(List), typeof(HandlebarsParameterTypeMetadata) }; - - // Act and Assert - foreach (var type in nonPrimitiveTypes) - { - Assert.False(KernelParameterMetadataExtensions.IsPrimitiveOrStringType(type)); - } - } - - [Fact] - public void ReturnsEmptySetForPrimitiveOrStringType() - { - // Arrange - var primitiveType = typeof(int); - - // Act - var result = primitiveType.ToHandlebarsParameterTypeMetadata(); - - // Assert - Assert.Empty(result); - } - - [Fact] - public void ReturnsSetWithOneElementForSimpleClassType() - { - // Arrange - var simpleClassType = typeof(SimpleClass); - - // Act - var result = simpleClassType.ToHandlebarsParameterTypeMetadata(); - - // Assert - Assert.Single(result); - Assert.Equal("SimpleClass", result.First().Name); - Assert.True(result.First().IsComplex); - Assert.Equal(2, result.First().Properties.Count); - Assert.Equal("Id", result.First().Properties[0].Name); - Assert.Equal(typeof(int), result.First().Properties[0].ParameterType); - Assert.Equal("Name", result.First().Properties[1].Name); - Assert.Equal(typeof(string), result.First().Properties[1].ParameterType); - } - - [Fact] - public void ReturnsSetWithOneElementForRecursiveClassType() - { - // Arrange - var recursiveClassType = typeof(RecursiveClass); - - // Act - var result = recursiveClassType.ToHandlebarsParameterTypeMetadata(); - - // Assert - Assert.Single(result); - Assert.Equal(nameof(RecursiveClass), result.First().Name); - Assert.True(result.First().IsComplex); - Assert.Equal(2, result.First().Properties.Count); - Assert.Equal(nameof(RecursiveClass.Name), result.First().Properties[0].Name); - Assert.Equal(typeof(string), result.First().Properties[0].ParameterType); - Assert.Equal(nameof(RecursiveClass.Next), result.First().Properties[1].Name); - Assert.Equal(typeof(RecursiveClass), result.First().Properties[1].ParameterType); - } - - [Fact] - public void ReturnsSetWithMultipleElementsForNestedClassType() - { - // Arrange - var nestedClassType = typeof(NestedClass); - - // Act - var result = nestedClassType.ToHandlebarsParameterTypeMetadata(); - - // Assert - Assert.Equal(3, result.Count); - Assert.Contains(result, r => r.Name == "NestedClass"); - Assert.Contains(result, r => r.Name == "SimpleClass"); - Assert.Contains(result, r => r.Name == "AnotherClass"); - - var nestedClass = result.First(r => r.Name == "NestedClass"); - Assert.True(nestedClass.IsComplex); - Assert.Equal(3, nestedClass.Properties.Count); - Assert.Equal("Id", nestedClass.Properties[0].Name); - Assert.Equal(typeof(int), nestedClass.Properties[0].ParameterType); - Assert.Equal("Simple", nestedClass.Properties[1].Name); - Assert.Equal(typeof(SimpleClass), nestedClass.Properties[1].ParameterType); - Assert.Equal("Another", nestedClass.Properties[2].Name); - Assert.Equal(typeof(AnotherClass), nestedClass.Properties[2].ParameterType); - - var simpleClass = result.First(r => r.Name == "SimpleClass"); - Assert.True(simpleClass.IsComplex); - Assert.Equal(2, simpleClass.Properties.Count); - Assert.Equal("Id", simpleClass.Properties[0].Name); - Assert.Equal(typeof(int), simpleClass.Properties[0].ParameterType); - Assert.Equal("Name", simpleClass.Properties[1].Name); - Assert.Equal(typeof(string), simpleClass.Properties[1].ParameterType); - - var anotherClass = result.First(r => r.Name == "AnotherClass"); - Assert.True(anotherClass.IsComplex); - Assert.Single(anotherClass.Properties); - Assert.Equal("Value", anotherClass.Properties[0].Name); - Assert.Equal(typeof(double), anotherClass.Properties[0].ParameterType); - - // Should not contain primitive types - Assert.DoesNotContain(result, r => r.Name == "Id"); - Assert.DoesNotContain(result, r => !r.IsComplex); - - // Should not contain empty complex types - Assert.DoesNotContain(result, r => r.IsComplex && r.Properties.Count == 0); - } - - [Fact] - public void ReturnsSetWithOneElementForTaskOfSimpleClassType() - { - // Arrange - var taskOfSimpleClassType = typeof(Task); - - // Act - var result = taskOfSimpleClassType.ToHandlebarsParameterTypeMetadata(); - - // Assert - Assert.Single(result); - Assert.Equal("SimpleClass", result.First().Name); - Assert.True(result.First().IsComplex); - Assert.Equal(2, result.First().Properties.Count); - Assert.Equal("Id", result.First().Properties[0].Name); - Assert.Equal(typeof(int), result.First().Properties[0].ParameterType); - Assert.Equal("Name", result.First().Properties[1].Name); - Assert.Equal(typeof(string), result.First().Properties[1].ParameterType); - } - - [Fact] - public void ReturnsEmptySetForTaskOfPrimitiveOrStringType() - { - // Arrange - var taskOfPrimitiveType = typeof(Task); - var taskOfStringType = typeof(Task); - - // Act - var result1 = taskOfPrimitiveType.ToHandlebarsParameterTypeMetadata(); - var result2 = taskOfStringType.ToHandlebarsParameterTypeMetadata(); - - // Assert - Assert.Empty(result1); - Assert.Empty(result2); - } - - [Fact] - public void ReturnsTrueForPrimitiveOrStringSchemaTypes() - { - // Arrange - var primitiveSchemaTypes = new string[] { "string", "number", "integer", "boolean" }; - - // Act and Assert - foreach (var type in primitiveSchemaTypes) - { - Assert.True(KernelParameterMetadataExtensions.IsPrimitiveOrStringType(type)); - } - } - - [Fact] - public void ReturnsFalseForNonPrimitiveOrStringSchemaTypes() - { - // Arrange - var nonPrimitiveSchemaTypes = new string[] { "object", "array", "any", "null" }; - - // Act and Assert - foreach (var type in nonPrimitiveSchemaTypes) - { - Assert.False(KernelParameterMetadataExtensions.IsPrimitiveOrStringType(type)); - } - } - - [Fact] - public void ReturnsParameterWithParameterTypeForPrimitiveOrStringSchemaType() - { - // Arrange - var schemaTypeMap = new Dictionary - { - {"string", typeof(string)}, - {"integer", typeof(long)}, - {"number", typeof(double)}, - {"boolean", typeof(bool)}, - {"null", typeof(object)} - }; - - foreach (var pair in schemaTypeMap) - { - var schema = KernelJsonSchema.Parse($$"""{"type": "{{pair.Key}}"}"""); - var parameter = new KernelParameterMetadata("test") { Schema = schema }; - - // Act - var result = parameter.ParseJsonSchema(); - - // Assert - Assert.Equal(pair.Value, result.ParameterType); - } - } - - [Fact] - public void ReturnsParameterWithSchemaForNonPrimitiveOrStringSchemaType() - { - // Arrange - var schema = KernelJsonSchema.Parse("""{"type": "object", "properties": {"name": {"type": "string"}}}"""); - var parameter = new KernelParameterMetadata("test") { Schema = schema }; - - // Act - var result = parameter.ParseJsonSchema(); - - // Assert - Assert.Null(result.ParameterType); - Assert.Equal(schema, result.Schema); - } - - [Fact] - public void ReturnsIndentedJsonStringForJsonElement() - { - // Arrange - var jsonProperties = KernelJsonSchema.Parse("""{"name": "Alice", "age": 25}""").RootElement; - - // Act - var result = jsonProperties.ToJsonString(); - - // Ensure that the line endings are consistent across different dotnet versions - result = result.Replace("\r\n", "\n", StringComparison.InvariantCulture); - - // Assert - var expected = "{\n \"name\": \"Alice\",\n \"age\": 25\n}"; - Assert.Equal(expected, result); - } - - [Fact] - public void ReturnsParameterNameAndSchemaType() - { - // Arrange - var schema = KernelJsonSchema.Parse("""{"type": "object", "properties": {"name": {"type": "string"}}}"""); - var parameter = new KernelParameterMetadata("test") { Schema = schema }; - - // Act - var result = parameter.GetSchemaTypeName(); - - // Assert - Assert.Equal("test-object", result); - } - - [Fact] - public void ConvertsReturnParameterMetadataToParameterMetadata() - { - // Arrange - var schema = KernelJsonSchema.Parse("""{"type": "object", "properties": {"name": {"type": "string"}}}"""); - var returnParameter = new KernelReturnParameterMetadata() { Description = "test", ParameterType = typeof(object), Schema = schema }; - - // Act - var functionName = "Foo"; - var result = returnParameter.ToKernelParameterMetadata(functionName); - - // Assert - Assert.Equal("FooReturns", result.Name); - Assert.Equal("test", result.Description); - Assert.Equal(typeof(object), result.ParameterType); - Assert.Equal(schema, result.Schema); - } - - [Fact] - public void ConvertsParameterMetadataToReturnParameterMetadata() - { - // Arrange - var schema = KernelJsonSchema.Parse("""{"type": "object", "properties": {"name": {"type": "string"}}}"""); - var parameter = new KernelParameterMetadata("test") { Description = "test", ParameterType = typeof(object), Schema = schema }; - - // Act - var result = parameter.ToKernelReturnParameterMetadata(); - - // Assert - Assert.Equal("test", result.Description); - Assert.Equal(typeof(object), result.ParameterType); - Assert.Equal(schema, result.Schema); - } - - #region Simple helper classes - - private sealed class SimpleClass - { - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - } - - private sealed class AnotherClass - { - public double Value { get; set; } - } - - private static class NestedClass - { - public static int Id { get; set; } - public static SimpleClass Simple { get; set; } = new SimpleClass(); - public static AnotherClass Another { get; set; } = new AnotherClass(); - } - - private sealed class RecursiveClass - { - public string Name { get; set; } = ""; - - public RecursiveClass Next { get; set; } = new(); - } - - #endregion -} diff --git a/dotnet/src/Planners/Planners.Handlebars.UnitTests/Planners.Handlebars.UnitTests.csproj b/dotnet/src/Planners/Planners.Handlebars.UnitTests/Planners.Handlebars.UnitTests.csproj deleted file mode 100644 index 448a5c2c60ff..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars.UnitTests/Planners.Handlebars.UnitTests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - Microsoft.SemanticKernel.Planners.Handlebars.UnitTests - Microsoft.SemanticKernel.Planners.UnitTests - net8.0 - true - enable - enable - false - $(NoWarn);CA2007,VSTHRD111,SKEXP0060 - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/dotnet/src/Planners/Planners.Handlebars/AssemblyInfo.cs b/dotnet/src/Planners/Planners.Handlebars/AssemblyInfo.cs deleted file mode 100644 index e105bdb168ac..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -// This assembly is currently experimental. -[assembly: Experimental("SKEXP0060")] diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPrompt.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPrompt.handlebars deleted file mode 100644 index dd820361219e..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPrompt.handlebars +++ /dev/null @@ -1,9 +0,0 @@ -{{> IntroductionWithExample }} -{{> Helpers }} - -{{> UserGoal }} - -{{> AdditionalContext }} -{{> RetryLogic }} - -{{> TipsAndInstructions }} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/AdditionalContext.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/AdditionalContext.handlebars deleted file mode 100644 index 986758452cc8..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/AdditionalContext.handlebars +++ /dev/null @@ -1,12 +0,0 @@ -{{#*inline "AdditionalContext"}} -{{#if additionalContext}} -{{#message role="user"}} -Here is additional context that can help you achieve the goal. While the helpers provided are tools for building your template, the context here will inform the content and logic that guide your use of these tools. - -### Additional Context -{{!-- Any domain knowledge or specific content that might help the model better fulfill the goal. Remember to keep the context relevant to the goal and helpers available. - -- This section should just hold static information that's the same for every request. It should not include variables, use KernelArguments when invoking the planner instead.--}} -{{additionalContext}} -{{/message}} -{{/if}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Examples/LoopsAllowedExample.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Examples/LoopsAllowedExample.handlebars deleted file mode 100644 index 17a9013c0e3b..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Examples/LoopsAllowedExample.handlebars +++ /dev/null @@ -1,37 +0,0 @@ -{{#*inline "LoopsAllowedExample"}} -{{#message role="user"}}## Goal -I want you to generate 10 random numbers and send them to another helper. -{{~/message}} - -{{#message role="assistant"}}Here's a Handlebars template that achieves the goal: -```handlebars -\{{!-- Step 0: Extract key values --}} -\{{set - "count" - 10 -}} -\{{!-- Step 1: Loop using the count --}} -\{{#each - (range - 1 - count - ) -}} - \{{!-- Step 2: Create random number --}} - \{{set - "randomNumber" - (Example{{nameDelimiter}}Random - seed=this - ) - }} - \{{!-- Step 3: Call example helper with random number and print the result to the screen --}} - \{{set - "result" - (Example{{nameDelimiter}}Helper - input=randomNumber - ) - }} - \{{json (concat "The result" " " "is:" " " result)}} -\{{/each}} -```{{/message}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Examples/NoLoopsExample.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Examples/NoLoopsExample.handlebars deleted file mode 100644 index 5c01713d1ae2..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Examples/NoLoopsExample.handlebars +++ /dev/null @@ -1,33 +0,0 @@ -{{#*inline "NoLoopsExample"}} -{{#message role="user"}}## Goal -What's the sum of 5+10+15? -{{~/message}} - -{{#message role="assistant"}}Here's a Handlebars template that achieves the goal: -```handlebars -\{{!-- Step 0: Extract key values --}} -\{{set - "num1" - 5 -}} -\{{set - "num2" - 10 -}} -\{{set - "num3" - 15 -}} -\{{!-- Step 1: Call the Example helper with the variables and store the result --}} -\{{set - "sum" - (Example{{nameDelimiter}}AddNums - num1=num1 - num2=num2 - num3=num3 - ) -}} -\{{!-- Step 2: Print the result using the json helper --}} -\{{json (concat "The sum of " num1 "+" num2 "+" num3 " " "is" " " sum)}} -```{{/message}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers.handlebars deleted file mode 100644 index 0e1dc838cde6..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers.handlebars +++ /dev/null @@ -1,21 +0,0 @@ -{{#*inline "Helpers"}} -{{#message role="user"}}The following helpers are available to you: - -{{! Built-in helpers from Handlebars and Semantic Kernel}} -{{> BlockHelpers }} - -{{#if allowLoops}} -{{> LoopHelpers }} -{{/if}} - -{{> MathHelpers }} - -{{> ComparisonHelpers }} - -{{> VariableHelpers }} - -{{! Kernel functions as helpers}} -{{> CustomHelpers }} -IMPORTANT: You can only use the helpers that are listed above. Do not use any other helpers that are not explicitly listed here. For example, do not use `\{{log}}` or any `\{{Example}}` helpers, as they are not supported. -{{/message}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/BlockHelpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/BlockHelpers.handlebars deleted file mode 100644 index 0b9fef9f8060..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/BlockHelpers.handlebars +++ /dev/null @@ -1,11 +0,0 @@ -{{! Built-in helpers from the Handlebars library}} -{{#*inline "BlockHelpers"}} -## Built-in block helpers -- `\{{#if}}\{{/if}}` -- `\{{#unless}}\{{/unless}}`{{#if allowLoops}} -- `\{{#each}}\{{/each}}` - inside this block, you can use: - - `this` to reference the element being iterated over - - `@index` to reference the current index - - `@key` to reference the current key (for object iteration){{/if}} -- `\{{#with}}\{{/with}}` -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/ComparisonHelpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/ComparisonHelpers.handlebars deleted file mode 100644 index d279c115e4bd..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/ComparisonHelpers.handlebars +++ /dev/null @@ -1,6 +0,0 @@ -{{! Built-in system helpers from Semantic Kernel}} -{{#*inline "ComparisonHelpers"}} -## Comparison helpers -If you need to compare two values, you can use the `\{{equals}}` helper. -To use the {{#if allowLoops}}math and {{/if}}comparison helpers, you must pass in two positional values. For example, to check if the variable `var` is equal to number `1`, you would use the following helper like so: `\{{#if (equals var 1)}}\{{/if}}`. -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/CustomHelpers/ComplexTypes.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/CustomHelpers/ComplexTypes.handlebars deleted file mode 100644 index 29ae93e26859..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/CustomHelpers/ComplexTypes.handlebars +++ /dev/null @@ -1,27 +0,0 @@ -{{#*inline "ComplexTypes"}} -## Complex types -Some helpers require arguments that are complex objects. The JSON schemas for these complex objects are defined below: - -{{! Complex Parameter Types}} -{{#each complexTypeDefinitions}} -### {{Name}}: -{ - "type": "Object", - "properties": { - {{#each Properties}} - "{{Name}}": { - "type": "{{ParameterType.Name}}", - }, - {{/each}} - } -} - -{{/each~}} - -{{! JSON Schema Definitions}} -{{#each complexSchemaDefinitions}} -### {{@key}}: -{{this}} - -{{/each}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/CustomHelpers/CustomHelpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/CustomHelpers/CustomHelpers.handlebars deleted file mode 100644 index 9cafce018d71..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/CustomHelpers/CustomHelpers.handlebars +++ /dev/null @@ -1,33 +0,0 @@ -{{! Kernel functions as helpers}} -{{#*inline "CustomHelpers"}} -{{#if (or complexTypeDefinitions complexSchemaDefinitions)}} -{{> ComplexTypes }} -{{/if}} -## Custom helpers -Lastly, you have the following custom helpers to use. - -{{#each functions}} -### `{{doubleOpen}}{{PluginName}}{{../nameDelimiter}}{{Name}}{{doubleClose}}` -Description: {{Description}} -Inputs: - {{#each Parameters}} - - {{Name}}: - {{~#if Schema}} {{getSchemaTypeName this}} - - {{~else}} - {{~#if ParameterType}} {{ParameterType.Name}} -{{/if}} - {{~/if}} - {{~#if Description}} {{Description}}{{/if}} - {{~#if IsRequired}} (required){{else}} (optional){{/if}} - {{/each}} -Output: -{{~#if ReturnParameter}} - {{~#if ReturnParameter.ParameterType}} {{ReturnParameter.ParameterType.Name}} - {{~else}} - {{~#if ReturnParameter.Schema}} {{getSchemaReturnTypeName ReturnParameter Name}} - {{else}} string{{/if}} - {{~/if}} - {{~#if ReturnParameter.Description}} - {{ReturnParameter.Description}}{{/if}} -{{/if}} - -{{/each}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/LoopHelpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/LoopHelpers.handlebars deleted file mode 100644 index 1849c467a342..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/LoopHelpers.handlebars +++ /dev/null @@ -1,9 +0,0 @@ -{{! Built-in system helpers from Semantic Kernel}} -{{#*inline "LoopHelpers"}} -## Loop helpers -If you need to loop through a list of values with `\{{#each}}`, you can use the following helpers: -- `\{{range}}` – Generates a list of integral numbers within a specified range, inclusive of the first and last value. -- `\{{array}}` – Generates an array of values from the given values (zero-indexed). - -IMPORTANT: `range` and `array` are the only supported data structures. Others like `hash` are not supported. Also, you cannot use any methods or properties on the built-in data structures. -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/MathHelpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/MathHelpers.handlebars deleted file mode 100644 index 2aa626200d37..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/MathHelpers.handlebars +++ /dev/null @@ -1,7 +0,0 @@ -{{! Built-in system helpers from Semantic Kernel}} -{{#*inline "MathHelpers"}} -## Math helpers -If you need to do basic operations, you can use these two helpers with numerical values: -- `\{{add}}` – Adds two values together. -- `\{{subtract}}` – Subtracts the second value from the first. -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/VariableHelpers.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/VariableHelpers.handlebars deleted file mode 100644 index 98515787d282..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/Helpers/VariableHelpers.handlebars +++ /dev/null @@ -1,8 +0,0 @@ -{{! Built-in system helpers from Semantic Kernel}} -{{#*inline "VariableHelpers"}} -## Variable helpers -If you need to create or retrieve a variable, you can use the following helpers: -- `\{{set}}` – Creates a variable with the given name and value. It does not print anything to the template, so you must use `\{{json}}` to print the value. -- `\{{json}}` – Serializes the given value and prints result as JSON string. -- `\{{concat}}` – Concatenates the given values into one string. -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/IntroductionWithExample.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/IntroductionWithExample.handlebars deleted file mode 100644 index 75abbd7bcbe0..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/IntroductionWithExample.handlebars +++ /dev/null @@ -1,13 +0,0 @@ -{{#*inline "IntroductionWithExample"}} -{{#message role="system"}}## Instructions -Explain how to achieve the user's goal using the available helpers with a Handlebars .Net template. - -## Example -If the user posed the goal below, you could answer with the following template. -{{~/message}} - -{{#if allowLoops}}{{> LoopsAllowedExample }} -{{~else}}{{> NoLoopsExample }} -{{/if}} -{{#message role="system"}}Now let's try the real thing.{{/message}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/RetryLogic.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/RetryLogic.handlebars deleted file mode 100644 index 0d86bad8f4f3..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/RetryLogic.handlebars +++ /dev/null @@ -1,17 +0,0 @@ -{{#*inline "RetryLogic"}} -{{~#if lastError}} -{{#message role="system"}}## Previous attempt -This previous plan failed to achieve the goal: -```handlebars -{{lastPlan}} -``` - -The error was: -``` -{{lastError}} -``` - -Try again to achieve the goal while fixing the error. -{{/message}} -{{/if}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/TipsAndInstructions.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/TipsAndInstructions.handlebars deleted file mode 100644 index 40cd61484ac8..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/TipsAndInstructions.handlebars +++ /dev/null @@ -1,35 +0,0 @@ -{{#*inline "TipsAndInstructions"}} -{{#message role="system"}} -## Tips and reminders -- Add a comment above each step to describe what the step does. -- Each variable should have a well-defined name. -- Be extremely careful about types. For example, if you pass an array to a helper that expects a number, the template will error out. -- Each step should contain only one helper call. - -## Start -Follow these steps to create one Handlebars template to achieve the goal: -0. Extract Key Values: - - Read the goal and any user-provided content carefully and identify any relevant strings, numbers, or conditions that you'll need. Do not modify any data. - - When generating variables or helper inputs, only use content that the user has explicitly provided or confirmed. If the user did not explicitly provide specific information, you should not invent or assume this information. - - Use the `\{{set}}` helper to create a variable for each key value. - - Omit this step if no values are needed from the initial context. -1. Choose the Right Helpers: - - Use the provided helpers to manipulate the variables you've created. Start with the basic helpers and only use custom helpers if necessary to accomplish the goal. - - Be careful with syntax, i.e., Always reference a custom helper by its full name and remember to use a `#` for all block helpers. -2. Don't Create or Assume Unlisted Helpers: - - Only use the helpers provided. Any helper not listed is considered hallucinated and must not be used. - - Do not invent or assume the existence of any functions not explicitly defined above. -3. What if I Need More Helpers? - - Stop here if the goal cannot be fully achieved with the provided helpers or you need a helper not defined, and just return a string with an appropriate error message. -4. Keep It Simple:{{#if allowLoops}} - - Avoid using loops or block expressions. They are allowed but not always necessary, so try to find a solution that does not use them.{{/if}} - - Your template should be intelligent and efficient, avoiding unnecessary complexity or redundant steps. -5. No Nested Helpers: - - Do not nest helpers or conditionals inside other helpers. This can cause errors in the template. -6. Output the Result: - - Once you have completed the necessary steps to reach the goal, use the `\{{json}}` helper and print only your final template. - - Ensure your template and all steps are enclosed in a ``` handlebars block. - -Remember, the objective is not to use all the helpers available, but to use the correct ones to achieve the desired outcome with a clear and concise template. -{{/message}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/UserGoal.handlebars b/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/UserGoal.handlebars deleted file mode 100644 index d440004cc65b..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/CreatePlanPromptPartials/UserGoal.handlebars +++ /dev/null @@ -1,18 +0,0 @@ -{{#*inline "UserGoal"}} -{{#message role="user"}}## Goal -{{goal}} - -{{~#if predefinedArguments}} - -### Predefined Variables -You have these predefined variables that can be used within the template. You can access these variables using `@root` (e.g., `@root.variableName`). -Only use the `@root` object when referencing values from this initial context. - -{{#each predefinedArguments}} -- "{{@key}}" ({{Type}}): {{Value}} -{{/each}} - -Please note that these variables are not necessarily relevant to your goal. Before you use a predefined variable in the template, ensure it makes sense in the context of the goal and helpers available. Remember to extract key values from the goal as well. -{{/if}} -{{/message}} -{{/inline}} \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/HandlebarsPlannerExtensions.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/HandlebarsPlannerExtensions.cs deleted file mode 100644 index 8e6d0614883a..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/HandlebarsPlannerExtensions.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -/// -/// Extension methods for the interface. -/// -internal static class HandlebarsPlannerExtensions -{ - /// - /// Reads the prompt for the given file name. - /// - /// The handlebars planner. - /// The name of the file to read. - /// The name of the additional namespace. - /// The content of the file as a string. - public static string ReadPlannerPrompt(this HandlebarsPlanner planner, string fileName, string? additionalNameSpace = "") - { - using var stream = planner.ReadPlannerPromptStream(fileName, additionalNameSpace); - using var reader = new StreamReader(stream); - - return reader.ReadToEnd(); - } - - /// - /// Reads the prompt stream for the given file name. - /// - /// The handlebars planner. - /// The name of the file to read. - /// The name of the additional namespace. - /// The stream for the given file name. - public static Stream ReadPlannerPromptStream(this HandlebarsPlanner planner, string fileName, string? additionalNamespace = "") - { - var assembly = Assembly.GetExecutingAssembly(); - var plannerNamespace = planner.GetType().Namespace; - var targetNamespace = !string.IsNullOrEmpty(additionalNamespace) ? $".{additionalNamespace}" : string.Empty; - var resourceName = $"{plannerNamespace}{targetNamespace}.{fileName}"; - - return assembly.GetManifestResourceStream(resourceName)!; - } - - /// - /// Constructs a Handblebars prompt from the given file name and corresponding partials, if any. - /// Partials must be contained in a directory following the naming convention: "{promptName}Partials" and loaded inline first to avoid reference errors. - /// - /// The handlebars planner. - /// The name of the file to read. - /// The name of the additional namespace. - /// Override for Create Plan prompt. - /// The constructed prompt. - public static string ConstructHandlebarsPrompt( - this HandlebarsPlanner planner, - string promptName, - string? additionalNamespace = "", - string? promptOverride = null) - { - var partials = planner.ReadAllPromptPartials(promptName, additionalNamespace); - var prompt = !string.IsNullOrEmpty(promptOverride) ? promptOverride : planner.ReadPlannerPrompt($"{promptName}.handlebars", additionalNamespace); - return partials + prompt; - } - - /// - /// Reads all embedded Handlebars prompt partials from the Handlebars Planner `PromptPartials` namespace and concatenates their contents. - /// - /// The handlebars planner. - /// The name of the parent Handlebars prompt file. - /// The name of the additional namespace. - /// The concatenated content of the embedded partials within the Handlebars Planner namespace. - public static string ReadAllPromptPartials(this HandlebarsPlanner planner, string promptName, string? additionalNamespace = "") - { - var assembly = Assembly.GetExecutingAssembly(); - var plannerNamespace = planner.GetType().Namespace; - var parentNamespace = !string.IsNullOrEmpty(additionalNamespace) ? $"{plannerNamespace}.{additionalNamespace}" : plannerNamespace; - var targetNamespace = $"{parentNamespace}.{promptName}Partials"; - - var resourceNames = assembly.GetManifestResourceNames() - .Where(name => - name.StartsWith(targetNamespace, StringComparison.CurrentCulture) - && name.EndsWith(".handlebars", StringComparison.CurrentCulture)) - // Sort by the number of dots in the name (subdirectory depth), loading subdirectories first, as the outer partials have dependencies on the inner ones. - .OrderByDescending(name => name.Count(c => c == '.')) - // then by the name itself - .ThenBy(name => name); - - var stringBuilder = new StringBuilder(); - foreach (var resourceName in resourceNames) - { - using Stream? resourceStream = assembly.GetManifestResourceStream(resourceName); - if (resourceStream is not null) - { - using var reader = new StreamReader(resourceStream); - stringBuilder.AppendLine(reader.ReadToEnd()); - } - } - - return stringBuilder.ToString(); - } -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/HandlebarsPromptTemplateExtensions.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/HandlebarsPromptTemplateExtensions.cs deleted file mode 100644 index 4bd2c59a94f4..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/HandlebarsPromptTemplateExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using HandlebarsDotNet; -using Microsoft.SemanticKernel.PromptTemplates.Handlebars; -using static Microsoft.SemanticKernel.PromptTemplates.Handlebars.HandlebarsPromptTemplateOptions; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -/// -/// Provides extension methods for rendering Handlebars templates in the context of a Semantic Kernel. -/// -internal sealed class HandlebarsPromptTemplateExtensions -{ - public static void RegisterCustomCreatePlanHelpers( - RegisterHelperCallback registerHelper, - HandlebarsPromptTemplateOptions options, - KernelArguments executionContext - ) - { - registerHelper("getSchemaTypeName", static (Context context, Arguments arguments) => - { - KernelParameterMetadata parameter = (KernelParameterMetadata)arguments[0]; - return parameter.GetSchemaTypeName(); - }); - - registerHelper("getSchemaReturnTypeName", static (Context context, Arguments arguments) => - { - KernelReturnParameterMetadata parameter = (KernelReturnParameterMetadata)arguments[0]; - var functionName = arguments[1].ToString() ?? string.Empty; - return parameter.ToKernelParameterMetadata(functionName).GetSchemaTypeName(); - }); - } -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/KernelParameterMetadataExtensions.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/KernelParameterMetadataExtensions.cs deleted file mode 100644 index a50380716421..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Extensions/KernelParameterMetadataExtensions.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -internal static class KernelParameterMetadataExtensions -{ - /// - /// Checks if type is primitive or string - /// - public static bool IsPrimitiveOrStringType(Type type) => type.IsPrimitive || type == typeof(string); - - /// - /// Checks if stringified type is primitive or string - /// - public static bool IsPrimitiveOrStringType(string type) => - type is "string" or "number" or "integer" or "boolean"; - - /// - /// Converts non-primitive types to a data class definition and returns a hash set of complex type metadata. - /// Complex types will become a data class. - /// If there are nested complex types, the nested complex type will also be returned. - /// Example: - /// Complex type: - /// class ComplexType: - /// propertyA: int - /// propertyB: str - /// propertyC: PropertyC - /// - public static HashSet ToHandlebarsParameterTypeMetadata(this Type type) - { - return type.ToHandlebarsParameterTypeMetadata([]); - } - - private static HashSet ToHandlebarsParameterTypeMetadata(this Type type, HashSet processedTypes) - { - var parameterTypes = new HashSet(); - if (type.TryGetGenericResultType(out var taskResultType)) - { - var resultTypeProperties = taskResultType.GetProperties(); - if (!IsPrimitiveOrStringType(taskResultType) && resultTypeProperties.Length is not 0) - { - parameterTypes.Add(new HandlebarsParameterTypeMetadata() - { - Name = taskResultType.Name, - IsComplex = true, - Properties = resultTypeProperties.Select(p => new KernelParameterMetadata(p.Name) { ParameterType = p.PropertyType }).ToList() - }); - - processedTypes.Add(taskResultType); - parameterTypes.AddNestedComplexTypes(resultTypeProperties, processedTypes); - } - } - else if (type.IsClass && type != typeof(string)) - { - // Class - var properties = type.GetProperties(); - - parameterTypes.Add(new HandlebarsParameterTypeMetadata() - { - Name = type.Name, - IsComplex = properties.Length is not 0, - Properties = properties.Select(p => new KernelParameterMetadata(p.Name) { ParameterType = p.PropertyType }).ToList() - }); - - processedTypes.Add(type); - parameterTypes.AddNestedComplexTypes(properties, processedTypes); - } - - return parameterTypes; - } - - private static void AddNestedComplexTypes(this HashSet parameterTypes, PropertyInfo[] properties, HashSet processedTypes) - { - // Add nested complex types - foreach (var property in properties) - { - // Only convert the property type if we have not already done so. - if (!processedTypes.Contains(property.PropertyType)) - { - parameterTypes.UnionWith(property.PropertyType.ToHandlebarsParameterTypeMetadata(processedTypes)); - } - } - } - - private static Type GetTypeFromSchema(string schemaType) => - schemaType switch - { - "string" => typeof(string), - "integer" => typeof(long), - "number" => typeof(double), - "boolean" => typeof(bool), - "array" => typeof(object[]), - _ => typeof(object) // default to object for "object", "null", or anything unexpected - }; - - public static KernelParameterMetadata ParseJsonSchema(this KernelParameterMetadata parameter) - { - var schema = parameter.Schema!; - - var type = "object"; - if (schema.RootElement.TryGetProperty("type", out var typeNode)) - { - type = typeNode.Deserialize()!; - } - - if (IsPrimitiveOrStringType(type) || type == "null") - { - return new(parameter) - { - ParameterType = GetTypeFromSchema(type), - Schema = null, - }; - } - - return parameter; - } - - public static string ToJsonString(this JsonElement jsonProperties) - { - return JsonSerializer.Serialize(jsonProperties, JsonOptionsCache.WriteIndented); - } - - public static string GetSchemaTypeName(this KernelParameterMetadata parameter) - { - var schemaType = parameter.Schema?.RootElement.TryGetProperty("type", out var typeElement) is true ? typeElement.ToString() : "object"; - return $"{parameter.Name}-{schemaType}"; - } - - public static KernelParameterMetadata ToKernelParameterMetadata(this KernelReturnParameterMetadata parameter, string functionName) => - new($"{functionName}Returns") - { - Description = parameter.Description, - ParameterType = parameter.ParameterType, - Schema = parameter.Schema - }; - - public static KernelReturnParameterMetadata ToKernelReturnParameterMetadata(this KernelParameterMetadata parameter) => - new() - { - Description = parameter.Description, - ParameterType = parameter.ParameterType, - Schema = parameter.Schema - }; -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlan.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlan.cs deleted file mode 100644 index c6196746156a..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlan.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading; -using System.Threading.Tasks; -using HandlebarsDotNet; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.PromptTemplates.Handlebars; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -/// -/// Represents a Handlebars plan. -/// -public sealed class HandlebarsPlan -{ - /// - /// Error message for hallucinated helpers (helpers that are not registered kernel functions or built-in library helpers). - /// - internal const string HallucinatedHelpersErrorMessage = "Template references a helper that cannot be resolved."; - - /// - /// The handlebars template representing the plan. - /// - private readonly string _template; - - /// - /// Gets the prompt template used to generate the plan. - /// - public string? Prompt { get; set; } = null; - - /// - /// Initializes a new instance of the class. - /// - /// A Handlebars template representing the generated plan. - /// Prompt template used to generate the plan. - public HandlebarsPlan(string generatedPlan, string? createPlanPromptTemplate = null) - { - this._template = generatedPlan; - this.Prompt = createPlanPromptTemplate; - } - - /// - /// Print the generated plan, aka handlebars template that was the create plan chat completion result. - /// - /// Handlebars template representing the plan. - public override string ToString() - { - return this._template; - } - - /// - /// Invokes the Handlebars plan. - /// - /// The containing services, plugins, and other state for use throughout the operation. - /// The arguments. - /// The cancellation token. - /// The plan result. - public Task InvokeAsync( - Kernel kernel, - KernelArguments? arguments = null, - CancellationToken cancellationToken = default) - { - var logger = kernel.LoggerFactory.CreateLogger(typeof(HandlebarsPlan)) ?? NullLogger.Instance; - - return PlannerInstrumentation.InvokePlanAsync( - static (HandlebarsPlan plan, Kernel kernel, KernelArguments? arguments, CancellationToken cancellationToken) - => plan.InvokeCoreAsync(kernel, arguments, cancellationToken), - this, kernel, arguments, logger, cancellationToken); - } - - private async Task InvokeCoreAsync( - Kernel kernel, - KernelArguments? arguments = null, - CancellationToken cancellationToken = default) - { - var templateFactory = new HandlebarsPromptTemplateFactory(options: HandlebarsPlanner.PromptTemplateOptions); - var promptTemplateConfig = new PromptTemplateConfig() - { - Template = this._template, - TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat, - Name = "InvokeHandlebarsPlan", - }; - - var handlebarsTemplate = templateFactory.Create(promptTemplateConfig); - try - { - return await handlebarsTemplate!.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false); - } - catch (HandlebarsRuntimeException ex) when (ex.Message.Contains(HallucinatedHelpersErrorMessage)) - { - var hallucinatedHelpers = ex.Message.Substring(HallucinatedHelpersErrorMessage.Length + 1); - throw new KernelException($"[{HandlebarsPlannerErrorCodes.HallucinatedHelpers}] The plan references hallucinated helpers: {hallucinatedHelpers}", ex); - } - } -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlanner.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlanner.cs deleted file mode 100644 index 9954c232358c..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlanner.cs +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using HandlebarsDotNet.Helpers.Enums; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.PromptTemplates.Handlebars; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -/// -/// Represents a Handlebars planner. -/// -public sealed partial class HandlebarsPlanner -{ - /// - /// Represents static options for all Handlebars Planner prompt templates. - /// - public static readonly HandlebarsPromptTemplateOptions PromptTemplateOptions = new() - { - // Options for built-in Handlebars helpers - Categories = [Category.DateTime], - UseCategoryPrefix = false, - - // Custom helpers - RegisterCustomHelpers = HandlebarsPromptTemplateExtensions.RegisterCustomCreatePlanHelpers, - }; - - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for Handlebars Planner. - public HandlebarsPlanner(HandlebarsPlannerOptions? options = default) - { - this._options = options ?? new HandlebarsPlannerOptions(); - this._templateFactory = new HandlebarsPromptTemplateFactory(options: PromptTemplateOptions); - this._options.ExcludedPlugins.Add("Planner_Excluded"); - } - - /// Creates a plan for the specified goal. - /// The containing services, plugins, and other state for use throughout the operation. - /// The goal for which a plan should be created. - /// Optional. Context arguments to pass to the planner. - /// The to monitor for cancellation requests. The default is . - /// The created plan. - /// is null. - /// is empty or entirely composed of whitespace. - /// A plan could not be created. - public Task CreatePlanAsync(Kernel kernel, string goal, KernelArguments? arguments = null, CancellationToken cancellationToken = default) - { - Verify.NotNullOrWhiteSpace(goal); - - var logger = kernel.LoggerFactory.CreateLogger(typeof(HandlebarsPlanner)) ?? NullLogger.Instance; - - return PlannerInstrumentation.CreatePlanAsync( - static (HandlebarsPlanner planner, Kernel kernel, string goal, KernelArguments? arguments, CancellationToken cancellationToken) - => planner.CreatePlanCoreAsync(kernel, goal, arguments, cancellationToken), - this, kernel, goal, arguments, logger, cancellationToken); - } - - #region private - - private readonly HandlebarsPlannerOptions _options; - - private readonly HandlebarsPromptTemplateFactory _templateFactory; - - private async Task CreatePlanCoreAsync(Kernel kernel, string goal, KernelArguments? arguments, CancellationToken cancellationToken = default) - { - string? createPlanPrompt = null; - ChatMessageContent? modelResults = null; - - try - { - // Get CreatePlan prompt template - var functionsMetadata = await kernel.Plugins.GetFunctionsAsync(this._options, null, null, cancellationToken).ConfigureAwait(false); - var availableFunctions = this.GetAvailableFunctionsManual(functionsMetadata, out var complexParameterTypes, out var complexParameterSchemas); - createPlanPrompt = await this.GetHandlebarsTemplateAsync(kernel, goal, arguments, availableFunctions, complexParameterTypes, complexParameterSchemas, cancellationToken).ConfigureAwait(false); - ChatHistory chatMessages = this.GetChatHistoryFromPrompt(createPlanPrompt); - - // Get the chat completion results - var chatCompletionService = kernel.GetRequiredService(); - modelResults = await chatCompletionService.GetChatMessageContentAsync(chatMessages, executionSettings: this._options.ExecutionSettings, cancellationToken: cancellationToken).ConfigureAwait(false); - - MatchCollection matches = ParseRegex().Matches(modelResults.Content ?? string.Empty); - if (matches.Count < 1) - { - throw new KernelException($"[{HandlebarsPlannerErrorCodes.InvalidTemplate}] Could not find the plan in the results. Additional helpers or input may be required.\n\nPlanner output:\n{modelResults.Content}"); - } - else if (matches.Count > 1) - { - throw new KernelException($"[{HandlebarsPlannerErrorCodes.InvalidTemplate}] Identified multiple Handlebars templates in model response. Please try again.\n\nPlanner output:\n{modelResults.Content}"); - } - - var planTemplate = matches[0].Groups[2].Value.Trim(); - planTemplate = MinifyHandlebarsTemplate(planTemplate); - - return new HandlebarsPlan(planTemplate, createPlanPrompt); - } - catch (KernelException ex) - { - throw new PlanCreationException( - "CreatePlan failed. See inner exception for details.", - createPlanPrompt, - modelResults, - ex - ); - } - } - - private List GetAvailableFunctionsManual( - IEnumerable availableFunctions, - out HashSet complexParameterTypes, - out Dictionary complexParameterSchemas) - { - complexParameterTypes = []; - complexParameterSchemas = []; - - var functionsMetadata = new List(); - foreach (var kernelFunction in availableFunctions) - { - // Extract any complex parameter types for isolated render in prompt template - var parametersMetadata = new List(); - foreach (var parameter in kernelFunction.Parameters) - { - var paramToAdd = this.SetComplexTypeDefinition(parameter, complexParameterTypes, complexParameterSchemas); - parametersMetadata.Add(paramToAdd); - } - - var returnParameter = kernelFunction.ReturnParameter.ToKernelParameterMetadata(kernelFunction.Name); - returnParameter = this.SetComplexTypeDefinition(returnParameter, complexParameterTypes, complexParameterSchemas); - - // Need to override function metadata in case parameter metadata changed (e.g., converted primitive types from schema objects) - var functionMetadata = new KernelFunctionMetadata(kernelFunction.Name) - { - PluginName = kernelFunction.PluginName, - Description = kernelFunction.Description, - Parameters = parametersMetadata, - ReturnParameter = returnParameter.ToKernelReturnParameterMetadata() - }; - functionsMetadata.Add(functionMetadata); - } - - return functionsMetadata; - } - - // Extract any complex types or schemas for isolated render in prompt template - private KernelParameterMetadata SetComplexTypeDefinition( - KernelParameterMetadata parameter, - HashSet complexParameterTypes, - Dictionary complexParameterSchemas) - { - if (parameter.Schema is not null) - { - // Class types will have a defined schema, but we want to handle those as built-in complex types below - if (parameter.ParameterType is not null && parameter.ParameterType!.IsClass) - { - parameter = new(parameter) { Schema = null }; - } - else - { - // Parse the schema to extract any primitive types and set in ParameterType property instead - var parsedParameter = parameter.ParseJsonSchema(); - if (parsedParameter.Schema is not null) - { - complexParameterSchemas[parameter.GetSchemaTypeName()] = parameter.Schema.RootElement.ToJsonString(); - } - - return parsedParameter; - } - } - - if (parameter.ParameterType is not null) - { - // Async return type - need to extract the actual return type and override ParameterType property - var type = parameter.ParameterType; - if (type.TryGetGenericResultType(out var taskResultType)) - { - parameter = new(parameter) { ParameterType = taskResultType }; // Actual Return Type - } - - complexParameterTypes.UnionWith(parameter.ParameterType!.ToHandlebarsParameterTypeMetadata()); - } - - return parameter; - } - - private ChatHistory GetChatHistoryFromPrompt(string prompt) - { - // Extract the chat history from the rendered prompt - string pattern = @"<(user~|system~|assistant~)>(.*?)<\/\1>"; - MatchCollection matches = Regex.Matches(prompt, pattern, RegexOptions.Singleline); - - // Add the chat history to the chat - var chatMessages = new ChatHistory(); - foreach (Match m in matches.Cast()) - { - string role = m.Groups[1].Value; - string message = m.Groups[2].Value; - - switch (role) - { - case "user~": - chatMessages.AddUserMessage(message); - break; - case "system~": - chatMessages.AddSystemMessage(message); - break; - case "assistant~": - chatMessages.AddAssistantMessage(message); - break; - default: - Debug.Fail($"Unexpected role: {role}"); - break; - } - } - - return chatMessages; - } - - private async Task GetHandlebarsTemplateAsync( - Kernel kernel, - string goal, - KernelArguments? predefinedArguments, - List availableFunctions, - HashSet complexParameterTypes, - Dictionary complexParameterSchemas, - CancellationToken cancellationToken) - { - // Set-up prompt context - var predefinedArgumentsWithTypes = predefinedArguments?.ToDictionary( - kvp => kvp.Key, - kvp => new - { - Type = kvp.Value?.GetType().GetFriendlyTypeName(), - Value = JsonSerializer.Serialize(kvp.Value, JsonOptionsCache.WriteIndented) - } - ); - - var additionalContext = this._options.GetAdditionalPromptContext is not null - ? await this._options.GetAdditionalPromptContext.Invoke().ConfigureAwait(false) - : null; - - var arguments = new KernelArguments() - { - { "functions", availableFunctions}, - { "goal", goal }, - { "predefinedArguments", predefinedArgumentsWithTypes}, - { "nameDelimiter", this._templateFactory.NameDelimiter}, - { "allowLoops", this._options.AllowLoops }, - { "complexTypeDefinitions", complexParameterTypes.Count > 0 && complexParameterTypes.Any(p => p.IsComplex) ? complexParameterTypes.Where(p => p.IsComplex) : null}, - { "complexSchemaDefinitions", complexParameterSchemas.Count > 0 ? complexParameterSchemas : null}, - { "lastPlan", this._options.LastPlan }, - { "lastError", this._options.LastError }, - { "additionalContext", !string.IsNullOrWhiteSpace(additionalContext) ? additionalContext : null }, - }; - - // Construct prompt from Partials and Prompt Template - var createPlanPrompt = this.ConstructHandlebarsPrompt("CreatePlanPrompt", promptOverride: this._options.CreatePlanPromptHandler?.Invoke()); - - // Render the prompt - var promptTemplateConfig = new PromptTemplateConfig() - { - Template = createPlanPrompt, - TemplateFormat = HandlebarsPromptTemplateFactory.HandlebarsTemplateFormat, - Name = "Planner_Excluded-CreateHandlebarsPlan", - }; - - var handlebarsTemplate = this._templateFactory.Create(promptTemplateConfig); - return await handlebarsTemplate!.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(true); - } - - private static string MinifyHandlebarsTemplate(string template) - { - // This regex pattern matches '{{', then any characters including newlines (non-greedy), then '}}' - // Replace all occurrences of the pattern in the input template - return MinifyRegex().Replace(template, m => - { - // For each match, remove the whitespace within the handlebars, except for spaces - // that separate different items (e.g., 'json' and '(get') - return WhitespaceRegex().Replace(m.Value, " ").Replace(" {", "{").Replace(" }", "}").Replace(" )", ")"); - }); - } - - /// - /// Regex breakdown: - /// (```\s*handlebars){1}\s*: Opening backticks, starting boundary for HB template - /// ((([^`]|`(?!``))+): Any non-backtick character or one backtick character not followed by 2 more consecutive backticks - /// (\s*```){1}: Closing backticks, closing boundary for HB template - /// -#if NET - [GeneratedRegex(@"(```\s*handlebars){1}\s*(([^`]|`(?!``))+)(\s*```){1}", RegexOptions.Multiline)] - private static partial Regex ParseRegex(); - - [GeneratedRegex(@"\{\{[\s\S]*?}}")] - private static partial Regex MinifyRegex(); - - [GeneratedRegex(@"\s+")] - private static partial Regex WhitespaceRegex(); -#else - private static readonly Regex s_parseRegex = new(@"(```\s*handlebars){1}\s*(([^`]|`(?!``))+)(\s*```){1}", RegexOptions.Multiline | RegexOptions.Compiled); - private static Regex ParseRegex() => s_parseRegex; - - private static readonly Regex s_minifyRegex = new(@"(\{\{[\s\S]*?}})"); - private static Regex MinifyRegex() => s_minifyRegex; - - private static readonly Regex s_whitespaceRegex = new(@"\s+"); - private static Regex WhitespaceRegex() => s_whitespaceRegex; -#endif - #endregion -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlannerOptions.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlannerOptions.cs deleted file mode 100644 index 485968ef9795..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/HandlebarsPlannerOptions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -/// -/// Configuration for Handlebars planner instances. -/// -public sealed class HandlebarsPlannerOptions : PlannerOptions -{ - /// - /// The prompt execution settings to use for the planner. - /// - public PromptExecutionSettings? ExecutionSettings { get; set; } - - /// - /// Delegate to get additional context for the prompt. - /// - /// - /// Additional context can be any domain knowledge or specific content that might help the model better fulfill the goal. - /// This context should just hold static information that's the same for every request. It should not include variables; use KernelArguments when invoking the planner instead. - /// - public Func>? GetAdditionalPromptContext { get; set; } - - /// - /// Delegate that returns an override for the CreatePlan prompt. - /// - /// - /// Handler will be used as a callback. The callback should return a valid Handlebars template string in a ```handlebars codeblock. - /// If this is set, the planner will use this prompt instead of the default prompt. - /// Devs can select any partial defined in Planners.Handlebars.CreatePlanPromptPartials namespace when constructing their own prompt. - /// No partials are included by default; make sure to select partials such as "{{> UserGoal }}" or {{> AdditionalContext}} if needed. - /// - public Func? CreatePlanPromptHandler { get; set; } - - /// - /// Gets or sets the last plan generated by the planner. - /// - public HandlebarsPlan? LastPlan { get; set; } - - /// - /// Gets or sets the last error that occurred during planning. - /// - public string? LastError { get; set; } - - /// - /// Gets or sets a value indicating whether loops are allowed in the plan. - /// - public bool AllowLoops { get; set; } = true; - - /// - /// Initializes a new instance of the class. - /// - public HandlebarsPlannerOptions( - HandlebarsPlan? lastPlan = default, - string? lastError = default, - bool allowLoops = true - ) - { - this.LastPlan = lastPlan; - this.LastError = lastError; - this.AllowLoops = allowLoops; - } -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Models/HandlebarsParameterTypeMetadata.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/Models/HandlebarsParameterTypeMetadata.cs deleted file mode 100644 index 7d2362729ed9..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Models/HandlebarsParameterTypeMetadata.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -internal sealed class HandlebarsParameterTypeMetadata -{ - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("isComplexType")] - public bool IsComplex { get; set; } = false; - - /// - /// If this is a complex type, this will contain the properties of the complex type. - /// - [JsonPropertyName("properties")] - public List Properties { get; set; } = []; - - // Override the Equals method to compare the property values - public override bool Equals(object? obj) - { - // Check to make sure the object is the expected type - if (obj is not HandlebarsParameterTypeMetadata other) - { - return false; - } - - // Compare the Name and IsComplex properties - if (this.Name != other.Name || this.IsComplex != other.IsComplex) - { - return false; - } - - // Compare the Properties lists using a helper method - return ArePropertiesEqual(this.Properties, other.Properties); - } - - // A helper method to compare two lists of KernelParameterMetadata - private static bool ArePropertiesEqual(List list1, List list2) - { - // Check if the lists are null or have different lengths - if (list1 is null || list2 is null || list1.Count != list2.Count) - { - return false; - } - - // Compare the elements of the lists by comparing the Name and ParameterType properties - for (int i = 0; i < list1.Count; i++) - { - if (!list1[i].Name.Equals(list2[i].Name, System.StringComparison.Ordinal) || !list1[i].ParameterType!.Equals(list2[i].ParameterType)) - { - return false; - } - } - - // If all elements are equal, return true - return true; - } - - // Override the GetHashCode method to generate a hash code based on the property values - public override int GetHashCode() - { - HashCode hash = default; - hash.Add(this.Name); - hash.Add(this.IsComplex); - foreach (var item in this.Properties) - { - // Combine the Name and ParameterType properties into one hash code - hash.Add( - HashCode.Combine(item.Name, item.ParameterType) - ); - } - - return hash.ToHashCode(); - } -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Models/HandlebarsPlannerErrorCodes.cs b/dotnet/src/Planners/Planners.Handlebars/Handlebars/Models/HandlebarsPlannerErrorCodes.cs deleted file mode 100644 index adfafe292933..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Handlebars/Models/HandlebarsPlannerErrorCodes.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace Microsoft.SemanticKernel.Planning.Handlebars; - -/// -/// Enum error codes for Handlebars planner exceptions. -/// -public enum HandlebarsPlannerErrorCodes -{ - /// - /// Error code for hallucinated helpers. - /// - HallucinatedHelpers, - - /// - /// Error code for invalid Handlebars template. - /// - InvalidTemplate, - - /// - /// Error code for insufficient functions to complete the goal. - /// - InsufficientFunctionsForGoal, -} diff --git a/dotnet/src/Planners/Planners.Handlebars/Planners.Handlebars.csproj b/dotnet/src/Planners/Planners.Handlebars/Planners.Handlebars.csproj deleted file mode 100644 index d39c23223a33..000000000000 --- a/dotnet/src/Planners/Planners.Handlebars/Planners.Handlebars.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Microsoft.SemanticKernel.Planners.Handlebars - Microsoft.SemanticKernel.Planning - net8.0;netstandard2.0 - preview - false - - - - - - - - Semantic Kernel - Planners - Semantic Kernel Handlebars Planners. - - - - - Always - - - Always - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Planners/Planners.OpenAI/AssemblyInfo.cs b/dotnet/src/Planners/Planners.OpenAI/AssemblyInfo.cs deleted file mode 100644 index e105bdb168ac..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -// This assembly is currently experimental. -[assembly: Experimental("SKEXP0060")] diff --git a/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj b/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj deleted file mode 100644 index 46efa703c1b6..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Microsoft.SemanticKernel.Planners.OpenAI - Microsoft.SemanticKernel.Planning - net8.0;netstandard2.0 - preview - false - - - - - - - - Semantic Kernel - Planners - Semantic Kernel OpenAI Planners. - - - - - - - - - Always - - - Always - - - - - - - - - diff --git a/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlanner.cs b/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlanner.cs deleted file mode 100644 index 7b0a39b845f0..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlanner.cs +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Microsoft.SemanticKernel.Planning; - -/// -/// A planner that uses OpenAI function calling in a stepwise manner to fulfill a user goal or question. -/// -public sealed class FunctionCallingStepwisePlanner -{ - /// - /// Initialize a new instance of the class. - /// - /// The planner options. - public FunctionCallingStepwisePlanner( - FunctionCallingStepwisePlannerOptions? options = null) - { - this._options = options ?? new(); - this._generatePlanYaml = this._options.GetInitialPlanPromptTemplate?.Invoke() ?? EmbeddedResource.Read("Stepwise.GeneratePlan.yaml"); - this._stepPrompt = this._options.GetStepPromptTemplate?.Invoke() ?? EmbeddedResource.Read("Stepwise.StepPrompt.txt"); - this._options.ExcludedPlugins.Add(StepwisePlannerPluginName); - } - - /// - /// Execute a plan - /// - /// The containing services, plugins, and other state for use throughout the operation. - /// The question to answer - /// The chat history for the steps of the plan. If null, the planner will generate the chat history for the first step. - /// The to monitor for cancellation requests. The default is . - /// Result containing the model's response message and chat history. - public Task ExecuteAsync( - Kernel kernel, - string question, - ChatHistory? chatHistoryForSteps = null, - CancellationToken cancellationToken = default) - { - var logger = kernel.LoggerFactory.CreateLogger(this.GetType()) ?? NullLogger.Instance; - -#pragma warning disable CS8604 // Possible null reference argument. - return PlannerInstrumentation.InvokePlanAsync( - static (FunctionCallingStepwisePlanner plan, Kernel kernel, Tuple? input, CancellationToken cancellationToken) - => plan.ExecuteCoreAsync(kernel, input?.Item1!, input?.Item2, cancellationToken), - this, kernel, new Tuple(question, chatHistoryForSteps), logger, cancellationToken); -#pragma warning restore CS8604 // Possible null reference argument. - } - - #region private - - private async Task ExecuteCoreAsync( - Kernel kernel, - string question, - ChatHistory chatHistoryForSteps, - CancellationToken cancellationToken = default) - { - Verify.NotNullOrWhiteSpace(question); - Verify.NotNull(kernel); - IChatCompletionService chatCompletion = kernel.GetRequiredService(); - ILoggerFactory loggerFactory = kernel.LoggerFactory; - ILogger logger = loggerFactory.CreateLogger(this.GetType()) ?? NullLogger.Instance; - var stepExecutionSettings = this._options.ExecutionSettings ?? new OpenAIPromptExecutionSettings(); - - // Clone the kernel so that we can add planner-specific plugins without affecting the original kernel instance - var clonedKernel = kernel.Clone(); - clonedKernel.ImportPluginFromType(); - - if (chatHistoryForSteps is null) - { - // Create and invoke a kernel function to generate the initial plan - var promptTemplateFactory = new KernelPromptTemplateFactory(loggerFactory); - var initialPlan = await this.GeneratePlanAsync(question, clonedKernel, logger, cancellationToken).ConfigureAwait(false); - - // Build chat history for the first step - chatHistoryForSteps = await this.BuildChatHistoryForStepAsync(question, initialPlan, clonedKernel, promptTemplateFactory, cancellationToken).ConfigureAwait(false); - } - - for (int i = 0; i < this._options.MaxIterations; i++) - { - // sleep for a bit to avoid rate limiting - if (i > 0 && this._options.MinIterationTimeMs > 0) - { - await Task.Delay(this._options.MinIterationTimeMs, cancellationToken).ConfigureAwait(false); - } - - // For each step, request another completion to select a function for that step - chatHistoryForSteps.AddUserMessage(StepwiseUserMessage); - var chatResult = await this.GetCompletionWithFunctionsAsync(chatHistoryForSteps, clonedKernel, chatCompletion, stepExecutionSettings, logger, cancellationToken).ConfigureAwait(false); - chatHistoryForSteps.Add(chatResult); - - // Check for function response - if (!this.TryGetFunctionResponse(chatResult, out IReadOnlyList? functionResponses, out string? functionResponseError)) - { - // No function response found. Either AI returned a chat message, or something went wrong when parsing the function. - // Log the error (if applicable), then let the planner continue. - if (functionResponseError is not null) - { - chatHistoryForSteps.AddUserMessage(functionResponseError); - } - continue; - } - - // Check for final answer in the function response - foreach (OpenAIFunctionToolCall functionResponse in functionResponses) - { - if (this.TryFindFinalAnswer(functionResponse, out string finalAnswer, out string? finalAnswerError)) - { - if (finalAnswerError is not null) - { - // We found a final answer, but failed to parse it properly. - // Log the error message in chat history and let the planner try again. - chatHistoryForSteps.AddMessage(AuthorRole.Tool, finalAnswerError, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, functionResponse.Id } }); - continue; - } - - // Success! We found a final answer, so return the planner result. - return new FunctionCallingStepwisePlannerResult - { - FinalAnswer = finalAnswer, - ChatHistory = chatHistoryForSteps, - Iterations = i + 1, - }; - } - } - - // Look up function in kernel - foreach (OpenAIFunctionToolCall functionResponse in functionResponses) - { - if (clonedKernel.Plugins.TryGetFunctionAndArguments(functionResponse, out KernelFunction? pluginFunction, out KernelArguments? arguments)) - { - try - { - // Execute function and add to result to chat history - var result = (await clonedKernel.InvokeAsync(pluginFunction, arguments, cancellationToken).ConfigureAwait(false)).GetValue(); - chatHistoryForSteps.AddMessage(AuthorRole.Tool, ParseObjectAsString(result), metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, functionResponse.Id } }); - } - catch (Exception ex) when (!ex.IsCriticalException()) - { - chatHistoryForSteps.AddMessage(AuthorRole.Tool, ex.Message, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, functionResponse.Id } }); - chatHistoryForSteps.AddUserMessage($"Failed to execute function {functionResponse.FullyQualifiedName}. Try something else!"); - } - } - else - { - chatHistoryForSteps.AddUserMessage($"Function {functionResponse.FullyQualifiedName} does not exist in the kernel. Try something else!"); - } - } - } - - // We've completed the max iterations, but the model hasn't returned a final answer. - return new FunctionCallingStepwisePlannerResult - { - FinalAnswer = string.Empty, - ChatHistory = chatHistoryForSteps, - Iterations = this._options.MaxIterations, - }; - } - - private async Task GetCompletionWithFunctionsAsync( - ChatHistory chatHistory, - Kernel kernel, - IChatCompletionService chatCompletion, - OpenAIPromptExecutionSettings openAIExecutionSettings, - ILogger logger, - CancellationToken cancellationToken) - { - openAIExecutionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false); - - await this.ValidateTokenCountAsync(chatHistory, kernel, logger, openAIExecutionSettings, cancellationToken).ConfigureAwait(false); - return await chatCompletion.GetChatMessageContentAsync(chatHistory, openAIExecutionSettings, kernel, cancellationToken).ConfigureAwait(false); - } - - private async Task GetFunctionsManualAsync(Kernel kernel, ILogger logger, CancellationToken cancellationToken) - { - return await kernel.Plugins.GetJsonSchemaFunctionsManualAsync(this._options, null, logger, false, OpenAIFunction.NameSeparator, cancellationToken).ConfigureAwait(false); - } - - // Create and invoke a kernel function to generate the initial plan - private async Task GeneratePlanAsync(string question, Kernel kernel, ILogger logger, CancellationToken cancellationToken) - { - var generatePlanFunction = kernel.CreateFunctionFromPromptYaml(this._generatePlanYaml); - string functionsManual = await this.GetFunctionsManualAsync(kernel, logger, cancellationToken).ConfigureAwait(false); - var generatePlanArgs = new KernelArguments - { - [NameDelimiterKey] = OpenAIFunction.NameSeparator, - [AvailableFunctionsKey] = functionsManual, - [GoalKey] = question - }; - var generatePlanResult = await kernel.InvokeAsync(generatePlanFunction, generatePlanArgs, cancellationToken).ConfigureAwait(false); - return generatePlanResult.GetValue() ?? throw new KernelException("Failed get a completion for the plan."); - } - - private async Task BuildChatHistoryForStepAsync( - string goal, - string initialPlan, - Kernel kernel, - KernelPromptTemplateFactory promptTemplateFactory, - CancellationToken cancellationToken) - { - var chatHistory = new ChatHistory(); - - // Add system message with context about the initial goal/plan - var arguments = new KernelArguments - { - [GoalKey] = goal, - [InitialPlanKey] = initialPlan - }; - var systemMessage = await promptTemplateFactory.Create(new PromptTemplateConfig(this._stepPrompt)).RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false); - - chatHistory.AddSystemMessage(systemMessage); - - return chatHistory; - } - - private bool TryGetFunctionResponse(ChatMessageContent chatMessage, [NotNullWhen(true)] out IReadOnlyList? functionResponses, out string? errorMessage) - { - OpenAIChatMessageContent? openAiChatMessage = chatMessage as OpenAIChatMessageContent; - Verify.NotNull(openAiChatMessage, nameof(openAiChatMessage)); - - functionResponses = null; - errorMessage = null; - try - { - functionResponses = openAiChatMessage.GetOpenAIFunctionToolCalls(); - } - catch (JsonException) - { - errorMessage = "That function call is invalid. Try something else!"; - } - - return functionResponses is { Count: > 0 }; - } - - private bool TryFindFinalAnswer(OpenAIFunctionToolCall functionResponse, out string finalAnswer, out string? errorMessage) - { - finalAnswer = string.Empty; - errorMessage = null; - - if (functionResponse.PluginName == "UserInteraction" && functionResponse.FunctionName == "SendFinalAnswer") - { - if (functionResponse.Arguments is { Count: > 0 } arguments && arguments.TryGetValue("answer", out object? valueObj)) - { - finalAnswer = ParseObjectAsString(valueObj); - } - else - { - errorMessage = "Returned answer in incorrect format. Try again!"; - } - return true; - } - return false; - } - - private static string ParseObjectAsString(object? valueObj) - { - string resultStr = string.Empty; - - if (valueObj is ChatMessageContent chatMessageContent) - { - return chatMessageContent.ToString(); - } - else if (valueObj is RestApiOperationResponse apiResponse) - { - resultStr = apiResponse.Content as string ?? string.Empty; - } - else if (valueObj is string valueStr) - { - resultStr = valueStr; - } - else if (valueObj is JsonElement valueElement) - { - if (valueElement.ValueKind == JsonValueKind.String) - { - resultStr = valueElement.GetString() ?? ""; - } - else - { - resultStr = JsonSerializer.Serialize(valueElement); - } - } - else - { -#pragma warning disable CS0618 // Type or member is obsolete - resultStr = JsonSerializer.Serialize(valueObj); -#pragma warning restore CS0618 // Type or member is obsolete - } - - return resultStr; - } - - private async Task ValidateTokenCountAsync( - ChatHistory chatHistory, - Kernel kernel, - ILogger logger, - OpenAIPromptExecutionSettings openAIExecutionSettings, - CancellationToken cancellationToken) - { - if (this._options.MaxPromptTokens is not null) - { - string functionManual = string.Empty; - - // If using functions, get the functions manual to include in token count estimate - if (openAIExecutionSettings.FunctionChoiceBehavior is not null) - { - functionManual = await this.GetFunctionsManualAsync(kernel, logger, cancellationToken).ConfigureAwait(false); - } - - var tokenCount = chatHistory.GetTokenCount(additionalMessage: functionManual); - if (tokenCount >= this._options.MaxPromptTokens) - { - throw new KernelException("ChatHistory is too long to get a completion. Try reducing the available functions."); - } - } - } - - /// - /// The options for the planner - /// - private readonly FunctionCallingStepwisePlannerOptions _options; - - /// - /// The prompt YAML for generating the initial stepwise plan. - /// - private readonly string _generatePlanYaml; - - /// - /// The prompt (system message) for performing the steps. - /// - private readonly string _stepPrompt; - - /// - /// The name to use when creating semantic functions that are restricted from plan creation - /// - private const string StepwisePlannerPluginName = "StepwisePlanner_Excluded"; - - /// - /// The user message to add to the chat history for each step of the plan. - /// - private const string StepwiseUserMessage = "Perform the next step of the plan if there is more work to do. When you have reached a final answer, use the UserInteraction-SendFinalAnswer function to communicate this back to the user."; - - // Context variable keys - private const string AvailableFunctionsKey = "available_functions"; - private const string InitialPlanKey = "initial_plan"; - private const string GoalKey = "goal"; - private const string NameDelimiterKey = "name_delimiter"; - - #endregion private - - /// - /// Plugin used by the to interact with the caller. - /// - public sealed class UserInteraction - { - /// - /// This function is used by the to indicate when the final answer has been found. - /// - /// The final answer for the plan. - [KernelFunction] - [Description("This function is used to send the final answer of a plan to the user.")] -#pragma warning disable IDE0060 // Remove unused parameter. The parameter is purely an indication to the LLM and is not intended to be used. - public string SendFinalAnswer([Description("The final answer")] string answer) -#pragma warning restore IDE0060 - { - return "Thanks"; - } - } -} diff --git a/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlannerOptions.cs b/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlannerOptions.cs deleted file mode 100644 index 777781cce016..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlannerOptions.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Microsoft.SemanticKernel.Planning; - -/// -/// Configuration for Stepwise planner instances. -/// -public sealed class FunctionCallingStepwisePlannerOptions : PlannerOptions -{ - /// - /// Initializes a new instance of the - /// - public FunctionCallingStepwisePlannerOptions() { } - - /// - /// The maximum total number of tokens to allow in a completion request, - /// which includes the tokens from the prompt and completion - /// - public int? MaxTokens { get; set; } - - /// - /// The ratio of tokens to allocate to the completion request. (prompt / (prompt + completion)) - /// - public double MaxTokensRatio { get; set; } = 0.1; - - internal int? MaxCompletionTokens => (this.MaxTokens is null) ? null : (int)(this.MaxTokens * this.MaxTokensRatio); - internal int? MaxPromptTokens => (this.MaxTokens is null) ? null : (int)(this.MaxTokens * (1 - this.MaxTokensRatio)); - - /// - /// Delegate to get the prompt template YAML for the initial plan generation phase. - /// - public Func? GetInitialPlanPromptTemplate { get; set; } - - /// - /// Delegate to get the prompt template string (system message) for the step execution phase. - /// - public Func? GetStepPromptTemplate { get; set; } - - /// - /// The maximum number of iterations to allow in a plan. - /// - public int MaxIterations { get; set; } = 15; - - /// - /// The minimum time to wait between iterations in milliseconds. - /// - public int MinIterationTimeMs { get; set; } - - /// - /// The prompt execution settings to use for the step execution phase. - /// - public OpenAIPromptExecutionSettings? ExecutionSettings { get; set; } -} diff --git a/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlannerResult.cs b/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlannerResult.cs deleted file mode 100644 index c4cfc3635bcc..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Stepwise/FunctionCallingStepwisePlannerResult.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Planning; - -/// -/// Result produced by the . -/// -public class FunctionCallingStepwisePlannerResult -{ - /// - /// Final result message of the plan. - /// - public string FinalAnswer { get; internal set; } = string.Empty; - - /// - /// Chat history containing the planning process. - /// - public ChatHistory? ChatHistory { get; internal set; } - - /// - /// Number of iterations performed by the planner. - /// - public int Iterations { get; internal set; } = 0; -} diff --git a/dotnet/src/Planners/Planners.OpenAI/Stepwise/GeneratePlan.yaml b/dotnet/src/Planners/Planners.OpenAI/Stepwise/GeneratePlan.yaml deleted file mode 100644 index 7793437aea74..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Stepwise/GeneratePlan.yaml +++ /dev/null @@ -1,32 +0,0 @@ -template_format: semantic-kernel -template: | - - You are an expert at generating plans from a given GOAL. Think step by step and determine a plan to satisfy the specified GOAL using only the FUNCTIONS provided to you. You can also make use of your own knowledge while forming an answer but you must not use functions that are not provided. Once you have come to a final answer, use the UserInteraction{{$name_delimiter}}SendFinalAnswer function to communicate this back to the user. - - [FUNCTIONS] - - {{$available_functions}} - - [END FUNCTIONS] - - To create the plan, follow these steps: - 0. Each step should be something that is capable of being done by the list of available functions. - 1. Steps can use output from one or more previous steps as input, if appropriate. - 2. The plan should be as short as possible. - - {{$goal}} -description: Generate a step-by-step plan to satisfy a given goal -name: GeneratePlan -input_variables: - - name: available_functions - description: A list of functions that can be used to generate the plan - - name: goal - description: The goal to satisfy -execution_settings: - default: - temperature: 0.0 - top_p: 0.0 - presence_penalty: 0.0 - frequency_penalty: 0.0 - max_tokens: 256 - stop_sequences: [] diff --git a/dotnet/src/Planners/Planners.OpenAI/Stepwise/StepPrompt.txt b/dotnet/src/Planners/Planners.OpenAI/Stepwise/StepPrompt.txt deleted file mode 100644 index 1299d55d62e3..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Stepwise/StepPrompt.txt +++ /dev/null @@ -1,6 +0,0 @@ -Original request: {{$goal}} - -You are in the process of helping the user fulfill this request using the following plan: -{{$initial_plan}} - -The user will ask you for help with each step. \ No newline at end of file diff --git a/dotnet/src/Planners/Planners.OpenAI/Utils/EmbeddedResource.cs b/dotnet/src/Planners/Planners.OpenAI/Utils/EmbeddedResource.cs deleted file mode 100644 index 8395297d301a..000000000000 --- a/dotnet/src/Planners/Planners.OpenAI/Utils/EmbeddedResource.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.IO; -using System.Reflection; - -namespace Microsoft.SemanticKernel.Planning; - -internal static class EmbeddedResource -{ - private static readonly string? s_namespace = typeof(EmbeddedResource).Namespace; - - internal static string Read(string name) - { - var assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly ?? - throw new FileNotFoundException($"[{s_namespace}] {name} assembly not found"); - - using Stream? resource = assembly.GetManifestResourceStream($"{s_namespace}." + name) ?? - throw new FileNotFoundException($"[{s_namespace}] {name} resource not found"); - - using var reader = new StreamReader(resource); - return reader.ReadToEnd(); - } -}