Repository navigation
.Net: Kernel Streaming Support - #2986
Roger Barreto (rogerbarreto) wants to merge 15 commits into
Conversation
| MaxTokens = 1000 | ||
| }); | ||
|
|
||
| await foreach (string token in (await kernel.RunAsync(fixedFunction)).GetValue<IAsyncEnumerable<string>>()!) |
There was a problem hiding this comment.
nit: I would break this up the way you did in Example 32 for better readability, like:
| await foreach (string token in (await kernel.RunAsync(fixedFunction)).GetValue<IAsyncEnumerable<string>>()!) | |
| var result = await kernel.RunAsync(fixedFunction); | |
| await foreach (string token in result.GetValue<IAsyncEnumerable<string>>()!) |
| Console.WriteLine("======== Azure OpenAI - Multiple Text Completion - Raw Streaming ========"); | ||
|
|
||
| var chatCompletion = new AzureChatCompletion( | ||
| var textCompletion = new AzureChatCompletion( |
There was a problem hiding this comment.
The type of completion didn't change to chat completion. - did you intend to change the type as well? If so - I was wondering if it makes sense to do this given that the text completion models are getting deprecated.
|
|
||
| namespace Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; | ||
|
|
||
| /// <summary> Represents a singular result of a chat completion.</summary> |
There was a problem hiding this comment.
Is this description accurate?
| /// <summary> Represents a singular result of a chat completion.</summary> | ||
| public class StreamingTextModelResult | ||
| { | ||
| /// <summary> A unique identifier associated with this chat completion response. </summary> |
There was a problem hiding this comment.
| /// <summary> A unique identifier associated with this chat completion response. </summary> | |
| /// <summary> A unique identifier associated with this text completion response. </summary> |
| } | ||
|
|
||
| /// <summary> | ||
| /// Retrieves a typed <see cref="Completions"/> OpenAI / AzureOpenAI result from text completion prompt. |
There was a problem hiding this comment.
It seems confusing that this returns the AzureOpenAI result type. Is this what you meant?
/// Retrieves a <see cref="StreamingTextModelResult "/> from the model result.
| { | ||
| get | ||
| { | ||
| if (this.ExtensionData.TryGetValue("results_per_prompt", out var value)) |
There was a problem hiding this comment.
suggestion: use a const for the string literal
| var prompt = "Write one paragraph why AI is awesome"; | ||
| var consoleLinesPerResult = 12; |
There was a problem hiding this comment.
nit: can be const
| /// </summary> | ||
| public StreamingChatChoice Choice { get; } | ||
|
|
||
| /// <summary> Initializes a new instance of ChatModelResult. </summary> |
There was a problem hiding this comment.
| /// <summary> Initializes a new instance of ChatModelResult. </summary> | |
| /// <summary> Initializes a new instance of <see cref="StreamingChatModelResult"/>. </summary> |
| /// </summary> | ||
| public StreamingChoice Choice { get; } | ||
|
|
||
| /// <summary> Initializes a new instance of StreamingTextModelResult. </summary> |
There was a problem hiding this comment.
| /// <summary> Initializes a new instance of StreamingTextModelResult. </summary> | |
| /// <summary> Initializes a new instance of <see cref="StreamingTextModelResult"/>. </summary> |
| /// <summary> | ||
| /// Enables streaming behavior when available | ||
| /// </summary> | ||
| public bool? Streaming { get; set; } = false; |
There was a problem hiding this comment.
I would suggest to rename this property to clearly show that it's boolean flag (e.g. IsStreamingEnabled or UseStreaming). It will be easier to understand how to use it (settings.IsStreamingEnabled = true is more clear than settings.Streaming = ?).
| namespace Microsoft.SemanticKernel.AI.ChatCompletion; | ||
|
|
||
| /// <summary> | ||
| /// Provides extension methods for the IChatCompletion interface. |
There was a problem hiding this comment.
| /// Provides extension methods for the IChatCompletion interface. | |
| /// Provides extension methods for the <see cref="IChatStreamingCompletion"/> interface. |
| result = new FunctionResult(this.Name, this.PluginName, context, completion); | ||
|
|
||
| var modelResults = completionResults.Select(c => c.ModelResult).ToArray(); | ||
| if (requestSettings?.Streaming == true && |
There was a problem hiding this comment.
I would suggest to move streaming and non-streaming logic to separate private methods to keep method RunPromptAsync as simple as possible.
| { | ||
| get | ||
| { | ||
| if (this.ExtensionData.TryGetValue("results_per_prompt", out var value)) |
There was a problem hiding this comment.
If this property is applicable to general SemanticFunction logic (Method GetResultsCountOrDefault), we can:
- Move
ResultsPerPromptparameter to baseAIRequestSettingswith default value1- although it will mean that this parameter is applicable to every AI connector, which is not the case. - Place logic for multiple results only where it's applicable - move it from SemanticFunction to OpenAI Connector.
- Remove this property from
OpenAIRequestSettingscompletely and place it as parameter of function. (I had an idea to replace SKContext as input parameter for function/kernel withFunctionRequest/KernelRequest, and propertyResultsPerPromptcan be good first candidate for request models, but this is definitely separate scope of work and PR.)
Main point here, that we should not use ExtensionData as a way of storing general settings for functions and accessing them from Kernel/Function logic. As far as I understand, it should be used by Connectors only.
|
|
||
| functionResult = await skFunction.InvokeAsync(context, cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| context = functionResult.Context; |
There was a problem hiding this comment.
This is breaking change with unknown outcomes. That's the reason why property Context still exists in FunctionResult model internally. Because, some functions may update context inside (since context is mutable at the moment), but some of them can return new cloned context instance with updated parameters (that's probably the way how it should be), which means that next function in pipeline won't receive updated variables if we remove this line.
We should investigate and test all available scenarios to understand if we can safely do this change and if we need to do it at all.
| var templateConfig = new PromptTemplateConfig(); | ||
| if (useStream is not null) | ||
| { | ||
| templateConfig.Completion = new() { Streaming = useStream }; |
There was a problem hiding this comment.
As mentioned above, name Streaming here doesn't show that it's boolean flag, that's why this test contains parameter useStream. I would prefer to use the same naming in settings and tests, like UseStreaming or IsStreamingEnabled.
| var functionResult2 = kernelResult.FunctionResults.First(l => l.FunctionName == "Function2" && l.PluginName == PluginName); | ||
| var functionResult3 = kernelResult.FunctionResults.First(l => l.FunctionName == "Function3" && l.PluginName == PluginName); | ||
|
|
||
| var function3Result = functionResult3.GetValue<string>(); |
There was a problem hiding this comment.
Do we need this result in separate variable?
|
Dropping this change for now, as I will be working in potentially other decision regarding the Streaming approach for v1 |
Motivation and Context
As a long-awaited feature request, this new allows streaming when calling Kernel.RunAsync or SKFunctions.InvokeAsync.
Resolves .Net: Streaming support for semantic functions on IKernel #1649
Resolves .NET Allow ISKFunction to return IAsyncEnumerable #1298
Adjusted
Completionabstract interface methods/name following a common patternAdded streaming component interface specializations.
Description
Streaming behavior by default is false since streaming responses from providers like OpenAI/Azure OpenAI won't give additional data like (PromptFilterResults, Usage(Completion Tokens, Prompt Tokens, Total Tokens, ...).
To enable streaming behavior set
Streaming = truein aAIRequestSettingsspecialized class likeOpenAIRequestSettings, once this is set for text/chat based models like OpenAi's GPT you can get streaming data usingGetValue<IAsyncEnumerable<string>>()in the result ofKernel.RunAsyncorFunction.InvokeAsync.How to:
ℹ️ Any attempt on using
SKContextfromFunctionResultswill cancel the streaming behavior as those require the full content to be available.New Streaming flag added to
bool AIRequestSettings.Streamingabstraction, this will signal that the caller has intention on use streaming when available.New streaming interfaces added:
ITextStreamingCompletionIChatStreamingCompletionChanges to the method names following the pattern (which service is it and what it is returning)
IText(Streaming)Completion
List<ITextResult> GetCompletionsAsyncNew:
GetTextResultsAsyncIAsyncEnumerable<ITextStreamingResult> GetStreamingCompletionsAsyncNew:
GetTextStreamingResultsAsyncIText(Streaming)Result
Task<string> GetCompletionAsyncNew:
GetTextAsyncIAsyncEnumerable<ITextStreamingResult> GetStreamingCompletionsAsyncNew:
GetTextStreamingResultsAsyncIChat(Streaming)Completion
Task<IReadOnlyList<IChatResult>> GetChatCompletionsAsyncNew:
GetChatResultsAsyncIAsyncEnumerable<IChatStreamingResult> GetStreamingChatCompletionsAsyncNew:
GetChatStreamingResultsAsyncServices following this abstraction signales to Kernel it can use streaming when requested.
Contribution Checklist