diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 66d3625881..90f6efe9ac 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -13,9 +13,14 @@ + + + + + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index d1fb50c218..6de865543f 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -116,15 +116,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144}" ProjectSection(SolutionItems) = preProject src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs - src\InternalUtilities\src\System\InternalTypeConverter.cs = src\InternalUtilities\src\System\InternalTypeConverter.cs - src\InternalUtilities\src\System\TypeConverterFactory.cs = src\InternalUtilities\src\System\TypeConverterFactory.cs - src\InternalUtilities\src\System\NonNullCollection.cs = src\InternalUtilities\src\System\NonNullCollection.cs EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Type", "Type", "{E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF}" - ProjectSection(SolutionItems) = preProject - src\InternalUtilities\src\Type\TypeExtensions.cs = src\InternalUtilities\src\Type\TypeExtensions.cs - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.Core", "src\Plugins\Plugins.Core\Plugins.Core.csproj", "{0D0C4DAD-E6BC-4504-AE3A-EEA4E35920C1}" EndProject @@ -197,6 +191,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow.UnitTests", "src\Experimental\Orchestration.Flow.UnitTests\Experimental.Orchestration.Flow.UnitTests.csproj", "{731CC542-8BE9-42D4-967D-99206EC2B310}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentationExamples", "samples\DocumentationExamples\DocumentationExamples.csproj", "{A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CreateChatGptPlugin", "samples\CreateChatGptPlugin\Solution\CreateChatGptPlugin.csproj", "{87AB5AF5-5783-4372-9789-664895E0A2FF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -460,6 +458,18 @@ Global {731CC542-8BE9-42D4-967D-99206EC2B310}.Publish|Any CPU.Build.0 = Debug|Any CPU {731CC542-8BE9-42D4-967D-99206EC2B310}.Release|Any CPU.ActiveCfg = Release|Any CPU {731CC542-8BE9-42D4-967D-99206EC2B310}.Release|Any CPU.Build.0 = Release|Any CPU + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}.Publish|Any CPU.Build.0 = Debug|Any CPU + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D}.Release|Any CPU.Build.0 = Release|Any CPU + {87AB5AF5-5783-4372-9789-664895E0A2FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87AB5AF5-5783-4372-9789-664895E0A2FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87AB5AF5-5783-4372-9789-664895E0A2FF}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {87AB5AF5-5783-4372-9789-664895E0A2FF}.Publish|Any CPU.Build.0 = Debug|Any CPU + {87AB5AF5-5783-4372-9789-664895E0A2FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87AB5AF5-5783-4372-9789-664895E0A2FF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -528,6 +538,8 @@ Global {B0CE8C69-EC56-4825-94AB-01CA7E8BA55B} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} {3A4B8F90-3E74-43E0-800C-84F8AA9B5BF3} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} {731CC542-8BE9-42D4-967D-99206EC2B310} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658} + {A8E0D3B2-49D7-4DF6-BF91-B234C1C5E25D} = {FA3720F1-C99A-49B2-9577-A940257098BF} + {87AB5AF5-5783-4372-9789-664895E0A2FF} = {FA3720F1-C99A-49B2-9577-A940257098BF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/CreateChatGptPlugin/.editorconfig b/dotnet/samples/CreateChatGptPlugin/.editorconfig new file mode 100644 index 0000000000..39b98ac3a7 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +dotnet_diagnostic.CA1016.severity = none \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/.gitignore b/dotnet/samples/CreateChatGptPlugin/MathPlugin/.gitignore new file mode 100644 index 0000000000..6491a696b6 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/.gitignore @@ -0,0 +1,4 @@ +**/bin +**/obj +local.settings.json +azure-function/appsettings.json \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/README.md b/dotnet/samples/CreateChatGptPlugin/MathPlugin/README.md new file mode 100644 index 0000000000..aa75b0475d --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/README.md @@ -0,0 +1,54 @@ +# Semantic Kernel OpenAI plugin starter + +This project provides starter code to create a OpenAI plugin. It includes the following components: + +- An endpoint that serves up an ai-plugin.json file for ChatGPT to discover the plugin +- A generator that automatically converts prompts into prompt endpoints +- The ability to add additional native functions as endpoints to the plugin + +## Prerequisites + +- [.NET 6](https://dotnet.microsoft.com/download/dotnet/8.0) is required to run this starter. +- [Azure Functions Core Tools](https://www.npmjs.com/package/azure-functions-core-tools) is required to run this starter. +- Install the recommended extensions +- [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) +- [Semantic Kernel Tools](https://marketplace.visualstudio.com/items?itemName=ms-semantic-kernel.semantic-kernel) (optional) + +## Configuring the starter + +To configure the starter, you need to provide the following information: + +- Define the properties of the plugin in the appsettings.json file. +- Enter the API key for your AI endpoint in the [local.settings.json](./azure-function/local.settings.json.example) file. + +### Using appsettings.json + +Configure an OpenAI endpoint + +1. Copy [settings.json.openai-example](./azure-function/config-samples/appsettings.json.openai-example) to `./appsettings.json` +1. Edit the `kernel` object to add your OpenAI endpoint configuration +1. Edit the `aiPlugin` object to define the properties that get exposed in the ai-plugin.json file + +Configure an Azure OpenAI endpoint + +1. Copy [settings.json.azure-example](./azure-function/config-samples/appsettings.json.azure-example) to `./appsettings.json` +1. Edit the `kernel` object to add your Azure OpenAI endpoint configuration +1. Edit the `aiPlugin` object to define the properties that get exposed in the ai-plugin.json file + +### Using local.settings.json + +1. Copy [local.settings.json.example](./azure-function/local.settings.json.example) to `./azure-function/local.settings.json` +1. Edit the `Values` object to add your OpenAI endpoint configuration in the `apiKey` property + +## Running the starter + +To run the Azure Functions application just hit `F5`. + +To build and run the Azure Functions application from a terminal use the following commands: + +```powershell +cd azure-function +dotnet build +cd bin/Debug/net8.0 +func host start +``` diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/AIPluginJson.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/AIPluginJson.cs new file mode 100644 index 0000000000..3c6fae77e9 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/AIPluginJson.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Models; + +public class AIPluginJson +{ + [Function("GetAIPluginJson")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = ".well-known/ai-plugin.json")] HttpRequestData req) + { + var currentDomain = $"{req.Url.Scheme}://{req.Url.Host}:{req.Url.Port}"; + + HttpResponseData response = req.CreateResponse(HttpStatusCode.OK); + response.Headers.Add("Content-Type", "application/json"); + + var appSettings = AppSettings.LoadSettings(); + + // serialize app settings to json using System.Text.Json + var json = System.Text.Json.JsonSerializer.Serialize(appSettings.AIPlugin); + + // replace {url} with the current domain + json = json.Replace("{url}", currentDomain, StringComparison.OrdinalIgnoreCase); + + response.WriteString(json); + + return response; + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Directory.Build.props b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Directory.Build.props new file mode 100644 index 0000000000..607fdf28db --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Directory.Build.props @@ -0,0 +1,33 @@ + + + + true + true + AllEnabledByDefault + latest + true + 11 + enable + disable + CS1591,CA1852,CA1050 + + + + + disable + + + + true + full + + + + portable + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + + + \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Directory.Build.targets b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Directory.Build.targets new file mode 100644 index 0000000000..6f85f5df39 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Directory.Build.targets @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + <_Parameter1 Condition="'%(InternalsVisibleTo.PublicKey)' != ''">%(InternalsVisibleTo.Identity), PublicKey="%(InternalsVisibleTo.PublicKey) + <_Parameter1 Condition="'%(InternalsVisibleTo.PublicKey)' == ''">%(InternalsVisibleTo.Identity) + <_Parameter1_TypeName>System.String + + + + \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Extensions/AIPluginRunner.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Extensions/AIPluginRunner.cs new file mode 100644 index 0000000000..eb7be42130 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Extensions/AIPluginRunner.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net; +using System.Reflection; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; + +namespace Plugins.AzureFunctions.Extensions; + +public class AIPluginRunner +{ + private readonly ILogger _logger; + private readonly Kernel _kernel; + + public AIPluginRunner(Kernel kernel, ILoggerFactory loggerFactory) + { + this._kernel = kernel; + this._logger = loggerFactory.CreateLogger(); + } + + /// + /// Runs a prompt using the operationID and returns back an HTTP response. + /// + /// + /// + /// + public async Task RunAIPluginOperationAsync(HttpRequestData req, string pluginName, string functionName) + { + KernelArguments arguments = ConvertToKernelArguments((await JsonSerializer.DeserializeAsync(req.Body).ConfigureAwait(true))!); + + var response = req.CreateResponse(HttpStatusCode.OK); + response.Headers.Add("Content-Type", "text/plain;charset=utf-8"); + await response.WriteStringAsync( + (await this._kernel.InvokeAsync(pluginName, functionName, arguments).ConfigureAwait(false)).ToString() + ).ConfigureAwait(false); + return response; + } + + // Method to convert model to dictionary + private static KernelArguments ConvertToKernelArguments(T model) + { + { + var arguments = new KernelArguments(); + foreach (PropertyInfo property in typeof(T).GetProperties()) + { + if (property.GetValue(model) != null) + { + arguments.Add(property.Name, property.GetValue(model)); + } + } + return arguments; + } + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Extensions/KernelBuilderExtensions.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Extensions/KernelBuilderExtensions.cs new file mode 100644 index 0000000000..64f1985e10 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Extensions/KernelBuilderExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Models; + +internal static class KernelBuilderExtensions +{ + /// + /// Adds a chat completion service to the list. It can be either an OpenAI or Azure OpenAI backend service. + /// + /// + /// + /// + internal static IServiceCollection WithChatCompletionService(this IServiceCollection kernelBuilder, KernelSettings kernelSettings) + { + switch (kernelSettings.ServiceType.ToUpperInvariant()) + { + case ServiceTypes.AzureOpenAI: + kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName: kernelSettings.DeploymentOrModelId, modelId: kernelSettings.DeploymentOrModelId, endpoint: kernelSettings.Endpoint, apiKey: kernelSettings.ApiKey, serviceId: kernelSettings.ServiceId); + break; + + case ServiceTypes.OpenAI: + kernelBuilder.AddOpenAIChatCompletion(modelId: kernelSettings.DeploymentOrModelId, apiKey: kernelSettings.ApiKey, orgId: kernelSettings.OrgId, serviceId: kernelSettings.ServiceId); + break; + + default: + throw new ArgumentException($"Invalid service type value: {kernelSettings.ServiceType}"); + } + + return kernelBuilder; + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Logo.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Logo.cs new file mode 100644 index 0000000000..ce6e70a69c --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Logo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +public class Logo +{ + [Function("GetLogo")] + public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "logo.png")] HttpRequestData req) + { + // Return logo.png that's in the root of the project + var response = req.CreateResponse(HttpStatusCode.OK); + response.Headers.Add("Content-Type", "image/png"); + + var logo = System.IO.File.ReadAllBytes("logo.png"); + response.Body.Write(logo); + + return response; + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/AIPluginSettings.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/AIPluginSettings.cs new file mode 100644 index 0000000000..929b21b26d --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/AIPluginSettings.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; + +namespace Models; + +#pragma warning disable CA1056 +#pragma warning disable CA1034 + +public class Pluginsettings +{ + [JsonPropertyName("schema_version")] + public string SchemaVersion { get; set; } = "v1"; + + [JsonPropertyName("name_for_model")] + public string NameForModel { get; set; } = string.Empty; + + [JsonPropertyName("name_for_human")] + public string NameForHuman { get; set; } = string.Empty; + + [JsonPropertyName("description_for_model")] + public string DescriptionForModel { get; set; } = string.Empty; + + [JsonPropertyName("description_for_human")] + public string DescriptionForHuman { get; set; } = string.Empty; + + [JsonPropertyName("auth")] + public AuthModel Auth { get; set; } = new AuthModel(); + + [JsonPropertyName("api")] + public ApiModel Api { get; set; } = new ApiModel(); + + [JsonPropertyName("logo_url")] + public string LogoUrl { get; set; } = string.Empty; + + [JsonPropertyName("contact_email")] + public string ContactEmail { get; set; } = string.Empty; + + [JsonPropertyName("legal_info_url")] + public string LegalInfoUrl { get; set; } = string.Empty; + + public class AuthModel + { + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("authorization_url")] + public string AuthorizationType { get; set; } = string.Empty; + } + + public class ApiModel + { + [JsonPropertyName("type")] + public string Type { get; set; } = "openapi"; + + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; + + [JsonPropertyName("has_user_authentication")] + public bool HasUserAuthentication { get; set; } = false; + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/AppSettings.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/AppSettings.cs new file mode 100644 index 0000000000..b173bbb3dc --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/AppSettings.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Configuration; + +namespace Models; + +#pragma warning disable CA1724 +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +public class AppSettings +{ + public const string DefaultConfigFile = "appsettings.json"; + + public KernelSettings Kernel { get; set; } + public Pluginsettings AIPlugin { get; set; } + + /// + /// Load the kernel settings from settings.json if the file exists and if not attempt to use user secrets. + /// + public static AppSettings LoadSettings() + { + try + { + var appSettings = FromFile(DefaultConfigFile); + appSettings.Kernel.ApiKey = GetApiKey(); + + return appSettings; + } + catch (InvalidDataException ide) + { + Console.Error.WriteLine( + "Unable to load app settings, please provide configuration settings using instructions in the README.\n" + + "Please refer to: https://github.com/microsoft/semantic-kernel-starters/blob/main/sk-csharp-chatgpt-plugin/README.md#configuring-the-starter" + ); + throw new InvalidOperationException(ide.Message); + } + } + + /// + /// Load the kernel settings from the specified configuration file if it exists. + /// + private static AppSettings FromFile(string configFile = DefaultConfigFile) + { + if (!File.Exists(configFile)) + { + throw new FileNotFoundException($"Configuration not found: {configFile}"); + } + + var configuration = new ConfigurationBuilder() + .SetBasePath(System.IO.Directory.GetCurrentDirectory()) + .AddJsonFile(configFile, optional: true, reloadOnChange: true) + .Build(); + + return configuration.Get() + ?? throw new InvalidDataException($"Invalid app settings in '{configFile}', please provide configuration settings using instructions in the README."); + } + + /// + /// Load the API key for the AI endpoint from user secrets. + /// + internal static string GetApiKey() + { + return System.Environment.GetEnvironmentVariable("apiKey", EnvironmentVariableTarget.Process) + ?? throw new InvalidDataException("Invalid semantic kernel settings in user secrets, please provide configuration settings using instructions in the README."); + } +} +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/KernelSettings.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/KernelSettings.cs new file mode 100644 index 0000000000..c785b7a935 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/KernelSettings.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Logging; + +namespace Models; + +#pragma warning disable CA1812 +public class KernelSettings +{ + public string ServiceType { get; set; } = string.Empty; + public string ServiceId { get; set; } = string.Empty; + public string DeploymentOrModelId { get; set; } = string.Empty; + public string Endpoint { get; set; } = string.Empty; + public string OrgId { get; set; } = string.Empty; + public LogLevel? LogLevel { get; set; } + public string ApiKey { get; set; } = string.Empty; +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/ServiceTypes.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/ServiceTypes.cs new file mode 100644 index 0000000000..2c671828cb --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Models/ServiceTypes.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Models; + +internal static class ServiceTypes +{ + internal const string OpenAI = "OPENAI"; + internal const string AzureOpenAI = "AZUREOPENAI"; +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Plugins/MathPlugin.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Plugins/MathPlugin.cs new file mode 100644 index 0000000000..a0b6bfa7c3 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Plugins/MathPlugin.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using Microsoft.SemanticKernel; + +namespace Plugins; + +public sealed class MathPlugin +{ + [KernelFunction, Description("Take the square root of a number")] + public static double Sqrt( + [Description("The number to take a square root of")] double number1 + ) + { + return Math.Sqrt(number1); + } + + [KernelFunction, Description("Add two numbers")] + public static double Add( + [Description("The first number to add")] double number1, + [Description("The second number to add")] double number2 + ) + { + return number1 + number2; + } + + [KernelFunction, Description("Subtract two numbers")] + public static double Subtract( + [Description("The first number to subtract from")] double number1, + [Description("The second number to subtract away")] double number2 + ) + { + return number1 - number2; + } + + [KernelFunction, Description("Multiply two numbers. When increasing by a percentage, don't forget to add 1 to the percentage.")] + public static double Multiply( + [Description("The first number to multiply")] double number1, + [Description("The second number to multiply")] double number2 + ) + { + return number1 * number2; + } + + [KernelFunction, Description("Divide two numbers")] + public static double Divide( + [Description("The first number to divide from")] double number1, + [Description("The second number to divide by")] double number2 + ) + { + return number1 / number2; + } + + [KernelFunction, Description("Raise a number to a power")] + public static double Power( + [Description("The number to raise")] double number1, + [Description("The power to raise the number to")] double number2 + ) + { + return Math.Pow(number1, number2); + } + + [KernelFunction, Description("Take the log of a number")] + public static double Log( + [Description("The number to take the log of")] double number1, + [Description("The base of the log")] double number2 + ) + { + return Math.Log(number1, number2); + } + + [KernelFunction, Description("Round a number to the target number of decimal places")] + public static double Round( + [Description("The number to round")] double number1, + [Description("The number of decimal places to round to")] double number2 + ) + { + return Math.Round(number1, (int)number2); + } + + [KernelFunction, Description("Take the absolute value of a number")] + public static double Abs( + [Description("The number to take the absolute value of")] double number1 + ) + { + return Math.Abs(number1); + } + + [KernelFunction, Description("Take the floor of a number")] + public static double Floor( + [Description("The number to take the floor of")] double number1 + ) + { + return Math.Floor(number1); + } + + [KernelFunction, Description("Take the ceiling of a number")] + public static double Ceiling( + [Description("The number to take the ceiling of")] double number1 + ) + { + return Math.Ceiling(number1); + } + + [KernelFunction, Description("Take the sine of a number")] + public static double Sin( + [Description("The number to take the sine of")] double number1 + ) + { + return Math.Sin(number1); + } + + [KernelFunction, Description("Take the cosine of a number")] + public static double Cos( + [Description("The number to take the cosine of")] double number1 + ) + { + return Math.Cos(number1); + } + + [KernelFunction, Description("Take the tangent of a number")] + public static double Tan( + [Description("The number to take the tangent of")] double number1 + ) + { + return Math.Tan(number1); + } + + [KernelFunction, Description("Take the arcsine of a number")] + public static double Asin( + [Description("The number to take the arcsine of")] double number1 + ) + { + return Math.Asin(number1); + } + + [KernelFunction, Description("Take the arccosine of a number")] + public static double Acos( + [Description("The number to take the arccosine of")] double number1 + ) + { + return Math.Acos(number1); + } + + [KernelFunction, Description("Take the arctangent of a number")] + public static double Atan( + [Description("The number to take the arctangent of")] double number1 + ) + { + return Math.Atan(number1); + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Program.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Program.cs new file mode 100644 index 0000000000..7edc8e78c0 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Program.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations; +using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using Microsoft.SemanticKernel; +using Models; +using Plugins; +using Plugins.AzureFunctions.Extensions; + +const string DefaultSemanticFunctionsFolder = "Prompts"; +string semanticFunctionsFolder = Environment.GetEnvironmentVariable("SEMANTIC_SKILLS_FOLDER") ?? DefaultSemanticFunctionsFolder; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureAppConfiguration(configuration => + { + var config = configuration.SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true); + var builtConfig = config.Build(); + }) + .ConfigureServices((context, services) => + { + services.Configure(options => + { + // `ConfigureFunctionsWorkerDefaults` sets the default to ignore casing already. + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + }); + + services.AddSingleton(_ => + { + var options = new OpenApiConfigurationOptions() + { + Info = new OpenApiInfo() + { + Version = "1.0.0", + Title = "My Plugin", + Description = "This plugin does..." + }, + Servers = DefaultOpenApiConfigurationOptions.GetHostNames(), + OpenApiVersion = OpenApiVersionType.V3, + //IncludeRequestingHostName = true, + ForceHttps = false, + ForceHttp = false, + }; + + return options; + }); + services + .AddTransient((providers) => + { + var appSettings = AppSettings.LoadSettings(); + var builder = Kernel.CreateBuilder(); + builder.Services.WithChatCompletionService(appSettings.Kernel); + builder.Services.AddLogging(loggingBuilder => + { + loggingBuilder.AddFilter(level => true); + loggingBuilder.AddConsole(); + }); + builder.Plugins.AddFromType(); + return builder.Build(); + }) + .AddScoped(); + }) + .Build(); + +host.Run(); diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Prompts/GetLogicalValue/config.json b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Prompts/GetLogicalValue/config.json new file mode 100644 index 0000000000..602efd48b2 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Prompts/GetLogicalValue/config.json @@ -0,0 +1,26 @@ +{ + "schema": 1, + "description": "If the request does not provide an explicit number for a parameter, use this function to make up a logical value; do not make up a value yourself.", + "type": "completion", + "completion": { + "max_tokens": 1000, + "temperature": 0.9, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + }, + "input": { + "parameters": [ + { + "name": "input", + "description": "A detailed description (2-3 sentences) of the missing value; provide all the context the user provided to get the best results.", + "defaultValue": "" + }, + { + "name": "units", + "description": "The units used to measure the value (e.g., 'meters', 'seconds', 'dollars', etc.). (required)", + "defaultValue": "" + } + ] + } +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Prompts/GetLogicalValue/skprompt.txt b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Prompts/GetLogicalValue/skprompt.txt new file mode 100644 index 0000000000..baa85e422a --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/Prompts/GetLogicalValue/skprompt.txt @@ -0,0 +1,18 @@ +INSTURCTIONS: +Provide a realistic value for the missing parameter. If you don't know the answer, provide a best guess using the limited information provided. +Do not give a range of values. Do not give a value that is not realistic. Do not give a value that is not possible. + +OUTPUT FORMAT: +{ + "value": "", + "reason": "", + "units": "", +} + +MISSING PARAMETER DESCRIPTION: +{{$input}} + +PARAMETER UNITS: +{{$units}} + +ANSWER: diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/azure-function.sln b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/azure-function.sln new file mode 100644 index 0000000000..ebbca6abe6 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/azure-function.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sk-chatgpt-azure-function", "sk-chatgpt-azure-function.csproj", "{20F608E8-E748-4432-B673-7D98782A6E23}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PluginShared", "shared\PluginShared.csproj", "{82A779FB-9CFD-4159-8AFC-54D00C76947C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {20F608E8-E748-4432-B673-7D98782A6E23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20F608E8-E748-4432-B673-7D98782A6E23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20F608E8-E748-4432-B673-7D98782A6E23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20F608E8-E748-4432-B673-7D98782A6E23}.Release|Any CPU.Build.0 = Release|Any CPU + {82A779FB-9CFD-4159-8AFC-54D00C76947C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82A779FB-9CFD-4159-8AFC-54D00C76947C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82A779FB-9CFD-4159-8AFC-54D00C76947C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82A779FB-9CFD-4159-8AFC-54D00C76947C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {76F2FDDE-51A7-4934-BEB6-2D6F082412BD} + EndGlobalSection +EndGlobal diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/config-samples/appsettings.json.azure-example b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/config-samples/appsettings.json.azure-example new file mode 100644 index 0000000000..ff4f1529a2 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/config-samples/appsettings.json.azure-example @@ -0,0 +1,25 @@ +{ + "kernel": { + "serviceType": "AzureOpenAI", + "serviceId": "gpt-35-turbo", + "deploymentOrModelId": "gpt-35-turbo", + "endpoint": "https:// ... your endpoint ... .openai.azure.com/" + }, + "aiPlugin": { + "schemaVersion": "v1", + "nameForModel": "MyPlugin", + "nameForHuman": "My Plugin", + "descriptionForModel": "", + "descriptionForHuman": "", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "{url}/swagger.json" + }, + "logoUrl": "{url}/logo.png", + "contactEmail": "support@example.com", + "legalInfoUrl": "http://www.example.com/legal" + } +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/config-samples/appsettings.json.openai-example b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/config-samples/appsettings.json.openai-example new file mode 100644 index 0000000000..b81fc7da29 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/config-samples/appsettings.json.openai-example @@ -0,0 +1,25 @@ +{ + "kernel": { + "serviceType": "OpenAI", + "serviceId": "gpt-3.5-turbo", + "deploymentOrModelId": "gpt-3.5-turbo", + "orgId": "" + }, + "aiPlugin": { + "schemaVersion": "v1", + "nameForModel": "MyPlugin", + "nameForHuman": "My Plugin", + "descriptionForModel": "", + "descriptionForHuman": "", + "auth": { + "type": "none" + }, + "api": { + "type": "openapi", + "url": "{url}/swagger.json" + }, + "logoUrl": "{url}/logo.png", + "contactEmail": "support@example.com", + "legalInfoUrl": "http://www.example.com/legal" + } +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/host.json b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/host.json new file mode 100644 index 0000000000..5ae5a72c78 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/host.json @@ -0,0 +1,16 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "http": { + "routePrefix": "" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/local.settings.json.example b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/local.settings.json.example new file mode 100644 index 0000000000..63158c5c94 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/local.settings.json.example @@ -0,0 +1,10 @@ +{ + "IsEncrypted": false, + "Host": { + "CORS": "*" + }, + "Values": { + "apiKey": "", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" + } +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/logo.png b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/logo.png new file mode 100644 index 0000000000..6f4b5cc5b9 Binary files /dev/null and b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/logo.png differ diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginApi.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginApi.cs new file mode 100644 index 0000000000..421d50b029 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginApi.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace skchatgptazurefunction.PluginShared; + +/// +/// This class represents the plugin API specification. +/// +public class PluginApi +{ + /// + /// The API specification + /// + public string Type { get; set; } = "openapi"; + + /// + /// URL used to fetch the specification + /// +#pragma warning disable CA1056 // URI-like properties should not be strings + public string Url { get; set; } = string.Empty; +#pragma warning restore CA1056 // URI-like properties should not be strings +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginAuth.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginAuth.cs new file mode 100644 index 0000000000..4b789b4ed6 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginAuth.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace skchatgptazurefunction.PluginShared; + +/// +/// This class represents the OpenAI plugin authentication schema. +/// +public class PluginAuth +{ + /// + /// Tokens for API key authentication + /// + public class VerificationTokens + { + /// + /// The API key + /// + public string OpenAI { get; set; } = string.Empty; + } + + /// + /// The authentication schema + /// Supported values: none, service_http, user_http + /// + public string Type { get; set; } = "none"; + + /// + /// Manifest schema version + /// + [JsonPropertyName("authorization_type")] + public string AuthorizationType { get; } = "bearer"; + + /// + /// Tokens for API key authentication + /// + [JsonPropertyName("verification_tokens")] + public VerificationTokens Tokens { get; set; } = new VerificationTokens(); +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginManifest.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginManifest.cs new file mode 100644 index 0000000000..6fee4d5f8e --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginManifest.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace skchatgptazurefunction.PluginShared; + +/// +/// This class represents the OpenAI plugin manifest: +/// https://platform.openai.com/docs/plugins/getting-started/plugin-manifest +/// +public class PluginManifest +{ + /// + /// Manifest schema version + /// + [JsonPropertyName("schema_version")] + public string SchemaVersion { get; set; } = "v1"; + + /// + /// The name of the plugin that the model will use + /// + [JsonPropertyName("name_for_model")] + public string NameForModel { get; set; } = string.Empty; + + /// + /// Human-readable name of the plugin + /// + [JsonPropertyName("name_for_human")] + public string NameForHuman { get; set; } = string.Empty; + + /// + /// Description of the plugin that the model will use + /// + [JsonPropertyName("description_for_model")] + public string DescriptionForModel { get; set; } = string.Empty; + + /// + /// Human-readable description of the plugin + /// + [JsonPropertyName("description_for_human")] + public string DescriptionForHuman { get; set; } = string.Empty; + + /// + /// The authentication schema + /// + public PluginAuth Auth { get; set; } = new PluginAuth(); + + /// + /// The API specification + /// + public PluginApi Api { get; set; } = new PluginApi(); + +#pragma warning disable CA1056 // URI-like properties should not be strings + /// + /// Redirect URL for users to get more information about the plugin + /// + [JsonPropertyName("legal_info_url")] + public string LegalInfoUrl { get; set; } = string.Empty; + + /// + /// URL used to fetch the logo + /// + [JsonPropertyName("logo_url")] + public string LogoUrl { get; set; } = string.Empty; +#pragma warning restore CA1056 // URI-like properties should not be strings + + /// + /// Email contact for safety/moderation, support and deactivation + /// + [JsonPropertyName("contact_email")] + public string ContactEmail { get; set; } = string.Empty; + + /// + /// "Bearer" or "Basic" + /// + public string HttpAuthorizationType { get; set; } = string.Empty; +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginShared.csproj b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginShared.csproj new file mode 100644 index 0000000000..fa0c17bf20 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/shared/PluginShared.csproj @@ -0,0 +1,10 @@ + + + + net6.0 + 10 + enable + skchatgptazurefunction.PluginShared + + + \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj new file mode 100644 index 0000000000..7ea6c27ad1 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj @@ -0,0 +1,68 @@ + + + + $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) + d733072a-9c0f-473c-a99b-6e12f7b5ef28 + + + + net6.0 + skchatgptazurefunction + v4 + <_FunctionsSkipCleanOutput>true + Exe + 10 + enable + enable + false + + + + false + false + + + + + + + + + + + + + + + + .generated\%(RecursiveDir)%(Filename)%(Extension) + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + PreserveNewest + + + PreserveNewest + + + + + + PreserveNewest + + + + + + + diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/Extensions/GeneratorExecutionContextExtensions.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/Extensions/GeneratorExecutionContextExtensions.cs new file mode 100644 index 0000000000..29854dbb38 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/Extensions/GeneratorExecutionContextExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.CodeAnalysis; + +namespace Plugins.AzureFunctions.Generator.Extensions; + +internal static class GeneratorExecutionContextExtensions +{ + public static string? GetMSBuildProperty( + this GeneratorExecutionContext context, + string name, + string defaultValue = "") + { + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); + return value ?? defaultValue; + } + + public static string? GetRootNamespace(this GeneratorExecutionContext context) + { + return context.GetMSBuildProperty("RootNamespace"); + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/KernelFunctionGenerator.cs b/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/KernelFunctionGenerator.cs new file mode 100644 index 0000000000..9ae83cf563 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/KernelFunctionGenerator.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Plugins.AzureFunctions.Generator; + +/// +/// Generates kernel functions +/// +[Generator] +public class KernelFunctionGenerator : ISourceGenerator +{ + /// + /// Generates kernel functions + /// + public void Execute(GeneratorExecutionContext context) + { + var functionDetailsByPlugin = new Dictionary>(); + + foreach (var syntaxTree in context.Compilation.SyntaxTrees) + { + var semanticModel = context.Compilation.GetSemanticModel(syntaxTree); + var root = syntaxTree.GetRoot(); + + var configureServicesCalls = root.DescendantNodes() + .OfType() + .Where(ies => ies.Expression is MemberAccessExpressionSyntax maes && maes.Name.ToString() == "AddTransient"); + + foreach (var configureServicesCall in configureServicesCalls) + { + // Analyze within ConfigureServices + foreach (var invocation in configureServicesCall.DescendantNodes().OfType()) + { + var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; + if (symbol?.ContainingType.ToString() == "Microsoft.SemanticKernel.KernelExtensions") + { + INamedTypeSymbol? pluginTypeArgument = null; + if (symbol.Name == "AddFromType") + { + pluginTypeArgument = symbol.TypeArguments.FirstOrDefault() as INamedTypeSymbol; + } + else if (symbol.Name == "AddFromObject") + { + var objectCreationExpression = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression as ObjectCreationExpressionSyntax; + if (objectCreationExpression != null) + { + var typeInfo = semanticModel.GetTypeInfo(objectCreationExpression); + pluginTypeArgument = typeInfo.Type as INamedTypeSymbol; + } + } + + if (pluginTypeArgument != null && configureServicesCall.Expression is MemberAccessExpressionSyntax maes) + { + var pluginName = pluginTypeArgument.Name; + var functionDetails = this.ExtractFunctionDetails(context, pluginTypeArgument); + functionDetailsByPlugin[pluginName] = functionDetails; + } + } + } + } + } + + // Generate source for each plugin + foreach (var pluginEntry in functionDetailsByPlugin) + { + var sourceCode = GenerateClassSource("AzureFunctionPlugins", pluginEntry.Key, pluginEntry.Value); + context.AddSource($"{pluginEntry.Key}.g.cs", sourceCode); + } + } + + private List ExtractFunctionDetails(GeneratorExecutionContext context, INamedTypeSymbol pluginClass) + { + var functionDetailsList = new List(); + + foreach (var member in pluginClass.GetMembers()) + { + if (member is IMethodSymbol methodSymbol && methodSymbol.GetAttributes().Any(attr => attr?.AttributeClass?.Name == "KernelFunctionAttribute")) + { + var functionDetails = new FunctionDetails + { + Name = methodSymbol.Name, + Description = methodSymbol.GetAttributes().FirstOrDefault(a => a?.AttributeClass?.Name == "DescriptionAttribute")?.ConstructorArguments.FirstOrDefault().Value.ToString(), + Parameters = new List() + }; + + foreach (var parameter in methodSymbol.Parameters) + { + var parameterDetails = new ParameterDetails + { + Name = parameter.Name, + Type = parameter.Type.ToString(), + Description = parameter.GetAttributes().FirstOrDefault(a => a?.AttributeClass?.Name == "DescriptionAttribute")?.ConstructorArguments.FirstOrDefault().Value.ToString() + }; + + functionDetails.Parameters.Add(parameterDetails); + } + + functionDetailsList.Add(functionDetails); + } + } + + return functionDetailsList; + } + + // Generate the source code for a folder of prompts + private static string GenerateClassSource(string rootNamespace, string pluginName, List functions) + { + StringBuilder functionsCode = new(); + + foreach (var function in functions) + { + functionsCode.AppendLine(GenerateFunctionSource(pluginName, function) ?? string.Empty); + } + + return $@"/* ### GENERATED CODE - Do not modify. Edits will be lost on build. ### */ + using System; + using System.Net; + using System.Reflection; + using System.Threading.Tasks; + using Microsoft.Azure.Functions.Worker; + using Microsoft.Azure.Functions.Worker.Http; + using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes; + using Microsoft.Extensions.Logging; + using Microsoft.OpenApi.Models; + using Microsoft.SemanticKernel; + using Plugins.AzureFunctions.Extensions; + + namespace {rootNamespace}; + + public class {pluginName} + {{ + private readonly ILogger _logger; + private readonly AIPluginRunner _pluginRunner; + + public {pluginName}(AIPluginRunner pluginRunner, ILoggerFactory loggerFactory) + {{ + _pluginRunner = pluginRunner; + _logger = loggerFactory.CreateLogger<{pluginName}>(); + }} + + {functionsCode} + }}"; + } + + private static string? GenerateFunctionSource(string pluginName, FunctionDetails functionDetails) + { + string modelClassName = $"{functionDetails.Name}Model"; // Name of the model class + string parameterAttributes = GenerateModelClassSource(modelClassName, functionDetails.Parameters); + + return $@" + {parameterAttributes} + + [OpenApiOperation(operationId: ""{functionDetails.Name}"", tags: new[] {{ ""{functionDetails.Name}"" }})] + [OpenApiRequestBody(contentType: ""application/json"", bodyType: typeof({modelClassName}), Required = true, Description = ""JSON request body"")] + [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: ""application/json"", bodyType: typeof(string), Description = ""The OK response"")] + [Function(""{functionDetails.Name}"")] + public async Task {functionDetails.Name}([HttpTrigger(AuthorizationLevel.Anonymous, ""post"")] HttpRequestData req) + {{ + _logger.LogInformation(""HTTP trigger processed a request for function {pluginName}-{functionDetails.Name}.""); + return await _pluginRunner.RunAIPluginOperationAsync<{modelClassName}>(req, ""{pluginName}"", ""{functionDetails.Name}""); + }}"; + } + + private static string GenerateModelClassSource(string modelClassName, List parameters) + { + StringBuilder modelClassBuilder = new(); + + modelClassBuilder.AppendLine($"public class {modelClassName}"); + modelClassBuilder.AppendLine("{"); + + foreach (var parameter in parameters) + { + modelClassBuilder.AppendLine($" public {parameter.Type} {parameter.Name} {{ get; set; }}"); + } + + modelClassBuilder.AppendLine("}"); + + return modelClassBuilder.ToString(); + } + + /// + /// Does nothing + /// + public void Initialize(GeneratorInitializationContext context) + { + // No initialization required + } +} + +/// +/// Function details +/// +public class FunctionDetails +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Parameters { get; set; } = new List(); +} + +/// +/// Parameter details +/// +public class ParameterDetails +{ + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} diff --git a/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/kernel-functions-generator.csproj b/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/kernel-functions-generator.csproj new file mode 100644 index 0000000000..274d4eb52e --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/MathPlugin/kernel-functions-generator/kernel-functions-generator.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + 10 + enable + true + RS1035,CS0612,CS1591,CS8601,CS8602,CS860218 + Plugins.AzureFunctions.Generator + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/README.md b/dotnet/samples/CreateChatGptPlugin/README.md new file mode 100644 index 0000000000..3394ad2b16 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/README.md @@ -0,0 +1,76 @@ +# Creating and using a OpenAI plugin + +## Prerequisites + +- [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools) version 4.x. +- [.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) is required to run this sample. +- Install the recommended extensions +- [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) +- [Semantic Kernel Tools](https://marketplace.visualstudio.com/items?itemName=ms-semantic-kernel.semantic-kernel) (optional) + +You must also have the Azure Function located [here](./MathPlugin/) running locally, otherwise the sample will fail. + +## Configuring the sample + +The sample can be configured by using the command line with .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) to avoid the risk of leaking secrets into the repository, branches and pull requests. + +This sample has been tested with the following models: + +| Service | Model type | Model | Model version | Supported | +| ------------ | --------------- | ---------------- | ------------: | --------- | +| OpenAI | Text Completion | text-davinci-003 | 1 | ❌ | +| OpenAI | Chat Completion | gpt-3.5-turbo | 1 | ❌ | +| OpenAI | Chat Completion | gpt-3.5-turbo | 0301 | ❌ | +| Azure OpenAI | Chat Completion | gpt-3.5-turbo | 0613 | ✅ | +| Azure OpenAI | Chat Completion | gpt-3.5-turbo | 1106 | ✅ | +| OpenAI | Chat Completion | gpt-4 | 1 | ❌ | +| OpenAI | Chat Completion | gpt-4 | 0314 | ❌ | +| Azure OpenAI | Chat Completion | gpt-4 | 0613 | ✅ | +| Azure OpenAI | Chat Completion | gpt-4 | 1106 | ✅ | + +This sample uses function calling, so it only works on models newer than 0613. + +### Using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) + +Configure an OpenAI endpoint + +```powershell +cd 14-Create-ChatGPT-Plugin/Solution + +dotnet user-secrets set "Global:LlmService" "OpenAI" + +dotnet user-secrets set "OpenAI:ModelType" "chat-completion" +dotnet user-secrets set "OpenAI:ChatCompletionModelId" "gpt-4" +dotnet user-secrets set "OpenAI:ApiKey" "... your OpenAI key ..." +dotnet user-secrets set "OpenAI:OrgId" "... your ord ID ..." +``` + +Configure an Azure OpenAI endpoint + +```powershell +cd 14-Create-ChatGPT-Plugin/Solution + +dotnet user-secrets set "Global:LlmService" "AzureOpenAI" + +dotnet user-secrets set "AzureOpenAI:DeploymentType" "chat-completion" +dotnet user-secrets set "AzureOpenAI:ChatCompletionDeploymentName" "gpt-35-turbo" +dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-3.5-turbo-0613" +dotnet user-secrets set "AzureOpenAI:Endpoint" "... your Azure OpenAI endpoint ..." +dotnet user-secrets set "AzureOpenAI:ApiKey" "... your Azure OpenAI key ..." +``` + +## Running the sample + +First, refer to the [README](./MathPlugin/README.md) in the MathPlugin\ folder +to start the Azure Function. + +After starting the Azure Function and configuring the sample, +to build and run the console application, navigate to the [Solution](./Solution/) folder and hit `F5`. + +To build and run the console application from the terminal use the following commands: + +```powershell +cd Solution +dotnet build +dotnet run +``` diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/.gitignore b/dotnet/samples/CreateChatGptPlugin/Solution/.gitignore new file mode 100644 index 0000000000..7e5351290b --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/.gitignore @@ -0,0 +1,7 @@ +appsettings.json +bin +obj +*.ini +*.cache +*.log +*.tmp diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/extensions.json b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/extensions.json new file mode 100644 index 0000000000..00de051b99 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-dotnettools.csharp", + "ms-semantic-kernel.semantic-kernel" + ] +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/launch.json b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/launch.json new file mode 100644 index 0000000000..85ab19de82 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net8.0/14-Create-ChatGPT-Plugin.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/settings.json b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/settings.json new file mode 100644 index 0000000000..440f8aa893 --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/settings.json @@ -0,0 +1,84 @@ +{ + "prettier.enable": true, + "css.lint.validProperties": [ + "composes" + ], + "editor.defaultFormatter": "ms-dotnettools.csharp", + "editor.formatOnType": true, + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit" + }, + "[csharp]": { + "editor.defaultFormatter": "ms-dotnettools.csharp" + }, + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active", + "python.formatting.provider": "autopep8", + "python.formatting.autopep8Args": [ + "--max-line-length=120" + ], + "notebook.output.textLineLimit": 500, + "python.analysis.extraPaths": [ + "./python/src" + ], + "javascript.updateImportsOnFileMove.enabled": "always", + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/build": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll": true + } + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.organizeImports": true, + "source.fixAll": true + } + }, + "typescript.updateImportsOnFileMove.enabled": "always", + "eslint.enable": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "eslint.lintTask.enable": true, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "eslint.options": { + "overrideConfigFile": ".eslintrc.js" + }, + "eslint.packageManager": "yarn", + "files.associations": { + "*.json": "jsonc" + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "cSpell.words": [ + "Partitioner" + ], + "[typescript][typescriptreact]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll": "explicit" + } + } + } \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/tasks.json b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/tasks.json new file mode 100644 index 0000000000..d8ca29a9bc --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/14-Create-ChatGPT-Plugin.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/14-Create-ChatGPT-Plugin.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/14-Create-ChatGPT-Plugin.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj b/dotnet/samples/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj new file mode 100644 index 0000000000..338d4dd18d --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj @@ -0,0 +1,29 @@ + + + + Exe + net6.0 + + enable + enable + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + false + SKEXP0042 + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/Program.cs b/dotnet/samples/CreateChatGptPlugin/Solution/Program.cs new file mode 100644 index 0000000000..ace962fe2a --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/Program.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Plugins.OpenApi; + +// Create kernel +var builder = Kernel.CreateBuilder(); +// Add a text or chat completion service using either: +// builder.Services.AddAzureOpenAIChatCompletion() +// builder.Services.AddAzureOpenAITextGeneration() +// builder.Services.AddOpenAIChatCompletion() +// builder.Services.AddOpenAITextGeneration() +builder.WithCompletionService(); +var kernel = builder.Build(); + +// Add the math plugin using the plugin manifest URL +await kernel.ImportPluginFromOpenApiAsync("MathPlugin", new Uri("http://localhost:7071/swagger.json")).ConfigureAwait(false); + +// Create chat history +ChatHistory history = new(); + +// Get chat completion service +var chatCompletionService = kernel.GetRequiredService(); + +// Start the conversation +while (true) +{ + // Get user input + Console.Write("User > "); + history.AddUserMessage(Console.ReadLine()!); + + // Enable auto function calling + OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }; + + // 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); +} diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/config/Env.cs b/dotnet/samples/CreateChatGptPlugin/Solution/config/Env.cs new file mode 100644 index 0000000000..1f2836f7bb --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/config/Env.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Configuration; + +#pragma warning disable CA1812 // instantiated by AddUserSecrets +internal sealed class Env +#pragma warning restore CA1812 +{ + /// + /// Simple helper used to load env vars and secrets like credentials, + /// to avoid hard coding them in the sample code + /// + /// Secret name / Env var name + /// Value found in Secret Manager or Environment Variable + internal static string? Var(string name) + { + var configuration = new ConfigurationBuilder() + .AddUserSecrets() + .Build(); + + var value = configuration[name]; + if (!string.IsNullOrEmpty(value)) + { + return value; + } + + value = Environment.GetEnvironmentVariable(name); + + return value; + } +} diff --git a/dotnet/samples/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs b/dotnet/samples/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs new file mode 100644 index 0000000000..21fc499fef --- /dev/null +++ b/dotnet/samples/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; + +internal static class KernelBuilderExtensions +{ + /// + /// Adds a text completion service to the list. It can be either an OpenAI or Azure OpenAI backend service. + /// + /// + /// + internal static IKernelBuilder WithCompletionService(this IKernelBuilder kernelBuilder) + { + switch (Env.Var("Global:LlmService")!) + { + case "AzureOpenAI": + if (Env.Var("AzureOpenAI:DeploymentType") == "text-completion") + { + kernelBuilder.Services.AddAzureOpenAITextGeneration( + deploymentName: Env.Var("AzureOpenAI:TextCompletionDeploymentName")!, + modelId: Env.Var("AzureOpenAI:TextCompletionModelId")!, + endpoint: Env.Var("AzureOpenAI:Endpoint")!, + apiKey: Env.Var("AzureOpenAI:ApiKey")! + ); + } + else if (Env.Var("AzureOpenAI:DeploymentType") == "chat-completion") + { + kernelBuilder.Services.AddAzureOpenAIChatCompletion( + deploymentName: Env.Var("AzureOpenAI:ChatCompletionDeploymentName")!, + modelId: Env.Var("AzureOpenAI:ChatCompletionModelId")!, + endpoint: Env.Var("AzureOpenAI:Endpoint")!, + apiKey: Env.Var("AzureOpenAI:ApiKey")! + ); + } + break; + + case "OpenAI": + if (Env.Var("OpenAI:ModelType") == "text-completion") + { + kernelBuilder.Services.AddOpenAITextGeneration( + modelId: Env.Var("OpenAI:TextCompletionModelId")!, + apiKey: Env.Var("OpenAI:ApiKey")!, + orgId: Env.Var("OpenAI:OrgId") + ); + } + else if (Env.Var("OpenAI:ModelType") == "chat-completion") + { + kernelBuilder.Services.AddOpenAIChatCompletion( + modelId: Env.Var("OpenAI:ChatCompletionModelId")!, + apiKey: Env.Var("OpenAI:ApiKey")!, + orgId: Env.Var("OpenAI:OrgId") + ); + } + break; + + default: + throw new ArgumentException($"Invalid service type value: {Env.Var("OpenAI:ModelType")}"); + } + + return kernelBuilder; + } +} diff --git a/dotnet/samples/DocumentationExamples/AIServices.cs b/dotnet/samples/DocumentationExamples/AIServices.cs new file mode 100644 index 0000000000..1975c278e3 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/AIServices.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to add AI services to a kernel as described at +/// https://learn.microsoft.com/semantic-kernel/agents/kernel/adding-services +/// +public class AIServices : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== AI Services ========"); + + string? endpoint = TestConfiguration.AzureOpenAI.Endpoint; + string? modelId = TestConfiguration.AzureOpenAI.ChatModelId; + string? textModelId = TestConfiguration.AzureOpenAI.ChatModelId; + string? apiKey = TestConfiguration.AzureOpenAI.ApiKey; + + if (endpoint is null || modelId is null || textModelId is null || apiKey is null) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + string? openAImodelId = TestConfiguration.OpenAI.ChatModelId; + string? openAItextModelId = TestConfiguration.OpenAI.ChatModelId; + string? openAIapiKey = TestConfiguration.OpenAI.ApiKey; + + if (openAImodelId is null || openAItextModelId is null || openAIapiKey is null) + { + WriteLine("OpenAI credentials not found. Skipping example."); + + return; + } + + // Create a kernel with an Azure OpenAI chat completion service + // + Kernel kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey) + .Build(); + // + + // You could instead create a kernel with a legacy Azure OpenAI text completion service + // + kernel = Kernel.CreateBuilder() + .AddAzureOpenAITextGeneration(textModelId, endpoint, apiKey) + .Build(); + // + + // You can also create a kernel with a (non-Azure) OpenAI chat completion service + // + kernel = Kernel.CreateBuilder() + .AddOpenAIChatCompletion(openAImodelId, openAIapiKey) + .Build(); + // + + // Or a kernel with a legacy OpenAI text completion service + // + kernel = Kernel.CreateBuilder() + .AddOpenAITextGeneration(openAItextModelId, openAIapiKey) + .Build(); + // + } + + public AIServices(ITestOutputHelper output) : base(output) + { + } +} diff --git a/dotnet/samples/DocumentationExamples/BaseTest.cs b/dotnet/samples/DocumentationExamples/BaseTest.cs new file mode 100644 index 0000000000..4017d80066 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/BaseTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Xunit.Abstractions; + +namespace Examples; + +public abstract class BaseTest +{ + protected ITestOutputHelper Output { get; } + + protected List SimulatedInputText = new(); + protected int SimulatedInputTextIndex = 0; + + protected BaseTest(ITestOutputHelper output) + { + this.Output = output; + LoadUserSecrets(); + } + + private static void LoadUserSecrets() + { + IConfigurationRoot configRoot = new ConfigurationBuilder() + .AddJsonFile("appsettings.Development.json", true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + TestConfiguration.Initialize(configRoot); + } + + /// + /// This method can be substituted by Console.WriteLine when used in Console apps. + /// + /// Target object to write + protected void WriteLine(object? target = null) + { + this.Output.WriteLine(target?.ToString() ?? string.Empty); + } + + /// + /// Current interface ITestOutputHelper does not have a Write method. This extension method adds it to make it analogous to Console.Write when used in Console apps. + /// + /// Target object to write + protected void Write(object? target = null) + { + this.Output.WriteLine(target?.ToString() ?? string.Empty); + } + + /// + /// Simulates reading input strings from a user for the purpose of running tests. + /// + /// A simulate user input string, if available. Null otherwise. + protected string? ReadLine() + { + if (SimulatedInputTextIndex < SimulatedInputText.Count) + { + return SimulatedInputText[SimulatedInputTextIndex++]; + } + + return null; + } +} diff --git a/dotnet/samples/DocumentationExamples/ConfiguringPrompts.cs b/dotnet/samples/DocumentationExamples/ConfiguringPrompts.cs new file mode 100644 index 0000000000..8802210f9d --- /dev/null +++ b/dotnet/samples/DocumentationExamples/ConfiguringPrompts.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Plugins.Core; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to configure prompts as described at +/// https://learn.microsoft.com/semantic-kernel/prompts/configure-prompts +/// +public class ConfiguringPrompts : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Configuring Prompts ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); + builder.Plugins.AddFromType(); + Kernel kernel = builder.Build(); + + // + // Create a template for chat with settings + var chat = kernel.CreateFunctionFromPrompt( + new PromptTemplateConfig() + { + Name = "Chat", + Description = "Chat with the assistant.", + Template = @"{{ConversationSummaryPlugin.SummarizeConversation $history}} + User: {{$request}} + Assistant: ", + TemplateFormat = "semantic-kernel", + InputVariables = new List() + { + new() { Name = "history", Description = "The history of the conversation.", IsRequired = false, Default = "" }, + new() { Name = "request", Description = "The user's request.", IsRequired = true } + }, + ExecutionSettings = + { + { + "default", + new OpenAIPromptExecutionSettings() + { + MaxTokens = 1000, + Temperature = 0 + } + }, + { + "gpt-3.5-turbo", new OpenAIPromptExecutionSettings() + { + ModelId = "gpt-3.5-turbo-0613", + MaxTokens = 4000, + Temperature = 0.2 + } + }, + { + "gpt-4", + new OpenAIPromptExecutionSettings() + { + ModelId = "gpt-4-1106-preview", + MaxTokens = 8000, + Temperature = 0.3 + } + } + } + } + ); + // + + // Create chat history and choices + ChatHistory history = new(); + + // Start the chat loop + Write("User > "); + string? userInput; + while ((userInput = ReadLine()) != null) + { + // Get chat response + var chatResult = kernel.InvokeStreamingAsync( + chat, + new() + { + { "request", userInput }, + { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) } + } + ); + + // Stream the response + string message = ""; + await foreach (var chunk in chatResult) + { + if (chunk.Role.HasValue) + { + Write(chunk.Role + " > "); + } + message += chunk; + Write(chunk); + } + WriteLine(); + + // Append to history + history.AddUserMessage(userInput); + history.AddAssistantMessage(message); + + // Get user input again + Write("User > "); + } + } + + public ConfiguringPrompts(ITestOutputHelper output) : base(output) + { + SimulatedInputText = ["Who were the Vikings?"]; + } +} diff --git a/dotnet/samples/DocumentationExamples/CreatingFunctions.cs b/dotnet/samples/DocumentationExamples/CreatingFunctions.cs new file mode 100644 index 0000000000..80f0024041 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/CreatingFunctions.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Plugins; +using Xunit; +using Xunit.Abstractions; + +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 CreatingFunctions : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Creating native functions ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + // + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); + builder.Plugins.AddFromType(); + Kernel kernel = builder.Build(); + + // Test the math plugin + double answer = await kernel.InvokeAsync( + "MathPlugin", "Sqrt", new() + { + { "number1", 12 } + }); + WriteLine($"The square root of 12 is {answer}."); + // + + // Create chat history + ChatHistory history = new(); + + // + + // Get chat completion service + var chatCompletionService = kernel.GetRequiredService(); + + // Start the conversation + Write("User > "); + string? userInput; + while ((userInput = ReadLine()) != null) + { + history.AddUserMessage(userInput); + + // Enable auto function calling + OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }; + + // 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) + { + Write("Assistant > "); + first = false; + } + Write(content.Content); + fullMessage += content.Content; + } + WriteLine(); + + // Add the message from the agent to the chat history + history.AddAssistantMessage(fullMessage); + + // Get user input again + Write("User > "); + } + + // + } + + public CreatingFunctions(ITestOutputHelper output) : base(output) + { + SimulatedInputText = ["What is 49 diivided by 37?"]; + } +} diff --git a/dotnet/samples/DocumentationExamples/DocumentationExamples.csproj b/dotnet/samples/DocumentationExamples/DocumentationExamples.csproj new file mode 100644 index 0000000000..d4591d8265 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/DocumentationExamples.csproj @@ -0,0 +1,72 @@ + + + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + + + DocumentationExamples + + net6.0 + LatestMajor + true + false + + CS8618,IDE0009,CA1051,CA1050,CA1707,CA2007,VSTHRD111,CS1591,RCS1110,CA5394,SKEXP0001,SKEXP0002,SKEXP0003,SKEXP0004,SKEXP0010,SKEXP0011,,SKEXP0012,SKEXP0020,SKEXP0021,SKEXP0022,SKEXP0023,SKEXP0024,SKEXP0025,SKEXP0026,SKEXP0027,SKEXP0028,SKEXP0029,SKEXP0030,SKEXP0031,SKEXP0032,SKEXP0040,SKEXP0041,SKEXP0042,SKEXP0050,SKEXP0051,SKEXP0052,SKEXP0053,SKEXP0054,SKEXP0055,SKEXP0060,SKEXP0061,SKEXP0101,SKEXP0102 + Library + 12.0 + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/dotnet/samples/DocumentationExamples/FunctionsWithinPrompts.cs b/dotnet/samples/DocumentationExamples/FunctionsWithinPrompts.cs new file mode 100644 index 0000000000..e2fb161176 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/FunctionsWithinPrompts.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Plugins.Core; +using Microsoft.SemanticKernel.PromptTemplates.Handlebars; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to call functions within prompts as described at +/// https://learn.microsoft.com/semantic-kernel/prompts/calling-nested-functions +/// +public class FunctionsWithinPrompts : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Functions within Prompts ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + // + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); + builder.Plugins.AddFromType(); + Kernel kernel = builder.Build(); + // + + List choices = new() { "ContinueConversation", "EndConversation" }; + + // Create few-shot examples + List fewShotExamples = new() + { + new ChatHistory() + { + new ChatMessageContent(AuthorRole.User, "Can you send a very quick approval to the marketing team?"), + new ChatMessageContent(AuthorRole.System, "Intent:"), + new ChatMessageContent(AuthorRole.Assistant, "ContinueConversation") + }, + new ChatHistory() + { + new ChatMessageContent(AuthorRole.User, "Can you send the full update to the marketing team?"), + new ChatMessageContent(AuthorRole.System, "Intent:"), + new ChatMessageContent(AuthorRole.Assistant, "EndConversation") + } + }; + + // Create handlebars template for intent + // + var getIntent = kernel.CreateFunctionFromPrompt( + new() + { + Template = @" +Instructions: What is the intent of this request? +Do not explain the reasoning, just reply back with the intent. If you are unsure, reply with {{choices[0]}}. +Choices: {{choices}}. + +{{#each fewShotExamples}} + {{#each this}} + {{content}} + {{/each}} +{{/each}} + +{{ConversationSummaryPlugin-SummarizeConversation history}} + +{{request}} +Intent:", + TemplateFormat = "handlebars" + }, + new HandlebarsPromptTemplateFactory() + ); + // + + // Create a Semantic Kernel template for chat + // + var chat = kernel.CreateFunctionFromPrompt( +@"{{ConversationSummaryPlugin.SummarizeConversation $history}} +User: {{$request}} +Assistant: " + ); + // + + // + // Create chat history + ChatHistory history = new(); + + // Start the chat loop + while (true) + { + // Get user input + Write("User > "); + var request = ReadLine(); + + // Invoke handlebars prompt + var intent = await kernel.InvokeAsync( + getIntent, + new() + { + { "request", request }, + { "choices", choices }, + { "history", history }, + { "fewShotExamples", fewShotExamples } + } + ); + + // End the chat if the intent is "Stop" + if (intent.ToString() == "EndConversation") + { + break; + } + + // Get chat response + var chatResult = kernel.InvokeStreamingAsync( + chat, + new() + { + { "request", request }, + { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) } + } + ); + + // Stream the response + string message = ""; + await foreach (var chunk in chatResult) + { + if (chunk.Role.HasValue) + { + Write(chunk.Role + " > "); + } + message += chunk; + Write(chunk); + } + WriteLine(); + + // Append to history + history.AddUserMessage(request!); + history.AddAssistantMessage(message); + } + + // + } + + public FunctionsWithinPrompts(ITestOutputHelper output) : base(output) + { + SimulatedInputText = [ + "Can you send an approval to the marketing team?", + "That is all, thanks."]; + } +} diff --git a/dotnet/samples/DocumentationExamples/Planner.cs b/dotnet/samples/DocumentationExamples/Planner.cs new file mode 100644 index 0000000000..c7e2bc727b --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Planner.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Plugins; +using Xunit; +using Xunit.Abstractions; + +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 : BaseTest +{ + [Fact] + public async Task RunAsync() + { + 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) + { + 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 = new(); + + // Start the conversation + Write("User > "); + string? userInput; + while ((userInput = ReadLine()) != null) + { + // Get user input + Write("User > "); + history.AddUserMessage(ReadLine()!); + + // Enable auto function calling + OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }; + + // 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) + { + Write("Assistant > "); + first = false; + } + Write(content.Content); + fullMessage += content.Content; + } + WriteLine(); + + // Add the message from the agent to the chat history + history.AddAssistantMessage(fullMessage); + + // Get user input again + Write("User > "); + } + } + + public Planner(ITestOutputHelper output) : base(output) + { + } +} diff --git a/dotnet/samples/DocumentationExamples/Plugin.cs b/dotnet/samples/DocumentationExamples/Plugin.cs new file mode 100644 index 0000000000..9888313a24 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugin.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example shows how to create a plugin class and interact with as described at +/// https://learn.microsoft.com/semantic-kernel/overview/ +/// This sample uses function calling, so it only works on models newer than 0613. +/// +public class Plugin : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Plugin ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + // Create kernel + // + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); + builder.Plugins.AddFromType(); + Kernel kernel = builder.Build(); + // + + // + + // Create chat history + var history = new ChatHistory(); + + // Get chat completion service + var chatCompletionService = kernel.GetRequiredService(); + + // Start the conversation + Write("User > "); + string? userInput; + while ((userInput = ReadLine()) != null) + { + // Add user input + history.AddUserMessage(userInput); + + // Enable auto function calling + OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new() + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }; + + // Get the response from the AI + var result = await chatCompletionService.GetChatMessageContentAsync( + history, + executionSettings: openAIPromptExecutionSettings, + kernel: kernel); + + // Print the results + WriteLine("Assistant > " + result); + + // Add the message from the agent to the chat history + history.AddMessage(result.Role, result.Content ?? string.Empty); + + // Get user input again + Write("User > "); + } + // + } + + public Plugin(ITestOutputHelper output) : base(output) + { + SimulatedInputText = [ + "Hello", + "Can you turn on the lights"]; + } +} + +// +public class LightPlugin +{ + public bool IsOn { get; set; } = false; + +#pragma warning disable CA1024 // Use properties where appropriate + [KernelFunction] + [Description("Gets the state of the light.")] + public string GetState() => IsOn ? "on" : "off"; +#pragma warning restore CA1024 // Use properties where appropriate + + [KernelFunction] + [Description("Changes the state of the light.'")] + public string ChangeState(bool newState) + { + this.IsOn = newState; + var state = GetState(); + + // Print the state to the console + Console.WriteLine($"[Light is now {state}]"); + + return state; + } +} +// diff --git a/dotnet/samples/DocumentationExamples/Plugins/MathPlugin.cs b/dotnet/samples/DocumentationExamples/Plugins/MathPlugin.cs new file mode 100644 index 0000000000..101f03505d --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/MathPlugin.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using Microsoft.SemanticKernel; + +namespace Plugins; + +public sealed class MathPlugin +{ + [KernelFunction, Description("Take the square root of a number")] + public static double Sqrt( + [Description("The number to take a square root of")] double number1 + ) + { + return Math.Sqrt(number1); + } + + [KernelFunction, Description("Add two numbers")] + public static double Add( + [Description("The first number to add")] double number1, + [Description("The second number to add")] double number2 + ) + { + return number1 + number2; + } + + [KernelFunction, Description("Subtract two numbers")] + public static double Subtract( + [Description("The first number to subtract from")] double number1, + [Description("The second number to subtract away")] double number2 + ) + { + return number1 - number2; + } + + [KernelFunction, Description("Multiply two numbers. When increasing by a percentage, don't forget to add 1 to the percentage.")] + public static double Multiply( + [Description("The first number to multiply")] double number1, + [Description("The second number to multiply")] double number2 + ) + { + return number1 * number2; + } + + [KernelFunction, Description("Divide two numbers")] + public static double Divide( + [Description("The first number to divide from")] double number1, + [Description("The second number to divide by")] double number2 + ) + { + return number1 / number2; + } + + [KernelFunction, Description("Raise a number to a power")] + public static double Power( + [Description("The number to raise")] double number1, + [Description("The power to raise the number to")] double number2 + ) + { + return Math.Pow(number1, number2); + } + + [KernelFunction, Description("Take the log of a number")] + public static double Log( + [Description("The number to take the log of")] double number1, + [Description("The base of the log")] double number2 + ) + { + return Math.Log(number1, number2); + } + + [KernelFunction, Description("Round a number to the target number of decimal places")] + public static double Round( + [Description("The number to round")] double number1, + [Description("The number of decimal places to round to")] double number2 + ) + { + return Math.Round(number1, (int)number2); + } + + [KernelFunction, Description("Take the absolute value of a number")] + public static double Abs( + [Description("The number to take the absolute value of")] double number1 + ) + { + return Math.Abs(number1); + } + + [KernelFunction, Description("Take the floor of a number")] + public static double Floor( + [Description("The number to take the floor of")] double number1 + ) + { + return Math.Floor(number1); + } + + [KernelFunction, Description("Take the ceiling of a number")] + public static double Ceiling( + [Description("The number to take the ceiling of")] double number1 + ) + { + return Math.Ceiling(number1); + } + + [KernelFunction, Description("Take the sine of a number")] + public static double Sin( + [Description("The number to take the sine of")] double number1 + ) + { + return Math.Sin(number1); + } + + [KernelFunction, Description("Take the cosine of a number")] + public static double Cos( + [Description("The number to take the cosine of")] double number1 + ) + { + return Math.Cos(number1); + } + + [KernelFunction, Description("Take the tangent of a number")] + public static double Tan( + [Description("The number to take the tangent of")] double number1 + ) + { + return Math.Tan(number1); + } + + [KernelFunction, Description("Take the arcsine of a number")] + public static double Asin( + [Description("The number to take the arcsine of")] double number1 + ) + { + return Math.Asin(number1); + } + + [KernelFunction, Description("Take the arccosine of a number")] + public static double Acos( + [Description("The number to take the arccosine of")] double number1 + ) + { + return Math.Acos(number1); + } + + [KernelFunction, Description("Take the arctangent of a number")] + public static double Atan( + [Description("The number to take the arctangent of")] double number1 + ) + { + return Math.Atan(number1); + } +} diff --git a/dotnet/samples/DocumentationExamples/Plugins/MathSolver.cs b/dotnet/samples/DocumentationExamples/Plugins/MathSolver.cs new file mode 100644 index 0000000000..23d0d3b9a0 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/MathSolver.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Planning.Handlebars; + +namespace Plugins; + +public class MathSolver +{ + private readonly ILogger _logger; + + public MathSolver(ILoggerFactory loggerFactory) + { + this._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/samples/DocumentationExamples/Plugins/OrchestratorPlugin/GetIntent/config.json b/dotnet/samples/DocumentationExamples/Plugins/OrchestratorPlugin/GetIntent/config.json new file mode 100644 index 0000000000..501fadf5b9 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/OrchestratorPlugin/GetIntent/config.json @@ -0,0 +1,31 @@ +{ + "schema": 1, + "type": "completion", + "description": "Gets the intent of the user.", + "completion": { + "max_tokens": 500, + "temperature": 0.0, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + }, + "input": { + "parameters": [ + { + "name": "input", + "description": "The user's request.", + "defaultValue": "" + }, + { + "name": "history", + "description": "The history of the conversation.", + "defaultValue": "" + }, + { + "name": "options", + "description": "The options to choose from.", + "defaultValue": "" + } + ] + } +} \ No newline at end of file diff --git a/dotnet/samples/DocumentationExamples/Plugins/OrchestratorPlugin/GetIntent/skprompt.txt b/dotnet/samples/DocumentationExamples/Plugins/OrchestratorPlugin/GetIntent/skprompt.txt new file mode 100644 index 0000000000..1fb1cab814 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/OrchestratorPlugin/GetIntent/skprompt.txt @@ -0,0 +1,10 @@ +[History] +{{$history}} + +User: {{$input}} + +--------------------------------------------- + +Provide the intent of the user. The intent should be one of the following: {{$options}} + +INTENT: \ No newline at end of file diff --git a/dotnet/samples/DocumentationExamples/Plugins/Prompts/chat/config.json b/dotnet/samples/DocumentationExamples/Plugins/Prompts/chat/config.json new file mode 100644 index 0000000000..28c9e2374c --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/Prompts/chat/config.json @@ -0,0 +1,33 @@ +{ + "schema": 1, + "type": "completion", + "description": "Creates a chat response to the user", + "execution_settings": { + "default": { + "max_tokens": 1000, + "temperature": 0 + }, + "gpt-3.5-turbo": { + "model_id": "gpt-3.5-turbo-0613", + "max_tokens": 4000, + "temperature": 0.1 + }, + "gpt-4": { + "model_id": "gpt-4-1106-preview", + "max_tokens": 8000, + "temperature": 0.3 + } + }, + "input_variables": [ + { + "name": "request", + "description": "The user's request.", + "required": true + }, + { + "name": "history", + "description": "The history of the conversation.", + "required": true + } + ] +} \ No newline at end of file diff --git a/dotnet/samples/DocumentationExamples/Plugins/Prompts/chat/skprompt.txt b/dotnet/samples/DocumentationExamples/Plugins/Prompts/chat/skprompt.txt new file mode 100644 index 0000000000..c4f3913469 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/Prompts/chat/skprompt.txt @@ -0,0 +1,3 @@ +{{ConversationSummaryPlugin.SummarizeConversation $history}} +User: {{$request}} +Assistant: \ No newline at end of file diff --git a/dotnet/samples/DocumentationExamples/Plugins/WriterPlugin/ShortPoem/config.json b/dotnet/samples/DocumentationExamples/Plugins/WriterPlugin/ShortPoem/config.json new file mode 100644 index 0000000000..c03553d766 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/WriterPlugin/ShortPoem/config.json @@ -0,0 +1,21 @@ +{ + "schema": 1, + "type": "completion", + "description": "Turn a scenario into a short and entertaining poem.", + "completion": { + "max_tokens": 200, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + }, + "input": { + "parameters": [ + { + "name": "input", + "description": "The scenario to turn into a poem.", + "defaultValue": "" + } + ] + } +} diff --git a/dotnet/samples/DocumentationExamples/Plugins/WriterPlugin/ShortPoem/skprompt.txt b/dotnet/samples/DocumentationExamples/Plugins/WriterPlugin/ShortPoem/skprompt.txt new file mode 100644 index 0000000000..d85146e031 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Plugins/WriterPlugin/ShortPoem/skprompt.txt @@ -0,0 +1,2 @@ +Generate a short funny poem or limerick to explain the given event. Be creative and be funny. Let your imagination run wild. +Event: {{$input}} diff --git a/dotnet/samples/DocumentationExamples/Prompts.cs b/dotnet/samples/DocumentationExamples/Prompts.cs new file mode 100644 index 0000000000..f84e29bb01 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Prompts.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to use prompts as described at +/// https://learn.microsoft.com/semantic-kernel/prompts/your-first-prompt +/// +public class Prompts : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Prompts ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + // + Kernel kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey) + .Build(); + // + + // 0.0 Initial prompt + ////////////////////////////////////////////////////////////////////////////////// + string request = "I want to send an email to the marketing team celebrating their recent milestone."; + string prompt = $"What is the intent of this request? {request}"; + + /* Uncomment this code to make this example interactive + // + Write("Your request: "); + string request = ReadLine()!; + string prompt = $"What is the intent of this request? {request}"; + // + */ + + WriteLine("0.0 Initial prompt"); + // + WriteLine(await kernel.InvokePromptAsync(prompt)); + // + + // 1.0 Make the prompt more specific + ////////////////////////////////////////////////////////////////////////////////// + // + prompt = @$"What is the intent of this request? {request} + You can choose between SendEmail, SendMessage, CompleteTask, CreateDocument."; + // + + WriteLine("1.0 Make the prompt more specific"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 2.0 Add structure to the output with formatting + ////////////////////////////////////////////////////////////////////////////////// + // + prompt = @$"Instructions: What is the intent of this request? + Choices: SendEmail, SendMessage, CompleteTask, CreateDocument. + User Input: {request} + Intent: "; + // + + WriteLine("2.0 Add structure to the output with formatting"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 2.1 Add structure to the output with formatting (using Markdown and JSON) + ////////////////////////////////////////////////////////////////////////////////// + // + prompt = @$"## Instructions +Provide the intent of the request using the following format: + +```json +{{ + ""intent"": {{intent}} +}} +``` + +## Choices +You can choose between the following intents: + +```json +[""SendEmail"", ""SendMessage"", ""CompleteTask"", ""CreateDocument""] +``` + +## User Input +The user input is: + +```json +{{ + ""request"": ""{request}"" +}} +``` + +## Intent"; + // + + WriteLine("2.1 Add structure to the output with formatting (using Markdown and JSON)"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 3.0 Provide examples with few-shot prompting + ////////////////////////////////////////////////////////////////////////////////// + // + prompt = @$"Instructions: What is the intent of this request? +Choices: SendEmail, SendMessage, CompleteTask, CreateDocument. + +User Input: Can you send a very quick approval to the marketing team? +Intent: SendMessage + +User Input: Can you send the full update to the marketing team? +Intent: SendEmail + +User Input: {request} +Intent: "; + // + + WriteLine("3.0 Provide examples with few-shot prompting"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 4.0 Tell the AI what to do to avoid doing something wrong + ////////////////////////////////////////////////////////////////////////////////// + // + prompt = @$"Instructions: What is the intent of this request? +If you don't know the intent, don't guess; instead respond with ""Unknown"". +Choices: SendEmail, SendMessage, CompleteTask, CreateDocument, Unknown. + +User Input: Can you send a very quick approval to the marketing team? +Intent: SendMessage + +User Input: Can you send the full update to the marketing team? +Intent: SendEmail + +User Input: {request} +Intent: "; + // + + WriteLine("4.0 Tell the AI what to do to avoid doing something wrong"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 5.0 Provide context to the AI + ////////////////////////////////////////////////////////////////////////////////// + // + string history = @"User input: I hate sending emails, no one ever reads them. +AI response: I'm sorry to hear that. Messages may be a better way to communicate."; + + prompt = @$"Instructions: What is the intent of this request? +If you don't know the intent, don't guess; instead respond with ""Unknown"". +Choices: SendEmail, SendMessage, CompleteTask, CreateDocument, Unknown. + +User Input: Can you send a very quick approval to the marketing team? +Intent: SendMessage + +User Input: Can you send the full update to the marketing team? +Intent: SendEmail + +{history} +User Input: {request} +Intent: "; + // + + WriteLine("5.0 Provide context to the AI"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 6.0 Using message roles in chat completion prompts + ////////////////////////////////////////////////////////////////////////////////// + // + history = @"I hate sending emails, no one ever reads them. +I'm sorry to hear that. Messages may be a better way to communicate."; + + prompt = @$"Instructions: What is the intent of this request? +If you don't know the intent, don't guess; instead respond with ""Unknown"". +Choices: SendEmail, SendMessage, CompleteTask, CreateDocument, Unknown. + +Can you send a very quick approval to the marketing team? +Intent: +SendMessage + +Can you send the full update to the marketing team? +Intent: +SendEmail + +{history} +{request} +Intent:"; + // + + WriteLine("6.0 Using message roles in chat completion prompts"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + + // 7.0 Give your AI words of encouragement + ////////////////////////////////////////////////////////////////////////////////// + // + history = @"I hate sending emails, no one ever reads them. +I'm sorry to hear that. Messages may be a better way to communicate."; + + prompt = @$"Instructions: What is the intent of this request? +If you don't know the intent, don't guess; instead respond with ""Unknown"". +Choices: SendEmail, SendMessage, CompleteTask, CreateDocument, Unknown. +Bonus: You'll get $20 if you get this right. + +Can you send a very quick approval to the marketing team? +Intent: +SendMessage + +Can you send the full update to the marketing team? +Intent: +SendEmail + +{history} +{request} +Intent:"; + // + + WriteLine("7.0 Give your AI words of encouragement"); + WriteLine(await kernel.InvokePromptAsync(prompt)); + } + + public Prompts(ITestOutputHelper output) : base(output) + { + } +} diff --git a/dotnet/samples/DocumentationExamples/README.md b/dotnet/samples/DocumentationExamples/README.md new file mode 100644 index 0000000000..7ad6666e3e --- /dev/null +++ b/dotnet/samples/DocumentationExamples/README.md @@ -0,0 +1,56 @@ +#Semantic Kernel documentation examples + +This project contains a collection of examples used in documentation on [learn.microsoft.com](https://learn.microsoft.com/). + +## Running Examples with Filters + +You can run specific examples by using test filters (dotnet test --filter). +Type "dotnet test --help" at the command line for more details. + +## Configuring Secrets + +Most of the examples will require secrets and credentials to access OpenAI, Azure OpenAI, +and other resources. We suggest using .NET +[Secret Manager](https://learn.microsoft.com/aspnet/core/security/app-secrets) +to avoid the risk of leaking secrets into the repository, branches and pull requests. +You can also use environment variables if you prefer. + +This project and KernelSyntaxExamples use the same pool of secrets. + +To set your secrets with Secret Manager: + +``` +cd dotnet/samples/DocumentationExamples + +dotnet user-secrets init + +dotnet user-secrets set "OpenAI:ModelId" "..." +dotnet user-secrets set "OpenAI:ChatModelId" "..." +dotnet user-secrets set "OpenAI:EmbeddingModelId" "..." +dotnet user-secrets set "OpenAI:ApiKey" "..." + +dotnet user-secrets set "AzureOpenAI:ServiceId" "..." +dotnet user-secrets set "AzureOpenAI:DeploymentName" "..." +dotnet user-secrets set "AzureOpenAI:ModelId" "..." +dotnet user-secrets set "AzureOpenAI:ChatDeploymentName" "..." +dotnet user-secrets set "AzureOpenAI:ChatModelId" "..." +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://... .openai.azure.com/" +dotnet user-secrets set "AzureOpenAI:ApiKey" "..." +``` + +To set your secrets with environment variables, use these names: + +``` +# OpenAI +OpenAI__ModelId +OpenAI__ChatModelId +OpenAI__EmbeddingModelId +OpenAI__ApiKey + +# Azure OpenAI +AzureOpenAI__ServiceId +AzureOpenAI__DeploymentName +AzureOpenAI__ChatDeploymentName +AzureOpenAI__Endpoint +AzureOpenAI__ApiKey +``` diff --git a/dotnet/samples/DocumentationExamples/Resources/getIntent.prompt.yaml b/dotnet/samples/DocumentationExamples/Resources/getIntent.prompt.yaml new file mode 100644 index 0000000000..e01cb765c2 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Resources/getIntent.prompt.yaml @@ -0,0 +1,40 @@ +name: getIntent +description: Gets the intent of the user. +template: | + Instructions: What is the intent of this request? + Do not explain the reasoning, just reply back with the intent. If you are unsure, reply with {{choices[0]}}. + Choices: {{choices}}. + + {{#each fewShotExamples}} + {{#each this}} + {{content}} + {{/each}} + {{/each}} + + {{ConversationSummaryPlugin.SummarizeConversation history}} + + {{request}} + Intent: +template_format: handlebars +input_variables: + - name: choices + description: The choices for the AI to choose from + default: ContinueConversation, EndConversation + - name: fewShotExamples + description: Few shot examples for the AI to learn from + is_required: true + - name: request + description: The user's request + is_required: true +execution_settings: + default: + max_tokens: 10 + temperature: 0 + gpt-3.5-turbo: + model_id: gpt-3.5-turbo-0613 + max_tokens: 10 + temperature: 0.2 + gpt-4: + model_id: gpt-4-1106-preview + max_tokens: 10 + temperature: 0.2 \ No newline at end of file diff --git a/dotnet/samples/DocumentationExamples/SerializingPrompts.cs b/dotnet/samples/DocumentationExamples/SerializingPrompts.cs new file mode 100644 index 0000000000..8d309e0eba --- /dev/null +++ b/dotnet/samples/DocumentationExamples/SerializingPrompts.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Plugins.Core; +using Microsoft.SemanticKernel.PromptTemplates.Handlebars; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to serialize prompts as described at +/// https://learn.microsoft.com/semantic-kernel/prompts/saving-prompts-as-files +/// +public class SerializingPrompts : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Serializing Prompts ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); + builder.Plugins.AddFromType(); + Kernel kernel = builder.Build(); + + // Load prompts + var prompts = kernel.CreatePluginFromPromptDirectory("./../../../Plugins/Prompts"); + + // Load prompt from YAML + using StreamReader reader = new(Assembly.GetExecutingAssembly().GetManifestResourceStream("Resources." + "getIntent.prompt.yaml")!); + KernelFunction getIntent = kernel.CreateFunctionFromPromptYaml( + await reader.ReadToEndAsync(), + promptTemplateFactory: new HandlebarsPromptTemplateFactory() + ); + + // Create choices + List choices = new() { "ContinueConversation", "EndConversation" }; + + // Create few-shot examples + List fewShotExamples = new() + { + new ChatHistory() + { + new ChatMessageContent(AuthorRole.User, "Can you send a very quick approval to the marketing team?"), + new ChatMessageContent(AuthorRole.System, "Intent:"), + new ChatMessageContent(AuthorRole.Assistant, "ContinueConversation") + }, + new ChatHistory() + { + new ChatMessageContent(AuthorRole.User, "Can you send the full update to the marketing team?"), + new ChatMessageContent(AuthorRole.System, "Intent:"), + new ChatMessageContent(AuthorRole.Assistant, "EndConversation") + } + }; + + // Create chat history + ChatHistory history = new(); + + // Start the chat loop + Write("User > "); + string? userInput; + while ((userInput = ReadLine()) != null) + { + // Invoke handlebars prompt + var intent = await kernel.InvokeAsync( + getIntent, + new() + { + { "request", userInput }, + { "choices", choices }, + { "history", history }, + { "fewShotExamples", fewShotExamples } + } + ); + + // End the chat if the intent is "Stop" + if (intent.ToString() == "EndConversation") + { + break; + } + + // Get chat response + var chatResult = kernel.InvokeStreamingAsync( + prompts["chat"], + new() + { + { "request", userInput }, + { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) } + } + ); + + // Stream the response + string message = ""; + await foreach (var chunk in chatResult) + { + if (chunk.Role.HasValue) + { + Write(chunk.Role + " > "); + } + message += chunk; + Write(chunk); + } + WriteLine(); + + // Append to history + history.AddUserMessage(userInput); + history.AddAssistantMessage(message); + + // Get user input again + Write("User > "); + } + } + + public SerializingPrompts(ITestOutputHelper output) : base(output) + { + SimulatedInputText = [ + "Can you send an approval to the marketing team?", + "That is all, thanks."]; + } +} diff --git a/dotnet/samples/DocumentationExamples/Templates.cs b/dotnet/samples/DocumentationExamples/Templates.cs new file mode 100644 index 0000000000..e75f6de982 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/Templates.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.PromptTemplates.Handlebars; +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to templatize prompts as described at +/// https://learn.microsoft.com/semantic-kernel/prompts/templatizing-prompts +/// +public class Templates : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Templates ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + Kernel kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey) + .Build(); + + // Create a Semantic Kernel template for chat + var chat = kernel.CreateFunctionFromPrompt( + @"{{$history}} + User: {{$request}} + Assistant: "); + + // Create choices + List choices = new() { "ContinueConversation", "EndConversation" }; + + // Create few-shot examples + List fewShotExamples = + [ + new ChatHistory() + { + new ChatMessageContent(AuthorRole.User, "Can you send a very quick approval to the marketing team?"), + new ChatMessageContent(AuthorRole.System, "Intent:"), + new ChatMessageContent(AuthorRole.Assistant, "ContinueConversation") + }, + new ChatHistory() + { + new ChatMessageContent(AuthorRole.User, "Thanks, I'm done for now"), + new ChatMessageContent(AuthorRole.System, "Intent:"), + new ChatMessageContent(AuthorRole.Assistant, "EndConversation") + } + ]; + + // Create handlebars template for intent + var getIntent = kernel.CreateFunctionFromPrompt( + new() + { + Template = @" +Instructions: What is the intent of this request? +Do not explain the reasoning, just reply back with the intent. If you are unsure, reply with {{choices[0]}}. +Choices: {{choices}}. + +{{#each fewShotExamples}} + {{#each this}} + {{content}} + {{/each}} +{{/each}} + +{{#each chatHistory}} + {{content}} +{{/each}} + +{{request}} +Intent:", + TemplateFormat = "handlebars" + }, + new HandlebarsPromptTemplateFactory() + ); + + ChatHistory history = new(); + + // Start the chat loop + while (true) + { + // Get user input + Write("User > "); + var request = ReadLine(); + + // Invoke prompt + var intent = await kernel.InvokeAsync( + getIntent, + new() + { + { "request", request }, + { "choices", choices }, + { "history", history }, + { "fewShotExamples", fewShotExamples } + } + ); + + // End the chat if the intent is "Stop" + if (intent.ToString() == "EndConversation") + { + break; + } + + // Get chat response + var chatResult = kernel.InvokeStreamingAsync( + chat, + new() + { + { "request", request }, + { "history", string.Join("\n", history.Select(x => x.Role + ": " + x.Content)) } + } + ); + + // Stream the response + string message = ""; + await foreach (var chunk in chatResult) + { + if (chunk.Role.HasValue) + { + Write(chunk.Role + " > "); + } + + message += chunk; + Write(chunk); + } + WriteLine(); + + // Append to history + history.AddUserMessage(request!); + history.AddAssistantMessage(message); + } + } + + public Templates(ITestOutputHelper output) : base(output) + { + SimulatedInputText = [ + "Can you send an approval to the marketing team?", + "That is all, thanks."]; + } +} diff --git a/dotnet/samples/DocumentationExamples/TestConfiguration.cs b/dotnet/samples/DocumentationExamples/TestConfiguration.cs new file mode 100644 index 0000000000..01108b8827 --- /dev/null +++ b/dotnet/samples/DocumentationExamples/TestConfiguration.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Configuration; + +public sealed class TestConfiguration +{ + private readonly IConfigurationRoot _configRoot; + private static TestConfiguration? s_instance; + + private TestConfiguration(IConfigurationRoot configRoot) + { + this._configRoot = configRoot; + } + + public static void Initialize(IConfigurationRoot configRoot) + { + s_instance = new TestConfiguration(configRoot); + } + + public static OpenAIConfig OpenAI => LoadSection(); + public static AzureOpenAIConfig AzureOpenAI => LoadSection(); + public static AzureOpenAIEmbeddingsConfig AzureOpenAIEmbeddings => LoadSection(); + + private static T LoadSection([CallerMemberName] string? caller = null) + { + if (s_instance == null) + { + throw new InvalidOperationException( + "TestConfiguration must be initialized with a call to Initialize(IConfigurationRoot) before accessing configuration values."); + } + + if (string.IsNullOrEmpty(caller)) + { + throw new ArgumentNullException(nameof(caller)); + } + return s_instance._configRoot.GetSection(caller).Get() ?? + throw new ArgumentException($"Missing {caller} configuration section"); + } + + public class OpenAIConfig + { + public string? ModelId { get; set; } + public string? ChatModelId { get; set; } + public string? EmbeddingModelId { get; set; } + public string? ApiKey { get; set; } + } + + public class AzureOpenAIConfig + { + public string? ServiceId { get; set; } + public string? DeploymentName { get; set; } + public string? ModelId { get; set; } + public string? ChatDeploymentName { get; set; } + public string? ChatModelId { get; set; } + public string? ImageDeploymentName { get; set; } + public string? ImageModelId { get; set; } + public string? ImageEndpoint { get; set; } + public string? Endpoint { get; set; } + public string? ApiKey { get; set; } + public string? ImageApiKey { get; set; } + } + + public class AzureOpenAIEmbeddingsConfig + { + public string? DeploymentName { get; set; } + public string? Endpoint { get; set; } + public string? ApiKey { get; set; } + } +} diff --git a/dotnet/samples/DocumentationExamples/UsingTheKernel.cs b/dotnet/samples/DocumentationExamples/UsingTheKernel.cs new file mode 100644 index 0000000000..8600efdddd --- /dev/null +++ b/dotnet/samples/DocumentationExamples/UsingTheKernel.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +// +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.Core; +// +using Xunit; +using Xunit.Abstractions; + +namespace Examples; + +/// +/// This example demonstrates how to interact with the kernel as described at +/// https://learn.microsoft.com/semantic-kernel/agents/kernel +/// +public class UsingTheKernel : BaseTest +{ + [Fact] + public async Task RunAsync() + { + WriteLine("======== Kernel ========"); + + 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) + { + WriteLine("Azure OpenAI credentials not found. Skipping example."); + + return; + } + + // Create a kernel with a logger and Azure OpenAI chat completion service + // + var builder = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(modelId, endpoint, apiKey); + builder.Services.AddLogging(c => c.AddDebug().SetMinimumLevel(LogLevel.Trace)); + builder.Plugins.AddFromType(); + builder.Plugins.AddFromPromptDirectory("./../../../Plugins/WriterPlugin"); + Kernel kernel = builder.Build(); + // + + // Get the current time + // + var currentTime = await kernel.InvokeAsync("TimePlugin", "UtcNow"); + WriteLine(currentTime); + // + + // Write a poem with the WriterPlugin.ShortPoem function using the current time as input + // + var poemResult = await kernel.InvokeAsync("WriterPlugin", "ShortPoem", new() + { + { "input", currentTime } + }); + WriteLine(poemResult); + // + } + + public UsingTheKernel(ITestOutputHelper output) : base(output) + { + } +} diff --git a/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs b/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs index e42883b52f..83f1e4509c 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example66_FunctionCallingStepwisePlanner.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Planning; using Microsoft.SemanticKernel.Plugins.Core; -using Plugins; using Xunit; using Xunit.Abstractions; @@ -52,7 +51,7 @@ public class Example66_FunctionCallingStepwisePlanner : BaseTest modelId: "gpt-3.5-turbo-1106") .Build(); - kernel.ImportPluginFromType(); + kernel.ImportPluginFromType(); kernel.ImportPluginFromType(); kernel.ImportPluginFromType(); diff --git a/dotnet/samples/KernelSyntaxExamples/README.md b/dotnet/samples/KernelSyntaxExamples/README.md index 8c8996efa3..56861e61a4 100644 --- a/dotnet/samples/KernelSyntaxExamples/README.md +++ b/dotnet/samples/KernelSyntaxExamples/README.md @@ -1,42 +1,13 @@ #Semantic Kernel syntax examples -This project contains a collection of semi-random examples about various scenarios -using SK components. +This project contains a collection of semi-random examples about various scenarios using SK components. -The examples are ordered by number, starting with very basic examples. +The examples can be run as integration tests but their code can also be copied to stand-alone programs. ## Running Examples with Filters -You can run individual examples in the KernelSyntaxExamples project using various methods to specify a filter. This allows you to execute specific examples without running all of them. Choose one of the following options to apply a filter: - -### Option 1: Set the Default Filter in Program.cs - -In your code, you can set a default filter by modifying the appropriate variable or parameter. Look for the section in your code where the filter is applied or where the examples are defined, and change the filter value accordingly. - -```csharp -// Example of setting a default filter in code -string defaultFilter = "Example0"; // will run all examples that contain 'example0' in the name -``` - -### Option 2: Set Command-Line Arguments - -Right-click on your console application project in the Solution Explorer. - -Choose "Properties" from the context menu. - -In the project properties window, navigate to the "Debug" tab on the left. - -Supply Command-Line Arguments: - -In the "Command line arguments" field, enter the command-line arguments that your console application expects. Separate multiple arguments with spaces. - -### Option 3: Use Visual Studio Code Filters - -If you are using Visual Studio Code, you can specify a filter using the built-in filter options provided by the IDE. These options can be helpful when running your code in a debugging environment. Consult the documentation for Visual Studio Code or the specific extension you're using for information on applying filters. - -### Option 4: Modify launch.json - -If you are using Visual Studio or a similar IDE that utilizes launch configurations, you can specify the filter in your launch.json configuration file. Edit the configuration for your project to include the filter parameter. +You can run specific examples in the KernelSyntaxExamples project by using test filters (dotnet test --filter). +Type "dotnet test --help" at the command line for more details. ## Configuring Secrets @@ -177,7 +148,8 @@ Redis__Configuration # Authentication for the OpenAPI Functions -The Semantic Kernel OpenAPI Function enables developers to take any REST API that follows the OpenAPI specification and import it as a plugin to the Semantic Kernel. However, the Kernel needs to be able to authenticate outgoing requests per the requirements of the target API. This document outlines the authentication model for the OpenAPI plugin. +The Semantic Kernel OpenAPI Function enables developers to take any REST API that follows the OpenAPI specification and import it as a plugin to the Semantic Kernel. +However, the Kernel needs to be able to authenticate outgoing requests per the requirements of the target API. This document outlines the authentication model for the OpenAPI plugin. ## The `AuthenticateRequestAsyncCallback` delegate @@ -187,7 +159,9 @@ The Semantic Kernel OpenAPI Function enables developers to take any REST API tha public delegate Task AuthenticateRequestAsyncCallback(HttpRequestMessage request); ``` -Developers may optionally provide an implementation of this delegate when importing an OpenAPI plugin to the Kernel. The delegate is then passed through to the `RestApiOperationRunner`, which is responsible for building the HTTP payload and sending the request for each REST API operation. Before the API request is sent, the delegate is executed with the HTTP request message as the parameter, allowing the request message to be updated with any necessary authentication information. +Developers may optionally provide an implementation of this delegate when importing an OpenAPI plugin to the Kernel. +The delegate is then passed through to the `RestApiOperationRunner`, which is responsible for building the HTTP payload and sending the request for each REST API operation. +Before the API request is sent, the delegate is executed with the HTTP request message as the parameter, allowing the request message to be updated with any necessary authentication information. This pattern was designed to be flexible enough to support a wide variety of authentication frameworks. @@ -195,7 +169,8 @@ This pattern was designed to be flexible enough to support a wide variety of aut ### BasicAuthenticationProvider -This class implements the HTTP "basic" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the user's credentials. When the `AuthenticateRequestAsync` method is called, it retrieves the credentials, encodes them as a UTF-8 encoded Base64 string, and adds them to the `HttpRequestMessage`'s authorization header. +This class implements the HTTP "basic" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the user's credentials. +When the `AuthenticateRequestAsync` method is called, it retrieves the credentials, encodes them as a UTF-8 encoded Base64 string, and adds them to the `HttpRequestMessage`'s authorization header. The following code demonstrates how to use this provider: @@ -212,7 +187,8 @@ var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.Jira, ne ### BearerAuthenticationProvider -This class implements the HTTP "bearer" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the bearer token. When the `AuthenticateRequestAsync` method is called, it retrieves the token and adds it to the `HttpRequestMessage`'s authorization header. +This class implements the HTTP "bearer" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the bearer token. +When the `AuthenticateRequestAsync` method is called, it retrieves the token and adds it to the `HttpRequestMessage`'s authorization header. The following code demonstrates how to use this provider: @@ -226,7 +202,9 @@ var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKey ### InteractiveMsalAuthenticationProvider -This class uses the [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview)'s .NET library to authenticate the user and acquire an OAuth token. It follows the interactive [authorization code flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow), requiring the user to sign in with a Microsoft or Azure identity. This is particularly useful for authenticating requests to the Microsoft Graph or Azure APIs. +This class uses the [Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview)'s .NET library to authenticate the user and acquire an OAuth token. +It follows the interactive [authorization code flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow), requiring the user to sign in with a Microsoft or Azure identity. +This is particularly useful for authenticating requests to the Microsoft Graph or Azure APIs. Once the token is acquired, it is added to the HTTP authentication header via the `AuthenticateRequestAsync` method, which is inherited from `BearerAuthenticationProvider`.