Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: New result types - FunctionResult and KernelResult #2864

Merged
merged 31 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
efcfdc3
Introduced FunctionResult and KernelResult
dmytrostruk Sep 18, 2023
ba85008
Fixed null warnings
dmytrostruk Sep 18, 2023
1366c4d
Fixed null reference warnings
dmytrostruk Sep 18, 2023
5fdf312
Small improvement
dmytrostruk Sep 18, 2023
e1d9fbb
Added unit tests to verify FunctionResult
dmytrostruk Sep 18, 2023
7de92ac
Added support for IAsyncEnumerable in FunctionResult
dmytrostruk Sep 18, 2023
6b2240f
Added XML documentation
dmytrostruk Sep 18, 2023
efdc7dc
Merge branch 'main' into new-result-types
dmytrostruk Sep 18, 2023
26f5dae
Run dotnet format
dmytrostruk Sep 18, 2023
5000e3f
Merge branch 'main' into new-result-types
dmytrostruk Sep 19, 2023
3ae474a
Addressed PR feedback
dmytrostruk Sep 19, 2023
e88dd5a
Improved KernelResult
dmytrostruk Sep 19, 2023
b35543d
Merge from main branch
dmytrostruk Sep 19, 2023
256fb2d
Merge branch 'main' into new-result-types
dmytrostruk Sep 19, 2023
e06cedb
Merge branch 'main' into new-result-types
dmytrostruk Sep 19, 2023
524d954
Merge from main branch
dmytrostruk Sep 20, 2023
fa39156
Fixes based on PR feedback
dmytrostruk Sep 20, 2023
42e89e2
Merge from main branch
dmytrostruk Sep 21, 2023
26e11b2
Added FunctionName, PluginName and Metadata to FunctionResult
dmytrostruk Sep 21, 2023
53eb4f6
Added unit test for function results
dmytrostruk Sep 21, 2023
be91c3c
Merge branch 'main' into pr/2864
dmytrostruk Sep 21, 2023
07fdbfe
Merge branch 'main' into new-result-types
dmytrostruk Sep 21, 2023
66536c0
Small improvements
dmytrostruk Sep 21, 2023
9535937
Merge branch 'new-result-types' of https://github.com/dmytrostruk/sem…
dmytrostruk Sep 21, 2023
cebec10
Small fix in Plan. Fixed tests.
dmytrostruk Sep 21, 2023
124e68d
Small fixes
dmytrostruk Sep 21, 2023
07e7a3d
Merge branch 'main' into new-result-types
dmytrostruk Sep 21, 2023
ba9144e
Small update in Plan
dmytrostruk Sep 21, 2023
cfa2c25
Added MADR
dmytrostruk Sep 21, 2023
a0c8124
Merge branch 'main' into pr/2864
dmytrostruk Sep 22, 2023
07e792d
Merge branch 'main' into new-result-types
dmytrostruk Sep 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/decisions/0009-function-and-kernel-result-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
# These are optional elements. Feel free to remove any of them.
status: accepted
date: 2023-09-21
deciders: shawncal, dmytrostruk
consulted:
informed:
---
# Replace SKContext as Function/Kernel result type with FunctionResult and KernelResult models

## Context and Problem Statement

Methods `function.InvokeAsync` and `kernel.RunAsync` return `SKContext` as result type. This has several problems:

1. `SKContext` contains property `Result`, which is `string`. Based on that, it's not possible to return complex type or implement streaming capability in Kernel.
2. `SKContext` contains property `ModelResults`, which is coupled to LLM-specific logic, so it's only applicable to semantic functions in specific cases.
3. `SKContext` as a mechanism of passing information between functions in pipeline should be internal implementation. Caller of Kernel should provide input/request and receive some result, but not `SKContext`.
4. `SKContext` contains information related to the last executed function without a way to access information about specific function in pipeline.

## Decision Drivers

1. Kernel should be able to return complex type as well as support streaming capability.
2. Kernel should be able to return data related to function execution (e.g. amount of tokens used) in a way, when it's not coupled to AI logic.
3. `SKContext` should work as internal mechanism of passing information between functions.
4. There should be a way how to differentiate function result from kernel result, since these entities are different by nature and may contain different set of properties in the future.
5. The possibility to access specific function result in the middle of pipeline will provide more insights to the users how their functions performed.

## Considered Options

1. Use `dynamic` as return type - this option provides some flexibility, but on the other hand removes strong typing, which is preferred option in .NET world. Also, there will be no way how to differentiate function result from Kernel result.
2. Define new types - `FunctionResult` and `KernelResult` - chosen approach.

## Decision Outcome

New `FunctionResult` and `KernelResult` return types should cover scenarios like returning complex types from functions, supporting streaming and possibility to access result of each function separately.

### Complex Types and Streaming

For complex types and streaming, property `object Value` will be defined in `FunctionResult` to store single function result, and in `KernelResult` to store result from last function in execution pipeline. For better usability, generic method `GetValue<T>` will allow to cast `object Value` to specific type.

Examples:

```csharp
// string
var text = (await kernel.RunAsync(function)).GetValue<string>();

// complex type
var myComplexType = (await kernel.RunAsync(function)).GetValue<MyComplexType>();

// streaming
var results = (await kernel.RunAsync(function)).GetValue<IAsyncEnumerable<int>>();

await foreach (var result in results)
{
Console.WriteLine(result);
}
```

When `FunctionResult`/`KernelResult` will store `TypeA` and caller will try to cast it to `TypeB` - in this case `InvalidCastException` will be thrown with details about types. This will provide some information to the caller which type should be used for casting.

### Metadata

To return additional information related to function execution - property `Dictionary<string, object> Metadata` will be added to `FunctionResult`. This will allow to pass any kind of information to the caller, which should provide some insights how function performed (e.g. amount of tokens used, AI model response etc.)

Examples:

```csharp
var functionResult = await function.InvokeAsync(context);
Console.WriteLine(functionResult.Metadata["MyInfo"]);
```

### Multiple function results

`KernelResult` will contain collection of function results - `IReadOnlyCollection<FunctionResult> FunctionResults`. This will allow to get specific function result from `KernelResult`. Properties `FunctionName` and `PluginName` in `FunctionResult` will help to get specific function from collection.

Example:

```csharp
var kernelResult = await kernel.RunAsync(function1, function2, function3);

var functionResult2 = kernelResult.FunctionResults.First(l => l.FunctionName == "Function2" && l.PluginName == "MyPlugin");

Assert.Equal("Result2", functionResult2.GetValue<string>());
```
2 changes: 1 addition & 1 deletion dotnet/samples/ApplicationInsightsExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public static async Task Main()
var result = await kernel.RunAsync(plan);

Console.WriteLine("Result:");
Console.WriteLine(result.Result);
Console.WriteLine(result.GetValue<string>());
}
finally
{
Expand Down
4 changes: 2 additions & 2 deletions dotnet/samples/KernelSyntaxExamples/Example02_Pipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public static async Task RunAsync()
// Load native plugin
var text = kernel.ImportPlugin(new TextPlugin());

SKContext result = await kernel.RunAsync(" i n f i n i t e s p a c e ",
KernelResult result = await kernel.RunAsync(" i n f i n i t e s p a c e ",
text["TrimStart"],
text["TrimEnd"],
text["Uppercase"]);

Console.WriteLine(result);
Console.WriteLine(result.GetValue<string>());
}
}
4 changes: 2 additions & 2 deletions dotnet/samples/KernelSyntaxExamples/Example03_Variables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ public static async Task RunAsync()
var variables = new ContextVariables("Today is: ");
variables.Set("day", DateTimeOffset.Now.ToString("dddd", CultureInfo.CurrentCulture));

SKContext result = await kernel.RunAsync(variables,
KernelResult result = await kernel.RunAsync(variables,
text["AppendDay"],
text["Uppercase"]);

Console.WriteLine(result);
Console.WriteLine(result.GetValue<string>());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,15 @@ private static async Task Example2Async(IKernel kernel)
["externalInformation"] = string.Empty
});

var result = answer.GetValue<string>()!;

// If the answer contains commands, execute them using the prompt renderer.
if (answer.Result.Contains("bing.search", StringComparison.OrdinalIgnoreCase))
if (result.Contains("bing.search", StringComparison.OrdinalIgnoreCase))
{
var promptRenderer = new PromptTemplateEngine();

Console.WriteLine("---- Fetching information from Bing...");
var information = await promptRenderer.RenderAsync(answer.Result, kernel.CreateNewContext());
var information = await promptRenderer.RenderAsync(result, kernel.CreateNewContext());

Console.WriteLine("Information found:");
Console.WriteLine(information);
Expand All @@ -168,7 +170,7 @@ private static async Task Example2Async(IKernel kernel)
}

Console.WriteLine("---- ANSWER:");
Console.WriteLine(answer);
Console.WriteLine(answer.GetValue<string>());

/* OUTPUT:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private static async Task PoetrySamplesAsync()
var result = await kernel.RunAsync(plan);

Console.WriteLine("Result:");
Console.WriteLine(result.Result);
Console.WriteLine(result.GetValue<string>());
}

private static async Task EmailSamplesWithRecallAsync()
Expand Down Expand Up @@ -187,7 +187,7 @@ await foreach (MemoryQueryResult memory in memories)
var result = await kernel.RunAsync(restoredPlan, new(newInput));

Console.WriteLine("Result:");
Console.WriteLine(result.Result);
Console.WriteLine(result.GetValue<string>());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,12 @@ private static async Task ConversationSummaryPluginAsync()
IDictionary<string, ISKFunction> conversationSummaryPlugin =
kernel.ImportPlugin(new ConversationSummaryPlugin(kernel));

SKContext summary = await kernel.RunAsync(
KernelResult summary = await kernel.RunAsync(
ChatTranscript,
conversationSummaryPlugin["SummarizeConversation"]);

Console.WriteLine("Generated Summary:");
Console.WriteLine(summary.Result);
Console.WriteLine(summary.GetValue<string>());
}

private static async Task GetConversationActionItemsAsync()
Expand All @@ -151,12 +151,12 @@ private static async Task GetConversationActionItemsAsync()
IDictionary<string, ISKFunction> conversationSummaryPlugin =
kernel.ImportPlugin(new ConversationSummaryPlugin(kernel));

SKContext summary = await kernel.RunAsync(
KernelResult summary = await kernel.RunAsync(
ChatTranscript,
conversationSummaryPlugin["GetConversationActionItems"]);

Console.WriteLine("Generated Action Items:");
Console.WriteLine(summary.Result);
Console.WriteLine(summary.GetValue<string>());
}

private static async Task GetConversationTopicsAsync()
Expand All @@ -167,12 +167,12 @@ private static async Task GetConversationTopicsAsync()
IDictionary<string, ISKFunction> conversationSummaryPlugin =
kernel.ImportPlugin(new ConversationSummaryPlugin(kernel));

SKContext summary = await kernel.RunAsync(
KernelResult summary = await kernel.RunAsync(
ChatTranscript,
conversationSummaryPlugin["GetConversationTopics"]);

Console.WriteLine("Generated Topics:");
Console.WriteLine(summary.Result);
Console.WriteLine(summary.GetValue<string>());
}

private static IKernel InitializeKernel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ private static async Task CustomTextCompletionWithSKFunctionAsync()

// Details of the my custom model response
Console.WriteLine(JsonSerializer.Serialize(
result.ModelResults,
result.GetModelResults(),
new JsonSerializerOptions() { WriteIndented = true }
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static async Task<string> ListPullRequestsFromGitHubAsync(BearerAuthentic
var result = await kernel.RunAsync(contextVariables, plugin["PullsList"]);

Console.WriteLine("Successful GitHub List Pull Requests plugin response.");
var resultJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(result.Result);
var resultJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(result.GetValue<string>()!);
var pullRequests = JArray.Parse((string)resultJson!["content"]);

if (pullRequests != null && pullRequests.First != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public static async Task RunAsync()
var result = await kernel.RunAsync(contextVariables, jiraSkills["GetIssue"]);

Console.WriteLine("\n\n\n");
var formattedContent = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result.Result), Formatting.Indented);
var formattedContent = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result.GetValue<string>()!), Formatting.Indented);
Console.WriteLine("GetIssue jiraSkills response: \n{0}", formattedContent);
}

Expand All @@ -74,7 +74,7 @@ public static async Task RunAsync()
var result = await kernel.RunAsync(contextVariables, jiraSkills["AddComment"]);

Console.WriteLine("\n\n\n");
var formattedContent = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result.Result), Formatting.Indented);
var formattedContent = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result.GetValue<string>()!), Formatting.Indented);
Console.WriteLine("AddComment jiraSkills response: \n{0}", formattedContent);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.Planning.Action;
using RepoUtils;
Expand Down Expand Up @@ -43,10 +42,10 @@ public static async Task RunAsync()
var plan = await planner.CreatePlanAsync(goal);

// Execute the full plan (which is a single function)
SKContext result = await plan.InvokeAsync(kernel);
var result = await plan.InvokeAsync(kernel);

// Show the result, which should match the given goal
Console.WriteLine(result);
Console.WriteLine(result.GetValue<string>());

/* Output:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static async Task RunAsync()
var result = await plan.InvokeAsync(context);

Console.WriteLine("Result:");
Console.WriteLine(result.Result);
Console.WriteLine(result.GetValue<string>());
Console.WriteLine();
}
/* Example Output
Expand Down Expand Up @@ -154,7 +154,7 @@ public async Task<string> RunMarkupAsync(string docString, SKContext context, IK
Console.WriteLine();

var result = await plan.InvokeAsync(kernel);
return result.Result;
return result.GetValue<string>()!;
}
}

Expand Down
17 changes: 11 additions & 6 deletions dotnet/samples/KernelSyntaxExamples/Example43_GetModelResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AI;
using Microsoft.SemanticKernel.AI.TextCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.ChatCompletion;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Orchestration;
using RepoUtils;

#pragma warning disable RCS1192 // (Unnecessary usage of verbatim string literal)
Expand All @@ -32,17 +34,20 @@ public static async Task RunAsync()
var myFunction = kernel.CreateSemanticFunction(FunctionDefinition);

// Using InvokeAsync with 3 results (Currently invoke only supports 1 result, but you can get the other results from the ModelResults)
var textResult = await myFunction.InvokeAsync("Sci-fi",
var functionResult = await myFunction.InvokeAsync("Sci-fi",
kernel,
requestSettings: new OpenAIRequestSettings { ResultsPerPrompt = 3, MaxTokens = 500, Temperature = 1, TopP = 0.5 });
Console.WriteLine(textResult);
Console.WriteLine(textResult.ModelResults.Select(result => result.GetOpenAIChatResult()).AsJson());

Console.WriteLine(functionResult.GetValue<string>());
Console.WriteLine(functionResult.GetModelResults()?.Select(result => result.GetOpenAIChatResult()).AsJson());
Console.WriteLine();

// Using the Kernel RunAsync
textResult = await kernel.RunAsync("sorry I forgot your birthday", myFunction);
Console.WriteLine(textResult);
Console.WriteLine(textResult.ModelResults.LastOrDefault()?.GetOpenAIChatResult()?.Usage.AsJson());
var kernelResult = await kernel.RunAsync("sorry I forgot your birthday", myFunction);
var modelResults = kernelResult.FunctionResults.SelectMany(l => l.GetModelResults() ?? Enumerable.Empty<ModelResult>());

Console.WriteLine(kernelResult.GetValue<string>());
Console.WriteLine(modelResults.LastOrDefault()?.GetOpenAIChatResult()?.Usage.AsJson());
Console.WriteLine();

// Using Chat Completion directly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ public static async Task GroundednessCheckingSkillAsync()
context.Variables.Set("topic", "people and places");
context.Variables.Set("example_entities", "John, Jane, mother, brother, Paris, Rome");

var extractionResult = (await kernel.RunAsync(context.Variables, entityExtraction)).Result;
var extractionResult = (await kernel.RunAsync(context.Variables, entityExtraction)).GetValue<string>();

Console.WriteLine("======== Extract Entities ========");
Console.WriteLine(extractionResult);

context.Variables.Update(extractionResult);
context.Variables.Set("reference_context", s_groundingText);

var groundingResult = (await kernel.RunAsync(context.Variables, reference_check)).Result;
var groundingResult = (await kernel.RunAsync(context.Variables, reference_check)).GetValue<string>();

Console.WriteLine("======== Reference Check ========");
Console.WriteLine(groundingResult);
Expand All @@ -106,7 +106,7 @@ public static async Task GroundednessCheckingSkillAsync()
var excisionResult = await kernel.RunAsync(context.Variables, entity_excision);

Console.WriteLine("======== Excise Entities ========");
Console.WriteLine(excisionResult.Result);
Console.WriteLine(excisionResult.GetValue<string>());
}

public static async Task PlanningWithGroundednessAsync()
Expand Down Expand Up @@ -142,7 +142,7 @@ public static async Task PlanningWithGroundednessAsync()
Console.WriteLine(plan.ToPlanWithGoalString());

var results = await kernel.RunAsync(s_groundingText, plan);
Console.WriteLine(results.Result);
Console.WriteLine(results.GetValue<string>());
}
}

Expand Down