-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
.Net: Example with native functions using custom types (#3255)
### 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. --> This PR contains changes to add new examples how to use native functions with custom types. Examples include: 1. One native function calls another native function (function chaining). 2. Two native functions are called sequentially in Kernel (function pipeline). 3. Auto conversion of context variables to primitive types in native function parameters. ### 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
4253c39
commit 078f445
Showing
1 changed file
with
227 additions
and
0 deletions.
There are no files selected for viewing
227 changes: 227 additions & 0 deletions
227
dotnet/samples/KernelSyntaxExamples/Example60_AdvancedNativeFunctions.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 |
---|---|---|
@@ -0,0 +1,227 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System; | ||
using System.ComponentModel; | ||
using System.Globalization; | ||
using System.Text.Json; | ||
using System.Threading.Tasks; | ||
using Microsoft.SemanticKernel; | ||
using Microsoft.SemanticKernel.Orchestration; | ||
|
||
/** | ||
* This example shows different ways how to define and execute native functions using custom and primitive types. | ||
*/ | ||
// ReSharper disable once InconsistentNaming | ||
public static class Example60_AdvancedNativeFunctions | ||
{ | ||
public static async Task RunAsync() | ||
{ | ||
await NativeFunctionsChainingAsync(); | ||
|
||
await NativeFunctionsPipelineAsync(); | ||
|
||
await PrimitiveTypesAutoConversionAsync(); | ||
} | ||
|
||
#region Native Functions Chaining | ||
|
||
/// <summary> | ||
/// This example executes Function1, which in turn executes Function2. | ||
/// </summary> | ||
private static async Task NativeFunctionsChainingAsync() | ||
{ | ||
Console.WriteLine("Running Native Function Chaining example..."); | ||
|
||
var kernel = new KernelBuilder().Build(); | ||
|
||
var functions = kernel.ImportFunctions(new FunctionsChainingPlugin(), FunctionsChainingPlugin.PluginName); | ||
|
||
var result = await kernel.RunAsync(functions["Function1"]); | ||
var customType = result.GetValue<MyCustomType>()!; | ||
|
||
Console.WriteLine(customType.Number); // 2 | ||
Console.WriteLine(customType.Text); // From Function1 + From Function2 | ||
} | ||
|
||
/// <summary> | ||
/// Plugin example with two native functions, where one function is called from another. | ||
/// </summary> | ||
private sealed class FunctionsChainingPlugin | ||
{ | ||
public const string PluginName = nameof(FunctionsChainingPlugin); | ||
|
||
[SKFunction, SKName("Function1")] | ||
public async Task<MyCustomType> Function1Async(SKContext context) | ||
{ | ||
// Execute another function | ||
var result = await context.Runner.RunAsync(PluginName, "Function2"); | ||
var value = result.GetValue<MyCustomType>()!; | ||
|
||
return new MyCustomType | ||
{ | ||
Number = 2 * value.Number, | ||
Text = "From Function1 + " + value.Text | ||
}; | ||
} | ||
|
||
[SKFunction, SKName("Function2")] | ||
public static MyCustomType Function2() | ||
{ | ||
return new MyCustomType | ||
{ | ||
Number = 1, | ||
Text = "From Function2" | ||
}; | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Native Functions Pipeline | ||
|
||
/// <summary> | ||
/// This example executes Function1 and Function2 sequentially. | ||
/// Kernel will pass required parameters to second function as result from first function. | ||
/// </summary> | ||
private static async Task NativeFunctionsPipelineAsync() | ||
{ | ||
Console.WriteLine("Running Native Function Pipeline example..."); | ||
|
||
var kernel = new KernelBuilder().Build(); | ||
|
||
var functions = kernel.ImportFunctions(new FunctionsPipelinePlugin(), FunctionsPipelinePlugin.PluginName); | ||
|
||
var result = await kernel.RunAsync(functions["Function1"], functions["Function2"]); | ||
var customType = result.GetValue<MyCustomType>()!; | ||
|
||
Console.WriteLine(customType.Number); // 2 | ||
Console.WriteLine(customType.Text); // From Function1 + From Function2 | ||
} | ||
|
||
/// <summary> | ||
/// Plugin example with two native functions, which will be called sequentially by Kernel. | ||
/// </summary> | ||
private sealed class FunctionsPipelinePlugin | ||
{ | ||
public const string PluginName = nameof(FunctionsPipelinePlugin); | ||
|
||
[SKFunction, SKName("Function1")] | ||
public MyCustomType Function1() | ||
{ | ||
return new MyCustomType | ||
{ | ||
Number = 1, | ||
Text = "From Function1" | ||
}; | ||
} | ||
|
||
[SKFunction, SKName("Function2")] | ||
public static MyCustomType Function2(MyCustomType customType) | ||
{ | ||
return new MyCustomType | ||
{ | ||
Number = customType.Number * 2, | ||
Text = customType.Text + " + From Function2" | ||
}; | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Primitive Types Auto Conversion | ||
|
||
/// <summary> | ||
/// This example shows how to initialize variables, which will be auto-converted to primitive types | ||
/// in parameters of native function. | ||
/// </summary> | ||
private static async Task PrimitiveTypesAutoConversionAsync() | ||
{ | ||
Console.WriteLine("Running Primitive Types Auto Conversion example..."); | ||
|
||
var kernel = new KernelBuilder().Build(); | ||
|
||
var functions = kernel.ImportFunctions(new PrimitiveTypesPlugin(), PrimitiveTypesPlugin.PluginName); | ||
|
||
var contextVariables = new ContextVariables(); | ||
|
||
contextVariables["number"] = "2"; | ||
contextVariables["text"] = "From Context Variables"; | ||
|
||
var result = await kernel.RunAsync(contextVariables, functions["Function1"]); | ||
var customType = result.GetValue<MyCustomType>()!; | ||
|
||
Console.WriteLine(customType.Number); // 2 | ||
Console.WriteLine(customType.Text); // From Context Variables | ||
} | ||
|
||
/// <summary> | ||
/// Plugin example with native function, which contains two parameters with primitive types. | ||
/// </summary> | ||
private sealed class PrimitiveTypesPlugin | ||
{ | ||
public const string PluginName = nameof(PrimitiveTypesPlugin); | ||
|
||
[SKFunction, SKName("Function1")] | ||
public MyCustomType Function1(int number, string text) | ||
{ | ||
return new MyCustomType | ||
{ | ||
Number = number, | ||
Text = text | ||
}; | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Custom Type | ||
|
||
/// <summary> | ||
/// In order to use custom types, <see cref="TypeConverter"/> should be specified, | ||
/// that will convert object instance to string representation. | ||
/// </summary> | ||
/// <remarks> | ||
/// <see cref="TypeConverter"/> is used to represent complex object as meaningful string, so | ||
/// it can be passed to AI for further processing using semantic functions. | ||
/// It's possible to choose any format (e.g. XML, JSON, YAML) to represent your object. | ||
/// </remarks> | ||
[TypeConverter(typeof(MyCustomTypeConverter))] | ||
private sealed class MyCustomType | ||
{ | ||
public int Number { get; set; } | ||
|
||
public string? Text { get; set; } | ||
} | ||
|
||
/// <summary> | ||
/// Implementation of <see cref="TypeConverter"/> for <see cref="MyCustomType"/>. | ||
/// In this example, object instance is serialized with <see cref="JsonSerializer"/> from System.Text.Json, | ||
/// but it's possible to convert object to string using any other serialization logic. | ||
/// </summary> | ||
#pragma warning disable CA1812 // instantiated by Kernel | ||
private sealed class MyCustomTypeConverter : TypeConverter | ||
#pragma warning restore CA1812 | ||
{ | ||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => true; | ||
|
||
/// <summary> | ||
/// This method is used to convert object from string to actual type. This will allow to pass object to | ||
/// native function which requires it. | ||
/// </summary> | ||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) | ||
{ | ||
return JsonSerializer.Deserialize<MyCustomType>((string)value); | ||
} | ||
|
||
/// <summary> | ||
/// This method is used to convert actual type to string representation, so it can be passed to AI | ||
/// for further processing. | ||
/// </summary> | ||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) | ||
{ | ||
return JsonSerializer.Serialize(value); | ||
} | ||
} | ||
|
||
#endregion | ||
} |