-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
.Net: Added support for multiple chat and text results from Kernel (#…
…6704) ### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> **Note**: This PR changes the behavior when AI connector returns multiple results, when using `kernel.InvokeAsync`. It won't throw an exception anymore, but instead it will return multiple results. The behavior for single result is not changed. Fixes: #6434 When executing prompt function using kernel and asking for multiple results per prompt, we will get an error: ```csharp var arguments = new KernelArguments(new OpenAIPromptExecutionSettings { ResultsPerPrompt = 3 }); var result = await this._kernel.InvokePromptAsync("Hi, can you help me today?", arguments); // this will throw an exception ``` Current `KernelFunctionFromPrompt` implementation expects only single result from AI connector, while its API can return multiple results per prompt/request. This PR updates `KernelFunctionFromPrompt` in a following way: 1. If AI connector returns single item - the behavior will be the same as it is today, `FunctionResult` will contain instance of that item, so it's possible to get its properties, use `ToString()` etc. 2. If AI connector returns multiple items - all items will be returned in collection to the caller, and this collection needs to be handled appropriately (by using loop or accessing specific item by index). One of the examples shows how to select one result, in case if we invoke prompt function inside another prompt function using prompt template engine. In this case, filter can be registered, which will get multiple results produced by function, select one of them and return it back to the prompt rendering operation. ### 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 😄
- Loading branch information
1 parent
d30250f
commit 4b8a526
Showing
6 changed files
with
404 additions
and
59 deletions.
There are no files selected for viewing
137 changes: 105 additions & 32 deletions
137
dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionMultipleChoices.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,133 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.ChatCompletion; | ||
using Microsoft.SemanticKernel.Connectors.OpenAI; | ||
|
||
namespace ChatCompletion; | ||
|
||
// The following example shows how to use Semantic Kernel with streaming Multiple Results Chat Completion. | ||
/// <summary> | ||
/// The following example shows how to use Semantic Kernel with multiple chat completion results. | ||
/// </summary> | ||
public class OpenAI_ChatCompletionMultipleChoices(ITestOutputHelper output) : BaseTest(output) | ||
{ | ||
/// <summary> | ||
/// Example with multiple chat completion results using <see cref="Kernel"/>. | ||
/// </summary> | ||
[Fact] | ||
public Task AzureOpenAIMultiChatCompletionAsync() | ||
public async Task MultipleChatCompletionResultsUsingKernelAsync() | ||
{ | ||
Console.WriteLine("======== Azure OpenAI - Multiple Chat Completion ========"); | ||
var kernel = Kernel | ||
.CreateBuilder() | ||
.AddOpenAIChatCompletion( | ||
modelId: TestConfiguration.OpenAI.ChatModelId, | ||
apiKey: TestConfiguration.OpenAI.ApiKey) | ||
.Build(); | ||
|
||
var chatCompletionService = new AzureOpenAIChatCompletionService( | ||
deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, | ||
endpoint: TestConfiguration.AzureOpenAI.Endpoint, | ||
apiKey: TestConfiguration.AzureOpenAI.ApiKey, | ||
modelId: TestConfiguration.AzureOpenAI.ChatModelId); | ||
// Execution settings with configured ResultsPerPrompt property. | ||
var executionSettings = new OpenAIPromptExecutionSettings { MaxTokens = 200, ResultsPerPrompt = 3 }; | ||
|
||
return ChatCompletionAsync(chatCompletionService); | ||
var contents = await kernel.InvokePromptAsync<IReadOnlyList<KernelContent>>("Write a paragraph about why AI is awesome", new(executionSettings)); | ||
|
||
foreach (var content in contents!) | ||
{ | ||
Console.Write(content.ToString() ?? string.Empty); | ||
Console.WriteLine("\n-------------\n"); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Example with multiple chat completion results using <see cref="IChatCompletionService"/>. | ||
/// </summary> | ||
[Fact] | ||
public Task OpenAIMultiChatCompletionAsync() | ||
public async Task MultipleChatCompletionResultsUsingChatCompletionServiceAsync() | ||
{ | ||
Console.WriteLine("======== Open AI - Multiple Chat Completion ========"); | ||
var kernel = Kernel | ||
.CreateBuilder() | ||
.AddOpenAIChatCompletion( | ||
modelId: TestConfiguration.OpenAI.ChatModelId, | ||
apiKey: TestConfiguration.OpenAI.ApiKey) | ||
.Build(); | ||
|
||
// Execution settings with configured ResultsPerPrompt property. | ||
var executionSettings = new OpenAIPromptExecutionSettings { MaxTokens = 200, ResultsPerPrompt = 3 }; | ||
|
||
var chatHistory = new ChatHistory(); | ||
chatHistory.AddUserMessage("Write a paragraph about why AI is awesome"); | ||
|
||
var chatCompletionService = new OpenAIChatCompletionService( | ||
TestConfiguration.OpenAI.ChatModelId, | ||
TestConfiguration.OpenAI.ApiKey); | ||
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>(); | ||
|
||
return ChatCompletionAsync(chatCompletionService); | ||
foreach (var chatMessageContent in await chatCompletionService.GetChatMessageContentsAsync(chatHistory, executionSettings)) | ||
{ | ||
Console.Write(chatMessageContent.Content ?? string.Empty); | ||
Console.WriteLine("\n-------------\n"); | ||
} | ||
} | ||
|
||
private async Task ChatCompletionAsync(IChatCompletionService chatCompletionService) | ||
/// <summary> | ||
/// This example shows how to handle multiple results in case if prompt template contains a call to another prompt function. | ||
/// <see cref="FunctionResultSelectionFilter"/> is used for result selection. | ||
/// </summary> | ||
[Fact] | ||
public async Task MultipleChatCompletionResultsInPromptTemplateAsync() | ||
{ | ||
var executionSettings = new OpenAIPromptExecutionSettings() | ||
{ | ||
MaxTokens = 200, | ||
FrequencyPenalty = 0, | ||
PresencePenalty = 0, | ||
Temperature = 1, | ||
TopP = 0.5, | ||
ResultsPerPrompt = 2, | ||
}; | ||
var kernel = Kernel | ||
.CreateBuilder() | ||
.AddOpenAIChatCompletion( | ||
modelId: TestConfiguration.OpenAI.ChatModelId, | ||
apiKey: TestConfiguration.OpenAI.ApiKey) | ||
.Build(); | ||
|
||
var chatHistory = new ChatHistory(); | ||
chatHistory.AddUserMessage("Write one paragraph about why AI is awesome"); | ||
var executionSettings = new OpenAIPromptExecutionSettings { MaxTokens = 200, ResultsPerPrompt = 3 }; | ||
|
||
// Initializing a function with execution settings for multiple results. | ||
// We ask AI to write one paragraph, but in execution settings we specified that we want 3 different results for this request. | ||
var function = KernelFunctionFactory.CreateFromPrompt("Write a paragraph about why AI is awesome", executionSettings, "GetParagraph"); | ||
var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); | ||
|
||
foreach (var chatMessageChoice in await chatCompletionService.GetChatMessageContentsAsync(chatHistory, executionSettings)) | ||
kernel.Plugins.Add(plugin); | ||
|
||
// Add function result selection filter. | ||
kernel.FunctionInvocationFilters.Add(new FunctionResultSelectionFilter(this.Output)); | ||
|
||
// Inside our main request, we call MyPlugin.GetParagraph function for text summarization. | ||
// Taking into account that MyPlugin.GetParagraph function produces 3 results, for text summarization we need to choose only one of them. | ||
// Registered filter will be invoked during execution, which will select and return only 1 result, and this result will be inserted in our main request for summarization. | ||
var result = await kernel.InvokePromptAsync("Summarize this text: {{MyPlugin.GetParagraph}}"); | ||
|
||
// It's possible to check what prompt was rendered for our main request. | ||
Console.WriteLine($"Rendered prompt: '{result.RenderedPrompt}'"); | ||
|
||
// Output: | ||
// Rendered prompt: 'Summarize this text: AI is awesome because...' | ||
} | ||
|
||
/// <summary> | ||
/// Example of filter which is responsible for result selection in case if some function produces multiple results. | ||
/// </summary> | ||
private sealed class FunctionResultSelectionFilter(ITestOutputHelper output) : IFunctionInvocationFilter | ||
{ | ||
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next) | ||
{ | ||
Console.Write(chatMessageChoice.Content ?? string.Empty); | ||
Console.WriteLine("\n-------------\n"); | ||
} | ||
await next(context); | ||
|
||
// Selection logic for function which is expected to produce multiple results. | ||
if (context.Function.Name == "GetParagraph") | ||
{ | ||
// Get multiple results from function invocation | ||
var contents = context.Result.GetValue<IReadOnlyList<KernelContent>>()!; | ||
|
||
Console.WriteLine(); | ||
output.WriteLine("Multiple results:"); | ||
|
||
foreach (var content in contents) | ||
{ | ||
output.WriteLine(content.ToString()); | ||
} | ||
|
||
// Select first result for correct prompt rendering | ||
var selectedContent = contents[0]; | ||
context.Result = new FunctionResult(context.Function, selectedContent, context.Kernel.Culture, selectedContent.Metadata); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.