Skip to content

Commit

Permalink
.Net: Function Calling Stepwise Planner (#3434)
Browse files Browse the repository at this point in the history
### Motivation and Context
Stepwise planner built on OpenAI function calling

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: Ben Thomas <[email protected]>
Co-authored-by: Mark Wallace <[email protected]>
  • Loading branch information
3 people authored Nov 16, 2023
1 parent 43d1ee7 commit 01fdb69
Show file tree
Hide file tree
Showing 20 changed files with 706 additions and 16 deletions.
9 changes: 9 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Orchestration.Flow.IntegrationTests", "src\Experimental\Orchestration.Flow.IntegrationTests\Experimental.Orchestration.Flow.IntegrationTests.csproj", "{C17DD7E1-E588-46AF-A87E-DF8829A021DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Planners.OpenAI", "src\Planners\Planners.OpenAI\Planners.OpenAI.csproj", "{348BBF45-23B4-4599-83A6-8AE1795227FB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Assistants", "src\Experimental\Assistants\Experimental.Assistants.csproj", "{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Experimental.Assistants.UnitTests", "src\Experimental\Assistants.UnitTests\Experimental.Assistants.UnitTests.csproj", "{815E113D-6AAC-4AE8-8E14-5381E01E49C7}"
Expand Down Expand Up @@ -419,6 +421,12 @@ Global
{C17DD7E1-E588-46AF-A87E-DF8829A021DC}.Publish|Any CPU.Build.0 = Debug|Any CPU
{C17DD7E1-E588-46AF-A87E-DF8829A021DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C17DD7E1-E588-46AF-A87E-DF8829A021DC}.Release|Any CPU.Build.0 = Release|Any CPU
{348BBF45-23B4-4599-83A6-8AE1795227FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{348BBF45-23B4-4599-83A6-8AE1795227FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{348BBF45-23B4-4599-83A6-8AE1795227FB}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
{348BBF45-23B4-4599-83A6-8AE1795227FB}.Publish|Any CPU.Build.0 = Publish|Any CPU
{348BBF45-23B4-4599-83A6-8AE1795227FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{348BBF45-23B4-4599-83A6-8AE1795227FB}.Release|Any CPU.Build.0 = Release|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA}.Publish|Any CPU.ActiveCfg = Publish|Any CPU
Expand Down Expand Up @@ -503,6 +511,7 @@ Global
{C8BFFC74-3050-4F63-9E9D-C4F600427DE9} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{CB6E74CD-3A25-459F-A578-DEF25A414335} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{C17DD7E1-E588-46AF-A87E-DF8829A021DC} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{348BBF45-23B4-4599-83A6-8AE1795227FB} = {A21FAC7C-0C09-4EAD-843B-926ACEF73C80}
{35EDBB17-925D-4776-93E0-3E6A6CA5D8FA} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{815E113D-6AAC-4AE8-8E14-5381E01E49C7} = {A2357CF8-3BB9-45A1-93F1-B366C9B63658}
{6009CC87-32F1-4282-88BB-8E5A7BA12925} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planners;
using Microsoft.SemanticKernel.Plugins.Core;
using Plugins;
using RepoUtils;

// ReSharper disable once InconsistentNaming
public static class Example66_FunctionCallingStepwisePlanner
{
public static async Task RunAsync()
{
string[] questions = new string[]
{
"What is the current hour number, plus 5?",
"What is 387 minus 22? Email the solution to John and Mary.",
"Write a limerick, translate it to Spanish, and send it to Jane",
};

var kernel = InitializeKernel();

var config = new FunctionCallingStepwisePlannerConfig
{
MaxIterations = 15,
MaxTokens = 4000,
};
var planner = new FunctionCallingStepwisePlanner(kernel, config);

foreach (var question in questions)
{
FunctionCallingStepwisePlannerResult result = await planner.ExecuteAsync(question);
Console.WriteLine($"Q: {question}\nA: {result.FinalAnswer}");

// You can uncomment the line below to see the planner's process for completing the request.
// Console.WriteLine($"Chat history:\n{result.ChatHistory?.AsJson()}");
}
}

/// <summary>
/// Initialize the kernel and load plugins.
/// </summary>
/// <returns>A kernel instance</returns>
private static IKernel InitializeKernel()
{
IKernel kernel = new KernelBuilder()
.WithLoggerFactory(ConsoleLogger.LoggerFactory)
.WithAzureOpenAIChatCompletionService(
TestConfiguration.AzureOpenAI.ChatDeploymentName,
TestConfiguration.AzureOpenAI.Endpoint,
TestConfiguration.AzureOpenAI.ApiKey)
.Build();

kernel.ImportFunctions(new EmailPlugin(), "EmailPlugin");
kernel.ImportFunctions(new MathPlugin(), "MathPlugin");
kernel.ImportFunctions(new TimePlugin(), "TimePlugin");

return kernel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<ProjectReference Include="..\..\src\Extensions\TemplateEngine.Basic\TemplateEngine.Basic.csproj" />
<ProjectReference Include="..\..\src\Extensions\Reliability.Polly\Reliability.Polly.csproj" />
<ProjectReference Include="..\..\src\Extensions\TemplateEngine.Handlebars\TemplateEngine.Handlebars.csproj" />
<ProjectReference Include="..\..\src\Planners\Planners.OpenAI\Planners.OpenAI.csproj" />
<ProjectReference Include="..\..\src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.Core\Plugins.Core.csproj" />
<ProjectReference Include="..\..\src\Plugins\Plugins.Memory\Plugins.Memory.csproj" />
Expand Down
8 changes: 7 additions & 1 deletion dotnet/samples/KernelSyntaxExamples/Plugins/EmailPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public string GetEmailAddress(
// Sensitive data, logging as trace, disabled by default
logger?.LogTrace("Returning hard coded email for {0}", input);

return "[email protected]";
return input switch
{
"Jane" => "[email protected]",
"Paul" => "[email protected]",
"Mary" => "[email protected]",
_ => "[email protected]",
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public class OpenAIFunction
/// <summary>
/// Separator between the plugin name and the function name
/// </summary>
public const string NameSeparator = "-";
public const string NameSeparator = "_";

/// <summary>
/// Name of the function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ public async Task ItCreatesCorrectFunctionsWhenUsingAutoAsync()
Assert.NotNull(actualRequestContent);
var optionsJson = JsonSerializer.Deserialize<JsonElement>(actualRequestContent);
Assert.Equal(2, optionsJson.GetProperty("functions").GetArrayLength());
Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("functions")[0].GetProperty("name").GetString());
Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("functions")[1].GetProperty("name").GetString());
Assert.Equal("TimePlugin_Date", optionsJson.GetProperty("functions")[0].GetProperty("name").GetString());
Assert.Equal("TimePlugin_Now", optionsJson.GetProperty("functions")[1].GetProperty("name").GetString());
}

[Fact]
Expand All @@ -95,7 +95,7 @@ public async Task ItCreatesCorrectFunctionsWhenUsingNowAsync()
var chatCompletion = new OpenAIChatCompletion(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient);
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{ Content = new StringContent(ChatCompletionResponse) };
this._requestSettings.FunctionCall = "TimePlugin-Now";
this._requestSettings.FunctionCall = "TimePlugin_Now";

// Act
await chatCompletion.GetChatCompletionsAsync(new ChatHistory(), this._requestSettings);
Expand All @@ -105,7 +105,7 @@ public async Task ItCreatesCorrectFunctionsWhenUsingNowAsync()
Assert.NotNull(actualRequestContent);
var optionsJson = JsonSerializer.Deserialize<JsonElement>(actualRequestContent);
Assert.Equal(1, optionsJson.GetProperty("functions").GetArrayLength());
Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("functions")[0].GetProperty("name").GetString());
Assert.Equal("TimePlugin_Now", optionsJson.GetProperty("functions")[0].GetProperty("name").GetString());
}

[Fact]
Expand Down Expand Up @@ -189,7 +189,7 @@ public void Dispose()
""role"": ""assistant"",
""content"": null,
""function_call"": {
""name"": ""TimePlugin-Date"",
""name"": ""TimePlugin_Date"",
""arguments"": ""{}""
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void ItCanConvertToOpenAIFunctionNoParameters()
Assert.Equal(sut.Name, result.FunctionName);
Assert.Equal(sut.PluginName, result.PluginName);
Assert.Equal(sut.Description, result.Description);
Assert.Equal($"{sut.PluginName}-{sut.Name}", result.FullyQualifiedName);
Assert.Equal($"{sut.PluginName}_{sut.Name}", result.FullyQualifiedName);
Assert.NotNull(result.ReturnParameter);
Assert.Equivalent(new OpenAIFunctionReturnParameter { Description = "retDesc", Schema = JsonDocument.Parse("\"schema\"") }, result.ReturnParameter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public sealed class OpenAIFunctionResponseTests
public void ItCanConvertFromFunctionCallWithPluginName()
{
// Arrange
var sut = new FunctionCall("foo-bar", "{}");
var sut = new FunctionCall("foo_bar", "{}");

// Act
var result = OpenAIFunctionResponse.FromFunctionCall(sut);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void ItCanConvertToFunctionDefinitionWithPluginName()
FunctionDefinition result = sut.ToFunctionDefinition();

// Assert
Assert.Equal("myplugin-myfunc", result.Name);
Assert.Equal("myplugin_myfunc", result.Name);
Assert.Equal(sut.Description, result.Description);
}

Expand Down Expand Up @@ -90,7 +90,7 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete
var act = JsonSerializer.Serialize(JsonDocument.Parse(functionDefinition.Parameters));

Assert.NotNull(functionDefinition);
Assert.Equal("Tests-TestFunction", functionDefinition.Name);
Assert.Equal("Tests_TestFunction", functionDefinition.Name);
Assert.Equal("My test function", functionDefinition.Description);
Assert.Equal(JsonSerializer.Serialize(JsonDocument.Parse(expectedParameterSchema)), JsonSerializer.Serialize(JsonDocument.Parse(functionDefinition.Parameters)));
}
Expand Down Expand Up @@ -129,7 +129,7 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParame
FunctionDefinition functionDefinition = sut.ToFunctionDefinition();

Assert.NotNull(functionDefinition);
Assert.Equal("Tests-TestFunction", functionDefinition.Name);
Assert.Equal("Tests_TestFunction", functionDefinition.Name);
Assert.Equal("My test function", functionDefinition.Description);
Assert.Equal(JsonSerializer.Serialize(JsonDocument.Parse(expectedParameterSchema)), JsonSerializer.Serialize(JsonDocument.Parse(functionDefinition.Parameters)));
}
Expand Down
1 change: 1 addition & 0 deletions dotnet/src/IntegrationTests/IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<ProjectReference Include="..\Extensions\Reliability.Basic\Reliability.Basic.csproj" />
<ProjectReference Include="..\Extensions\TemplateEngine.Basic\TemplateEngine.Basic.csproj" />
<ProjectReference Include="..\Extensions\Reliability.Polly\Reliability.Polly.csproj" />
<ProjectReference Include="..\Planners\Planners.OpenAI\Planners.OpenAI.csproj" />
<ProjectReference Include="..\Plugins\Plugins.Core\Plugins.Core.csproj" />
<ProjectReference Include="..\Plugins\Plugins.Memory\Plugins.Memory.csproj" />
<ProjectReference Include="..\Functions\Functions.OpenAPI\Functions.OpenAPI.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planners;
using Microsoft.SemanticKernel.Plugins.Core;
using Microsoft.SemanticKernel.Plugins.Web;
using Microsoft.SemanticKernel.Plugins.Web.Bing;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
using Xunit.Abstractions;

namespace SemanticKernel.IntegrationTests.Planners.StepwisePlanner;

public sealed class FunctionCallingStepwisePlannerTests : IDisposable
{
private readonly string _bingApiKey;

public FunctionCallingStepwisePlannerTests(ITestOutputHelper output)
{
this._loggerFactory = NullLoggerFactory.Instance;
this._testOutputHelper = new RedirectOutput(output);

// Load configuration
this._configuration = new ConfigurationBuilder()
.AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddUserSecrets<StepwisePlannerTests>()
.Build();

string? bingApiKeyCandidate = this._configuration["Bing:ApiKey"];
Assert.NotNull(bingApiKeyCandidate);
this._bingApiKey = bingApiKeyCandidate;
}

[Theory(Skip = "Requires model deployment that supports function calling.")]
[InlineData("What is the tallest mountain on Earth? How tall is it?", "Everest")]
[InlineData("What is the weather in Seattle?", "Seattle")]
public async Task CanExecuteStepwisePlanAsync(string prompt, string partialExpectedAnswer)
{
// Arrange
bool useEmbeddings = false;
IKernel kernel = this.InitializeKernel(useEmbeddings);
var bingConnector = new BingConnector(this._bingApiKey);
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
kernel.ImportFunctions(webSearchEnginePlugin, "WebSearch");
kernel.ImportFunctions(new TimePlugin(), "time");

var planner = new FunctionCallingStepwisePlanner(
kernel,
new FunctionCallingStepwisePlannerConfig() { MaxIterations = 10 });

// Act
var planResult = await planner.ExecuteAsync(prompt);

// Assert - should contain the expected answer
Assert.NotNull(planResult);
Assert.NotEqual(string.Empty, planResult.FinalAnswer);
Assert.Contains(partialExpectedAnswer, planResult.FinalAnswer, StringComparison.InvariantCultureIgnoreCase);
Assert.True(planResult.Iterations > 0);
Assert.True(planResult.Iterations <= 10);
}

private IKernel InitializeKernel(bool useEmbeddings = false)
{
AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
Assert.NotNull(azureOpenAIConfiguration);

AzureOpenAIConfiguration? azureOpenAIEmbeddingsConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get<AzureOpenAIConfiguration>();
Assert.NotNull(azureOpenAIEmbeddingsConfiguration);

var builder = new KernelBuilder()
.WithLoggerFactory(this._loggerFactory)
.WithRetryBasic();

builder.WithAzureOpenAIChatCompletionService(
deploymentName: azureOpenAIConfiguration.ChatDeploymentName!,
endpoint: azureOpenAIConfiguration.Endpoint,
apiKey: azureOpenAIConfiguration.ApiKey);

if (useEmbeddings)
{
builder.WithAzureOpenAITextEmbeddingGenerationService(
deploymentName: azureOpenAIEmbeddingsConfiguration.DeploymentName,
endpoint: azureOpenAIEmbeddingsConfiguration.Endpoint,
apiKey: azureOpenAIEmbeddingsConfiguration.ApiKey);
}

var kernel = builder.Build();

return kernel;
}

private readonly ILoggerFactory _loggerFactory;
private readonly RedirectOutput _testOutputHelper;
private readonly IConfigurationRoot _configuration;

public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}

~FunctionCallingStepwisePlannerTests()
{
this.Dispose(false);
}

private void Dispose(bool disposing)
{
if (disposing)
{
if (this._loggerFactory is IDisposable ld)
{
ld.Dispose();
}

this._testOutputHelper.Dispose();
}
}
}
5 changes: 2 additions & 3 deletions dotnet/src/Planners/Planners.Core/Utils/EmbeddedResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System.IO;
using System.Reflection;
using Microsoft.SemanticKernel.Diagnostics;

#pragma warning disable IDE0130
namespace Microsoft.SemanticKernel.Planners;
Expand All @@ -15,10 +14,10 @@ internal static class EmbeddedResource
internal static string Read(string name)
{
var assembly = typeof(EmbeddedResource).GetTypeInfo().Assembly;
if (assembly == null) { throw new SKException($"[{s_namespace}] {name} assembly not found"); }
if (assembly == null) { throw new FileNotFoundException($"[{s_namespace}] {name} assembly not found"); }

using Stream? resource = assembly.GetManifestResourceStream($"{s_namespace}." + name);
if (resource == null) { throw new SKException($"[{s_namespace}] {name} resource not found"); }
if (resource == null) { throw new FileNotFoundException($"[{s_namespace}] {name} resource not found"); }

using var reader = new StreamReader(resource);
return reader.ReadToEnd();
Expand Down
36 changes: 36 additions & 0 deletions dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- THIS PROPERTY GROUP MUST COME FIRST -->
<AssemblyName>Microsoft.SemanticKernel.Planners.OpenAI</AssemblyName>
<RootNamespace>Microsoft.SemanticKernel.Planners</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<Import Project="$(RepoRoot)/dotnet/src/InternalUtilities/src/InternalUtilities.props" />

<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Semantic Kernel - Planners</Title>
<Description>Semantic Kernel OpenAI Planners.</Description>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="Stepwise\InitialPlanPrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Stepwise\StepPrompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Connectors\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\Functions\Functions.OpenAPI\Functions.OpenAPI.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
<ProjectReference Include="..\..\Extensions\TemplateEngine.Basic\TemplateEngine.Basic.csproj" />
<ProjectReference Include="..\Planners.Core\Planners.Core.csproj" />
</ItemGroup>
</Project>
Loading

0 comments on commit 01fdb69

Please sign in to comment.