Skip to content

.Net: Kernel Streaming Support - #2986

Closed
Roger Barreto (rogerbarreto) wants to merge 15 commits into
microsoft:mainfrom
rogerbarreto:features/kernel-streaming-poc1
Closed

Roger Barreto (rogerbarreto) wants to merge 15 commits into
microsoft:mainfrom
rogerbarreto:features/kernel-streaming-poc1

Conversation

@rogerbarreto

@rogerbarreto Roger Barreto (rogerbarreto) commented Sep 26, 2023 •

Copy link
Copy Markdown
Member

Motivation and Context

Description

⚠️ Breaking Change

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 = true in a AIRequestSettings specialized class like OpenAIRequestSettings, once this is set for text/chat based models like OpenAi's GPT you can get streaming data using GetValue<IAsyncEnumerable<string>>() in the result of Kernel.RunAsync or Function.InvokeAsync.

How to:

myFunction = kernel.CreateSemanticFunction($"your prompt",
        requestSettings: new OpenAIRequestSettings
        {
            Streaming = true,
        });

var result = await kernel.RunAsync(myFunction);
await foreach (string token in result.GetValue<IAsyncEnumerable<string>>())
{
    Console.Write(token);
}

ℹ️ Any attempt on using SKContext from FunctionResults will cancel the streaming behavior as those require the full content to be available.

New Streaming flag added to bool AIRequestSettings.Streaming abstraction, this will signal that the caller has intention on use streaming when available.

New streaming interfaces added:

  • ITextStreamingCompletion
  • IChatStreamingCompletion

Changes to the method names following the pattern (which service is it and what it is returning)

  • IText(Streaming)Completion

    • Old: List<ITextResult> GetCompletionsAsync
      New: GetTextResultsAsync
    • Old: IAsyncEnumerable<ITextStreamingResult> GetStreamingCompletionsAsync
      New: GetTextStreamingResultsAsync
  • IText(Streaming)Result

    • Old: Task<string> GetCompletionAsync
      New: GetTextAsync
    • Old: IAsyncEnumerable<ITextStreamingResult> GetStreamingCompletionsAsync
      New: GetTextStreamingResultsAsync
  • IChat(Streaming)Completion

    • Old: Task<IReadOnlyList<IChatResult>> GetChatCompletionsAsync
      New: GetChatResultsAsync
    • Old: IAsyncEnumerable<IChatStreamingResult> GetStreamingChatCompletionsAsync
      New: GetChatStreamingResultsAsync

Services following this abstraction signales to Kernel it can use streaming when requested.

Contribution Checklist

@shawncal Shawn Callegari (shawncal) added .NET Issue or Pull requests regarding .NET code kernel Issues or pull requests impacting the core kernel kernel.core labels Sep 26, 2023
@github-actions github-actions Bot changed the title Kernel Streaming Support - Proof of Concept .Net: Kernel Streaming Support - Proof of Concept Sep 26, 2023
@rogerbarreto Roger Barreto (rogerbarreto) changed the title .Net: Kernel Streaming Support - Proof of Concept .Net: Kernel Streaming Support Sep 26, 2023
@rogerbarreto Roger Barreto (rogerbarreto) added the PR: ready for review All feedback addressed, ready for reviews label Sep 26, 2023
@rogerbarreto Roger Barreto (rogerbarreto) added this to the v1 milestone Sep 26, 2023
@rogerbarreto Roger Barreto (rogerbarreto) added the PR: breaking change Pull requests that introduce breaking changes label Sep 26, 2023
MaxTokens = 1000
});

await foreach (string token in (await kernel.RunAsync(fixedFunction)).GetValue<IAsyncEnumerable<string>>()!)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would break this up the way you did in Example 32 for better readability, like:

Suggested change
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: use a const for the string literal

Comment on lines +60 to +61
var prompt = "Write one paragraph why AI is awesome";
var consoleLinesPerResult = 12;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can be const

/// </summary>
public StreamingChatChoice Choice { get; }

/// <summary> Initializes a new instance of ChatModelResult. </summary>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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 &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

@dmytrostruk Dmytro Struk (dmytrostruk) Sep 28, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this property is applicable to general SemanticFunction logic (Method GetResultsCountOrDefault), we can:

  • Move ResultsPerPrompt parameter to base AIRequestSettings with default value 1 - 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 OpenAIRequestSettings completely and place it as parameter of function. (I had an idea to replace SKContext as input parameter for function/kernel with FunctionRequest/KernelRequest, and property ResultsPerPrompt can 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this result in separate variable?

@rogerbarreto

Copy link
Copy Markdown
Member Author

Dropping this change for now, as I will be working in potentially other decision regarding the Streaming approach for v1

@rogerbarreto
Roger Barreto (rogerbarreto) deleted the features/kernel-streaming-poc1 branch December 5, 2023 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kernel Issues or pull requests impacting the core kernel .NET Issue or Pull requests regarding .NET code PR: breaking change Pull requests that introduce breaking changes PR: ready for review All feedback addressed, ready for reviews

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

.Net: Streaming support for semantic functions on IKernel .NET Allow ISKFunction to return IAsyncEnumerable

4 participants