Merge branch 'microsoft:main' into update-modelid

This commit is contained in:
Sandra Ahlgrimm
2024-01-24 21:33:28 +01:00
committed by GitHub
161 changed files with 3232 additions and 1384 deletions
+150
View File
@@ -0,0 +1,150 @@
---
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: dmytrostruk
date: 2023-01-23
deciders: sergeymenshykh, markwallace, rbarreto, stephentoub, dmytrostruk
---
# Kernel Filters
## Context and Problem Statement
Current way of intercepting some event during function execution works as expected using Kernel Events and event handlers. Example:
```csharp
ILogger logger = loggerFactory.CreateLogger("MyLogger");
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e)
{
logger.LogInformation("Invoking: {FunctionName}", e.Function.Name)
}
void MyInvokedHandler(object? sender, FunctionInvokedEventArgs e)
{
if (e.Result.Metadata is not null && e.Result.Metadata.ContainsKey("Usage"))
{
logger.LogInformation("Token usage: {TokenUsage}", e.Result.Metadata?["Usage"]?.AsJson());
}
}
kernel.FunctionInvoking += MyInvokingHandler;
kernel.FunctionInvoked += MyInvokedHandler;
var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.")
```
There are a couple of problems with this approach:
1. Event handlers does not support dependency injection. It's hard to get access to specific service, which is registered in application, unless the handler is defined in the same scope where specific service is available. This approach provides some limitations in what place in solution the handler could be defined. (e.g. If developer wants to use `ILoggerFactory` in handler, the handler should be defined in place where `ILoggerFactory` instance is available).
2. It's not clear in what specific period of application runtime the handler should be attached to kernel. Also, it's not clear if developer needs to detach it at some point.
3. Mechanism of events and event handlers in .NET may not be familiar to .NET developers who didn't work with events previously.
<!-- This is an optional element. Feel free to remove. -->
## Decision Drivers
1. Dependency injection for handlers should be supported to easily access registered services within application.
2. There should not be any limitations where handlers are defined within solution, whether it's Startup.cs or separate file.
3. There should be clear way of registering and removing handlers at specific point of application runtime.
4. The mechanism of receiving and processing events in Kernel should be easy and common in .NET ecosystem.
5. New approach should support the same functionality that is available in Kernel Events - cancel function execution, change kernel arguments, change rendered prompt before sending it to AI etc.
## Decision Outcome
Introduce Kernel Filters - the approach of receiving the events in Kernel in similar way as action filters in ASP.NET.
Two new abstractions will be used across Semantic Kernel and developers will have to implement these abstractions in a way that will cover their needs.
For function-related events: `IFunctionFilter`
```csharp
public interface IFunctionFilter
{
void OnFunctionInvoking(FunctionInvokingContext context);
void OnFunctionInvoked(FunctionInvokedContext context);
}
```
For prompt-related events: `IPromptFilter`
```csharp
public interface IPromptFilter
{
void OnPromptRendering(PromptRenderingContext context);
void OnPromptRendered(PromptRenderedContext context);
}
```
New approach will allow developers to define filters in separate classes and easily inject required services to process kernel event correctly:
MyFunctionFilter.cs - filter with the same logic as event handler presented above:
```csharp
public sealed class MyFunctionFilter : IFunctionFilter
{
private readonly ILogger _logger;
public MyFunctionFilter(ILoggerFactory loggerFactory)
{
this._logger = loggerFactory.CreateLogger("MyLogger");
}
public void OnFunctionInvoking(FunctionInvokingContext context)
{
this._logger.LogInformation("Invoking {FunctionName}", context.Function.Name);
}
public void OnFunctionInvoked(FunctionInvokedContext context)
{
var metadata = context.Result.Metadata;
if (metadata is not null && metadata.ContainsKey("Usage"))
{
this._logger.LogInformation("Token usage: {TokenUsage}", metadata["Usage"]?.AsJson());
}
}
}
```
As soon as new filter is defined, it's easy to configure it to be used in Kernel using dependency injection (pre-construction) or add filter after Kernel initialization (post-construction):
```csharp
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
// Adding filter with DI (pre-construction)
kernelBuilder.Services.AddSingleton<IFunctionFilter, MyFunctionFilter>();
Kernel kernel = kernelBuilder.Build();
// Adding filter after Kernel initialization (post-construction)
// kernel.FunctionFilters.Add(new MyAwesomeFilter());
var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.");
```
It's also possible to configure multiple filters which will be triggered in order of registration:
```csharp
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter1>();
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter2>();
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter3>();
```
And it's possible to change the order of filter execution in runtime or remove specific filter if needed:
```csharp
kernel.FunctionFilters.Insert(0, new InitialFilter());
kernel.FunctionFilters.RemoveAt(1);
```
+2 -2
View File
@@ -51,7 +51,7 @@
<PackageVersion Include="xretry" Version="1.9.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<!-- Plugins -->
<PackageVersion Include="DocumentFormat.OpenXml" Version="3.0.0" />
<PackageVersion Include="DocumentFormat.OpenXml" Version="3.0.1" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageVersion Include="DuckDB.NET.Data.Full" Version="0.9.2" />
<PackageVersion Include="DuckDB.NET.Data" Version="0.9.2" />
@@ -59,7 +59,7 @@
<PackageVersion Include="Microsoft.Graph" Version="[4.51.0, 5)" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="[3.32.3, )" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="[2.28.0, )" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.11" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.12" />
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.11" />
<PackageVersion Include="Google.Apis.CustomSearchAPI.v1" Version="[1.60.0.3001, )" />
<PackageVersion Include="Grpc.Net.Client" Version="2.60.0" />
+3
View File
@@ -116,6 +116,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144}"
ProjectSection(SolutionItems) = preProject
src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs
src\InternalUtilities\src\System\InternalTypeConverter.cs = src\InternalUtilities\src\System\InternalTypeConverter.cs
src\InternalUtilities\src\System\TypeConverterFactory.cs = src\InternalUtilities\src\System\TypeConverterFactory.cs
src\InternalUtilities\src\System\NonNullCollection.cs = src\InternalUtilities\src\System\NonNullCollection.cs
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Type", "Type", "{E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF}"
+1 -1
View File
@@ -15,7 +15,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
- SKEXP0001: Embedding services
- SKEXP0002: Image services
- SKEXP0003: Memory connectors
- SKEXP0004: Kernel Events
- SKEXP0004: Kernel Filters
## OpenAI and Azure OpenAI services
+2 -2
View File
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.2.0</VersionPrefix>
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix)</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)</PackageVersion>
@@ -10,7 +10,7 @@
<IsPackable>true</IsPackable>
<!-- Package validation. Baseline Version should be lower than current version. -->
<PackageValidationBaselineVersion>1.1.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
<!-- Validate assembly attributes only for Publish builds -->
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
<!-- Do not validate reference assemblies -->
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using RepoUtils;
using Xunit.Abstractions;
@@ -10,9 +11,13 @@ public abstract class BaseTest
{
protected ITestOutputHelper Output { get; }
protected ILoggerFactory LoggerFactory { get; }
protected BaseTest(ITestOutputHelper output)
{
this.Output = output;
this.LoggerFactory = new XunitLogger<object>(output);
LoadUserSecrets();
}
@@ -10,6 +10,8 @@ using Xunit.Abstractions;
namespace Examples;
#pragma warning disable CS0618 // Events are deprecated
public class Example57_KernelHooks : BaseTest
{
/// <summary>
@@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
@@ -60,94 +61,12 @@ provides:
public Task RunAsync()
{
return RunExampleAsync();
//return RunInteractiveAsync();
}
private static async Task RunInteractiveAsync()
private async Task RunExampleAsync()
{
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
using var loggerFactory = LoggerFactory.Create(loggerBuilder =>
loggerBuilder
.AddConsole()
.AddFilter(null, LogLevel.Error));
Dictionary<object, string?> plugins = new()
{
{ webSearchEnginePlugin, "WebSearch" },
{ new TimePlugin(), "Time" }
};
FlowOrchestrator orchestrator = new(
GetKernelBuilder(loggerFactory),
await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
plugins,
config: GetOrchestratorConfig());
var sessionId = Guid.NewGuid().ToString();
Console.WriteLine("*****************************************************");
Console.WriteLine("Executing {0}", nameof(RunInteractiveAsync));
Stopwatch sw = new();
sw.Start();
Console.WriteLine("Flow: " + s_flow.Name);
Console.WriteLine("Please type the question you'd like to ask");
FunctionResult? result = null;
string? goal = null;
do
{
Console.WriteLine("User: ");
string input = Console.ReadLine() ?? string.Empty;
if (string.IsNullOrEmpty(input))
{
Console.WriteLine("No input, exiting");
break;
}
if (string.IsNullOrEmpty(goal))
{
goal = input;
s_flow.Steps.First().Goal = input;
}
try
{
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, input);
}
catch (KernelException ex)
{
Console.WriteLine("Error: " + ex.Message);
Console.WriteLine("Please try again.");
continue;
}
var responses = result.GetValue<List<string>>()!;
foreach (var response in responses)
{
Console.WriteLine("Assistant: " + response);
}
if (result.IsComplete(s_flow))
{
Console.WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]);
Console.WriteLine("\tEmail Payload: " + result.Metadata!["email"]);
Console.WriteLine("Flow completed, exiting");
break;
}
} while (result == null || result.GetValue<List<string>>()?.Count > 0);
Console.WriteLine("Time Taken: " + sw.Elapsed);
Console.WriteLine("*****************************************************");
}
private static async Task RunExampleAsync()
{
var bingConnector = new BingConnector(TestConfiguration.Bing.ApiKey);
var webSearchEnginePlugin = new WebSearchEnginePlugin(bingConnector);
using var loggerFactory = LoggerFactory.Create(loggerBuilder =>
loggerBuilder
.AddConsole()
.AddFilter(null, LogLevel.Error));
Dictionary<object, string?> plugins = new()
{
@@ -156,23 +75,23 @@ provides:
};
FlowOrchestrator orchestrator = new(
GetKernelBuilder(loggerFactory),
GetKernelBuilder(LoggerFactory),
await FlowStatusProvider.ConnectAsync(new VolatileMemoryStore()).ConfigureAwait(false),
plugins,
config: GetOrchestratorConfig());
var sessionId = Guid.NewGuid().ToString();
Console.WriteLine("*****************************************************");
Console.WriteLine("Executing {0}", nameof(RunExampleAsync));
WriteLine("*****************************************************");
WriteLine("Executing " + nameof(RunExampleAsync));
Stopwatch sw = new();
sw.Start();
Console.WriteLine("Flow: " + s_flow.Name);
WriteLine("Flow: " + s_flow.Name);
var question = s_flow.Steps.First().Goal;
var result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, question).ConfigureAwait(false);
Console.WriteLine("Question: " + question);
Console.WriteLine("Answer: " + result.Metadata!["answer"]);
Console.WriteLine("Assistant: " + result.GetValue<List<string>>()!.Single());
WriteLine("Question: " + question);
WriteLine("Answer: " + result.Metadata!["answer"]);
WriteLine("Assistant: " + result.GetValue<List<string>>()!.Single());
string[] userInputs = new[]
{
@@ -185,12 +104,12 @@ provides:
foreach (var t in userInputs)
{
Console.WriteLine($"User: {t}");
WriteLine($"User: {t}");
result = await orchestrator.ExecuteFlowAsync(s_flow, sessionId, t).ConfigureAwait(false);
var responses = result.GetValue<List<string>>()!;
foreach (var response in responses)
{
Console.WriteLine("Assistant: " + response);
WriteLine("Assistant: " + response);
}
if (result.IsComplete(s_flow))
@@ -199,11 +118,11 @@ provides:
}
}
Console.WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]);
Console.WriteLine("\tEmail Payload: " + result.Metadata!["email"]);
WriteLine("\tEmail Address: " + result.Metadata!["email_addresses"]);
WriteLine("\tEmail Payload: " + result.Metadata!["email"]);
Console.WriteLine("Time Taken: " + sw.Elapsed);
Console.WriteLine("*****************************************************");
WriteLine("Time Taken: " + sw.Elapsed);
WriteLine("*****************************************************");
}
private static FlowOrchestratorConfig GetOrchestratorConfig()
@@ -219,6 +138,7 @@ provides:
private static IKernelBuilder GetKernelBuilder(ILoggerFactory loggerFactory)
{
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<ILoggerFactory>(loggerFactory);
return builder
.AddAzureOpenAIChatCompletion(
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Microsoft.SemanticKernel;
using Xunit;
using Xunit.Abstractions;
namespace Examples;
// The following example shows how to receive the results from the kernel in a strongly typed object
// which stores the usage in tokens and converts the JSON result to a strongly typed object, where a validation can also
// be performed
public class Example77_StronglyTypedFunctionResult : BaseTest
{
[Fact]
public async Task RunAsync()
{
this.WriteLine("======== Extended function result ========");
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
var promptTestDataGeneration = "Return a JSON with an array of 3 JSON objects with the following fields: " +
"First, an id field with a random GUID, next a name field with a random company name and last a description field with a random short company description. " +
"Ensure the JSON is valid and it contains a JSON array named testcompanies with the three fields.";
// Time it
var sw = new Stopwatch();
sw.Start();
FunctionResult functionResult = await kernel.InvokePromptAsync(promptTestDataGeneration);
// Stop the timer
sw.Stop();
var functionResultTestDataGen = new FunctionResultTestDataGen(functionResult!, sw.ElapsedMilliseconds);
this.WriteLine($"Test data: {functionResultTestDataGen.Result} \n");
this.WriteLine($"Milliseconds: {functionResultTestDataGen.ExecutionTimeInMilliseconds} \n");
this.WriteLine($"Total Tokens: {functionResultTestDataGen.TokenCounts!.TotalTokens} \n");
}
public Example77_StronglyTypedFunctionResult(ITestOutputHelper output) : base(output)
{
}
/// <summary>
/// Helper classes for the example,
/// put in the same file for simplicity
/// </summary>
/// <remarks>The structure to put the JSON result in a strongly typed object</remarks>
private sealed class RootObject
{
public List<TestCompany> TestCompanies { get; set; }
}
private sealed class TestCompany
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
/// <summary>
/// The FunctionResult custom wrapper to parse the result and the tokens
/// </summary>
private sealed class FunctionResultTestDataGen : FunctionResultExtended
{
public List<TestCompany> TestCompanies { get; set; }
public long ExecutionTimeInMilliseconds { get; init; }
public FunctionResultTestDataGen(FunctionResult functionResult, long executionTimeInMilliseconds)
: base(functionResult)
{
this.TestCompanies = ParseTestCompanies();
this.ExecutionTimeInMilliseconds = executionTimeInMilliseconds;
this.TokenCounts = this.ParseTokenCounts();
}
private TokenCounts? ParseTokenCounts()
{
CompletionsUsage? usage = FunctionResult.Metadata?["Usage"] as CompletionsUsage;
return new TokenCounts(
completionTokens: usage?.CompletionTokens ?? 0,
promptTokens: usage?.PromptTokens ?? 0,
totalTokens: usage?.TotalTokens ?? 0);
}
private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
private List<TestCompany> ParseTestCompanies()
{
// This could also perform some validation logic
var rootObject = JsonSerializer.Deserialize<RootObject>(this.Result, s_jsonSerializerOptions);
List<TestCompany> companies = rootObject!.TestCompanies;
return companies;
}
}
private sealed class TokenCounts
{
public int CompletionTokens { get; init; }
public int PromptTokens { get; init; }
public int TotalTokens { get; init; }
public TokenCounts(int completionTokens, int promptTokens, int totalTokens)
{
CompletionTokens = completionTokens;
PromptTokens = promptTokens;
TotalTokens = totalTokens;
}
}
/// <summary>
/// The FunctionResult extension to provide base functionality
/// </summary>
private class FunctionResultExtended
{
public string Result { get; init; }
public TokenCounts? TokenCounts { get; set; }
public FunctionResult FunctionResult { get; init; }
public FunctionResultExtended(FunctionResult functionResult)
{
this.FunctionResult = functionResult;
this.Result = this.ParseResultFromFunctionResult();
}
private string ParseResultFromFunctionResult()
{
return this.FunctionResult.GetValue<string>() ?? string.Empty;
}
}
}
@@ -2,54 +2,73 @@
using System.Threading.Tasks;
using Examples;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Xunit;
using Xunit.Abstractions;
namespace GettingStarted;
// This example shows how to use rendering event hooks to ensure that prompts are rendered in a responsible manner.
public class Step6_Responsible_AI : BaseTest
{
/// <summary>
/// Show how to use rendering event hooks to ensure that prompts are rendered in a responsible manner.
/// Show how to use prompt filters to ensure that prompts are rendered in a responsible manner.
/// </summary>
[Fact]
public async Task RunAsync()
{
// Create a kernel with OpenAI chat completion
Kernel kernel = Kernel.CreateBuilder()
var builder = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
apiKey: TestConfiguration.OpenAI.ApiKey);
// Handler which is called before a prompt is rendered
void MyRenderingHandler(object? sender, PromptRenderingEventArgs e)
{
if (e.Arguments.ContainsName("card_number"))
{
e.Arguments["card_number"] = "**** **** **** ****";
}
}
builder.Services.AddSingleton<ITestOutputHelper>(this.Output);
// Handler which is called after a prompt is rendered
void MyRenderedHandler(object? sender, PromptRenderedEventArgs e)
{
e.RenderedPrompt += " NO SEXISM, RACISM OR OTHER BIAS/BIGOTRY";
// Add prompt filter to the kernel
builder.Services.AddSingleton<IPromptFilter, PromptFilter>();
WriteLine(e.RenderedPrompt);
}
// Add the handlers to the kernel
kernel.PromptRendering += MyRenderingHandler;
kernel.PromptRendered += MyRenderedHandler;
var kernel = builder.Build();
KernelArguments arguments = new() { { "card_number", "4444 3333 2222 1111" } };
WriteLine(await kernel.InvokePromptAsync("Tell me some useful information about this credit card number {{$card_number}}?", arguments));
var result = await kernel.InvokePromptAsync("Tell me some useful information about this credit card number {{$card_number}}?", arguments);
WriteLine(result);
}
public Step6_Responsible_AI(ITestOutputHelper output) : base(output)
{
}
private sealed class PromptFilter : IPromptFilter
{
private readonly ITestOutputHelper _output;
public PromptFilter(ITestOutputHelper output)
{
this._output = output;
}
/// <summary>
/// Method which is called before a prompt is rendered.
/// </summary>
public void OnPromptRendered(PromptRenderedContext context)
{
context.RenderedPrompt += " NO SEXISM, RACISM OR OTHER BIAS/BIGOTRY";
this._output.WriteLine(context.RenderedPrompt);
}
/// <summary>
/// Method which is called after a prompt is rendered.
/// </summary>
public void OnPromptRendering(PromptRenderingContext context)
{
if (context.Arguments.ContainsName("card_number"))
{
context.Arguments["card_number"] = "**** **** **** ****";
}
}
}
}
@@ -4,6 +4,7 @@ using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Examples;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using RepoUtils;
@@ -15,17 +16,48 @@ namespace GettingStarted;
public class Step7_Observability : BaseTest
{
/// <summary>
/// Shows different ways observe the execution of a <see cref="KernelPlugin"/> instances.
/// Shows how to observe the execution of a <see cref="KernelPlugin"/> instance with filters.
/// </summary>
[Fact]
public async Task RunAsync()
public async Task ObservabilityWithFiltersAsync()
{
// Create a kernel with OpenAI chat completion
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
kernelBuilder.Plugins.AddFromType<TimeInformation>();
// Add filter using DI
kernelBuilder.Services.AddSingleton<ITestOutputHelper>(this.Output);
kernelBuilder.Services.AddSingleton<IFunctionFilter, MyFunctionFilter>();
Kernel kernel = kernelBuilder.Build();
// Add filter without DI
kernel.PromptFilters.Add(new MyPromptFilter(this.Output));
// Invoke the kernel with a prompt and allow the AI to automatically invoke functions
OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };
WriteLine(await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings)));
}
/// <summary>
/// Shows how to observe the execution of a <see cref="KernelPlugin"/> instance with hooks.
/// </summary>
[Fact]
[Obsolete("Events are deprecated in favor of filters.")]
public async Task ObservabilityWithHooksAsync()
{
// Create a kernel with OpenAI chat completion
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
kernelBuilder.Plugins.AddFromType<TimeInformation>();
Kernel kernel = kernelBuilder.Build();
// Handler which is called before a function is invoked
@@ -34,6 +66,12 @@ public class Step7_Observability : BaseTest
WriteLine($"Invoking {e.Function.Name}");
}
// Handler which is called before a prompt is rendered
void MyRenderingHandler(object? sender, PromptRenderingEventArgs e)
{
WriteLine($"Rendering prompt for {e.Function.Name}");
}
// Handler which is called after a prompt is rendered
void MyRenderedHandler(object? sender, PromptRenderedEventArgs e)
{
@@ -51,6 +89,7 @@ public class Step7_Observability : BaseTest
// Add the handlers to the kernel
kernel.FunctionInvoking += MyInvokingHandler;
kernel.PromptRendering += MyRenderingHandler;
kernel.PromptRendered += MyRenderedHandler;
kernel.FunctionInvoked += MyInvokedHandler;
@@ -62,13 +101,64 @@ public class Step7_Observability : BaseTest
/// <summary>
/// A plugin that returns the current time.
/// </summary>
public class TimeInformation
private sealed class TimeInformation
{
[KernelFunction]
[Description("Retrieves the current time in UTC.")]
public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R");
}
/// <summary>
/// Function filter for observability.
/// </summary>
private sealed class MyFunctionFilter : IFunctionFilter
{
private readonly ITestOutputHelper _output;
public MyFunctionFilter(ITestOutputHelper output)
{
this._output = output;
}
public void OnFunctionInvoked(FunctionInvokedContext context)
{
var metadata = context.Result.Metadata;
if (metadata is not null && metadata.ContainsKey("Usage"))
{
this._output.WriteLine($"Token usage: {metadata["Usage"]?.AsJson()}");
}
}
public void OnFunctionInvoking(FunctionInvokingContext context)
{
this._output.WriteLine($"Invoking {context.Function.Name}");
}
}
/// <summary>
/// Prompt filter for observability.
/// </summary>
private sealed class MyPromptFilter : IPromptFilter
{
private readonly ITestOutputHelper _output;
public MyPromptFilter(ITestOutputHelper output)
{
this._output = output;
}
public void OnPromptRendered(PromptRenderedContext context)
{
this._output.WriteLine($"Prompt sent to model: {context.RenderedPrompt}");
}
public void OnPromptRendering(PromptRenderingContext context)
{
this._output.WriteLine($"Rendering prompt for {context.Function.Name}");
}
}
public Step7_Observability(ITestOutputHelper output) : base(output)
{
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace RepoUtils;
/// <summary>
/// A logger that writes to the Xunit test output
/// </summary>
internal sealed class XunitLogger<T> : ILoggerFactory, ILogger, IDisposable
{
private readonly ITestOutputHelper _output;
public XunitLogger(ITestOutputHelper output)
{
this._output = output;
}
/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
this._output.WriteLine(state?.ToString());
}
/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel) => true;
/// <inheritdoc/>
public IDisposable BeginScope<TState>(TState state) where TState : notnull
=> this;
/// <inheritdoc/>
public void Dispose()
{
// This class is marked as disposable to support the BeginScope method.
// However, there is no need to dispose anything.
}
public ILogger CreateLogger(string categoryName) => this;
public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
}
@@ -117,12 +117,10 @@ internal class FlowExecutor : IFlowExecutor
this._flowStatusProvider = statusProvider;
this._globalPluginCollection = globalPluginCollection;
var checkRepeatStepConfig = PromptTemplateConfig.FromJson(EmbeddedResource.Read("Plugins.CheckRepeatStep.config.json")!);
checkRepeatStepConfig.Template = EmbeddedResource.Read("Plugins.CheckRepeatStep.skprompt.txt")!;
var checkRepeatStepConfig = this.ImportPromptTemplateConfig("CheckRepeatStep");
this._checkRepeatStepFunction = KernelFunctionFactory.CreateFromPrompt(checkRepeatStepConfig);
var checkStartStepConfig = PromptTemplateConfig.FromJson(EmbeddedResource.Read("Plugins.CheckStartStep.config.json")!);
checkStartStepConfig.Template = EmbeddedResource.Read("Plugins.CheckStartStep.skprompt.txt")!;
var checkStartStepConfig = this.ImportPromptTemplateConfig("CheckStartStep");
this._checkStartStepFunction = KernelFunctionFactory.CreateFromPrompt(checkStartStepConfig);
this._config.ExcludedPlugins.Add(RestrictedPluginName);
@@ -132,6 +130,23 @@ internal class FlowExecutor : IFlowExecutor
this._executeStepFunction = KernelFunctionFactory.CreateFromMethod(this.ExecuteStepAsync, "ExecuteStep", "Execute a flow step");
}
private PromptTemplateConfig ImportPromptTemplateConfig(string functionName)
{
var config = KernelFunctionYaml.ToPromptTemplateConfig(EmbeddedResource.Read($"Plugins.{functionName}.yaml")!);
// if AIServiceIds is specified, only include the relevant execution settings
if (this._config.AIServiceIds.Count > 0)
{
var serviceIdsToRemove = config.ExecutionSettings.Keys.Except(this._config.AIServiceIds);
foreach (var serviceId in serviceIdsToRemove)
{
config.ExecutionSettings.Remove(serviceId);
}
}
return config;
}
public async Task<FunctionResult> ExecuteFlowAsync(Flow flow, string sessionId, string input, KernelArguments kernelArguments)
{
Verify.NotNull(flow, nameof(flow));
@@ -262,12 +277,11 @@ internal class FlowExecutor : IFlowExecutor
if (!string.IsNullOrEmpty(stepResult.ToString()) && (stepResult.IsPromptInput() || stepResult.IsTerminateFlow()))
{
try
if (stepResult.ValueType == typeof(List<string>))
{
var stepOutputs = JsonSerializer.Deserialize<string[]>(stepResult.ToString());
outputs.AddRange(stepOutputs!);
outputs.AddRange(stepResult.GetValue<List<string>>()!);
}
catch (JsonException)
else
{
outputs.Add(stepResult.ToString());
}
@@ -276,9 +290,15 @@ internal class FlowExecutor : IFlowExecutor
{
stepState.Status = ExecutionState.Status.Completed;
var metadata = stepResult.Metadata!
.Where(kvp => step.Provides.Contains(kvp.Key))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var metadata = stepResult.Metadata!.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
foreach (var variable in step.Provides)
{
if (!metadata.ContainsKey(variable))
{
metadata[variable] = string.Empty;
}
}
stepResult = new FunctionResult(stepResult.Function, stepResult.GetValue<object>(), metadata: metadata);
if (!string.IsNullOrWhiteSpace(exitResponse))
@@ -538,7 +558,7 @@ internal class FlowExecutor : IFlowExecutor
}
this._logger.LogWarning("Missing result tag from {Function} : {ActionText}", "CheckRepeatOrStartStep", llmResponseText);
chatHistory.AddSystemMessage(llmResponseText + "\nI should provide either [QUESTION] or [FINAL_ANSWER]");
chatHistory.AddSystemMessage(llmResponseText + "\nI should provide either [QUESTION] or [FINAL_ANSWER].");
await this._flowStatusProvider.SaveChatHistoryAsync(sessionId, checkRepeatOrStartStepId, chatHistory).ConfigureAwait(false);
return null;
}
@@ -603,7 +623,15 @@ internal class FlowExecutor : IFlowExecutor
this._logger.LogInformation("Thought: {Thought}", actionStep.Thought);
}
if (!string.IsNullOrEmpty(actionStep.Action!))
if (!string.IsNullOrEmpty(actionStep.FinalAnswer))
{
if (step.Provides.Count() == 1)
{
arguments[step.Provides.Single()] = actionStep.FinalAnswer;
return new FunctionResult(this._executeStepFunction, actionStep.FinalAnswer, metadata: arguments);
}
}
else if (!string.IsNullOrEmpty(actionStep.Action!))
{
if (actionStep.Action!.Contains(Constants.StopAndPromptFunctionName))
{
@@ -727,14 +755,6 @@ internal class FlowExecutor : IFlowExecutor
this._logger?.LogWarning("Action: No result from action");
}
else if (!string.IsNullOrEmpty(actionStep.FinalAnswer))
{
if (step.Provides.Count() == 1)
{
arguments[step.Provides.Single()] = actionStep.FinalAnswer;
return new FunctionResult(this._executeStepFunction, actionStep.FinalAnswer, metadata: arguments);
}
}
else
{
actionStep.Observation = "ACTION $JSON_BLOB must be provided as part of thought process.";
@@ -93,34 +93,19 @@ internal sealed class ReActEngine
var promptConfig = config.ReActPromptTemplateConfig;
if (promptConfig is null)
{
promptConfig = new PromptTemplateConfig();
string promptConfigString = EmbeddedResource.Read("Plugins.ReActEngine.config.json")!;
string promptConfigString = EmbeddedResource.Read("Plugins.ReActEngine.yaml")!;
if (!string.IsNullOrEmpty(modelId))
{
var modelConfigString = EmbeddedResource.Read($"Plugins.ReActEngine.{modelId}.config.json", false);
var modelConfigString = EmbeddedResource.Read($"Plugins.ReActEngine.{modelId}.yaml", false);
promptConfigString = string.IsNullOrEmpty(modelConfigString) ? promptConfigString : modelConfigString!;
}
if (!string.IsNullOrEmpty(promptConfigString))
{
promptConfig = PromptTemplateConfig.FromJson(promptConfigString);
}
else
{
promptConfig.SetMaxTokens(config.MaxTokens);
}
}
promptConfig = KernelFunctionYaml.ToPromptTemplateConfig(promptConfigString);
var promptTemplate = config.ReActPromptTemplate;
if (string.IsNullOrEmpty(promptTemplate))
{
promptTemplate = EmbeddedResource.Read("Plugins.ReActEngine.skprompt.txt")!;
if (!string.IsNullOrEmpty(promptTemplate))
if (!string.IsNullOrEmpty(modelId))
{
var modelPromptTemplate = EmbeddedResource.Read($"Plugins.ReActEngine.{modelId}.skprompt.txt", false);
promptConfig.Template = string.IsNullOrEmpty(modelPromptTemplate) ? promptTemplate : modelPromptTemplate!;
var modelConfigString = EmbeddedResource.Read($"Plugins.ReActEngine.{modelId}.yaml", false);
promptConfigString = string.IsNullOrEmpty(modelConfigString) ? promptConfigString : modelConfigString!;
}
}
@@ -174,11 +159,12 @@ internal sealed class ReActEngine
var actionStep = this.ParseResult(llmResponseText);
if (!string.IsNullOrEmpty(actionStep.Action) || previousSteps.Count == 0)
if (!string.IsNullOrEmpty(actionStep.Action) || previousSteps.Count == 0 || !string.IsNullOrEmpty(actionStep.FinalAnswer))
{
return actionStep;
}
actionStep.Thought = llmResponseText;
actionStep.Observation = "Failed to parse valid action step, missing action or final answer.";
this._logger?.LogWarning("Failed to parse valid action step from llm response={LLMResponseText}", llmResponseText);
this._logger?.LogWarning("Scratchpad={ScratchPad}", scratchPad);
@@ -23,9 +23,7 @@
<InternalsVisibleTo Include="SemanticKernel.Experimental.Orchestration.Flow.UnitTests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Functions\Functions.Yaml\Functions.Yaml.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj" />
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
</ItemGroup>
@@ -46,11 +46,6 @@ public sealed class FlowOrchestratorConfig
/// </summary>
public int MinIterationTimeMs { get; set; } = 0;
/// <summary>
/// Optional. The prompt template override for ReAct engine.
/// </summary>
public string? ReActPromptTemplate { get; set; } = null;
/// <summary>
/// Optional. The prompt template configuration override for the ReAct engine.
/// </summary>
@@ -61,12 +56,17 @@ public sealed class FlowOrchestratorConfig
/// </summary>
public bool EnableAutoTermination { get; set; } = false;
/// <summary>
/// Optional. The allowed AI service id for the React engine.
/// </summary>
public HashSet<string> AIServiceIds { get; set; } = new();
/// <summary>
/// Optional. The AI request settings for the ReAct engine.
/// </summary>
/// <remarks>
/// Prompt used for reasoning may be different for different models, the prompt selection would be based on the PromptExecutionSettings.
/// if the built in prompt template does not work for your model, suggest to override it with <see cref="ReActPromptTemplate"/>.
/// if the built in prompt template does not work for your model, suggest to override it with <see cref="ReActPromptTemplateConfig"/>.
/// </remarks>
public PromptExecutionSettings? AIRequestSettings { get; set; } = null;
}
@@ -0,0 +1,69 @@
template_format: semantic-kernel
template: |
[INSTRUCTION]
Work with user to determine if he or she would like to work on the previous step for one more time.
[THOUGHT PROCESS]
[Goal]
The goal of proposed step.
[THOUGHT]
To solve this problem, I should carefully analyze the previous question and response to identify if user is willing to repeat the action again.
Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
[QUESTION]
If there is any ambiguity in the chat history, ask the follow up question to the user to get clarification on whether the user wants to repeat the previous step.
The way you will check if the user wants to repeat the step is by asking the question "{{$transitionMessage}}".
IMPORTANT: Do NOT update the wording in the question stated above. If you need clarification, you will ask that question word for word.
If the user says something along the lines of "yes", "sure", "fine", "ok", then they are asking to repeat the step.
[RESPONSE]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/QUESTION/RESPONSE can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and can reliably tell if user would like to repeat the step for one more time, output TRUE or FALSE.
[END THOUGHT PROCESS]
Example:
[Goal]
{{$goal}}
[QUESTION]
{{$transitionMessage}}
[RESPONSE]
yes
[THOUGHT]
Based on the response, the user wants to try the previous step again.
[FINAL ANSWER]
TRUE
IMPORTANT REMINDER: Your each response MUST contain one of [QUESTION] and [FINAL ANSWER]!
Let's break down the problem step by step and think about the best approach.
Begin!
[Goal]
{{$goal}}
{{$agentScratchPad}}
[THOUGHT]
description: Given the chat history, determine if user would like to execute the previous task for one more time. If not concluded, generate the next message for follow up.
name: CheckRepeatStep
input_variables:
- name: goal
description: The goal of proposed step
- name: transitionMessage
description: The transition message
default: Do you want to try the previous step again?
- name: agentScratchPad
description: The agent's scratch pad
execution_settings:
text-davinci-003:
temperature: 0.0
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[RESPONSE]"]
default:
temperature: 0.0
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[RESPONSE]"]
@@ -1,32 +0,0 @@
{
"schema": 1,
"name": "CheckRepeatStep",
"description": "Given the chat history, determine if user would like to execute the previous task for one more time. If not concluded, generate the next message for follow up.",
"execution_settings": {
"default": {
"max_tokens": 400,
"temperature": 0,
"top_p": 1,
"presence_penalty": 0.0,
"frequency_penalty": 0.0,
"stop_sequences": ["[RESPONSE]"]
}
},
"input_variables": [
{
"name": "goal",
"description": "The goal of proposed step",
"default": ""
},
{
"name": "transitionMessage",
"description": "The transition message",
"default": "Did you want to try the previous step again?"
},
{
"name": "agentScratchPad",
"description": "The agent's scratch pad",
"default": ""
}
]
}
@@ -1,42 +0,0 @@
[INSTRUCTION]
Work with user to determine if he or she would like to work on the previous step for one more time.
[THOUGHT PROCESS]
[Goal]
The goal of proposed step.
[THOUGHT]
To solve this problem, I should carefully analyze the previous question and response to identify if user is willing to repeat the action again.
Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
[QUESTION]
If there is any ambiguity in the chat history, ask the follow up question to the user to get clarification on whether the user wants to repeat the previous step.
The way you will check if the user wants to repeat the step is by asking the question "{{$transitionMessage}}".
IMPORTANT: Do NOT update the wording in the question stated above. If you need clarification, you will ask that question word for word.
If the user says something along the lines of "yes", "sure", "fine", "ok", then they are asking to repeat the step.
[RESPONSE]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/QUESTION/RESPONSE can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and can reliably tell if user would like to repeat the step for one more time, output TRUE or FALSE.
[END THOUGHT PROCESS]
Example:
[Goal]
{{$goal}}
[QUESTION]
{{$transitionMessage}}
[RESPONSE]
yes
[THOUGHT]
Based on the response, the user wants to try the previous step again.
[FINAL ANSWER]
TRUE
IMPORTANT REMINDER: Your each response MUST contain one of [QUESTION] and [FINAL ANSWER]!
Let's break down the problem step by step and think about the best approach.
Begin!
[Goal]
{{$goal}}
{{$agentScratchPad}}
[THOUGHT]
@@ -0,0 +1,68 @@
template_format: semantic-kernel
template: |
[INSTRUCTION]
Work with user to determine if he or she would like to execute the current step for the first time.
[THOUGHT PROCESS]
[Goal]
The goal of proposed step.
[THOUGHT]
To solve this problem, I should carefully analyze the question and response to identify if user is willing to begin the current action.
Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
[QUESTION]
If there is any ambiguity in the chat history, ask the follow up question to the user to get clarification on whether the user wants to repeat the previous step.
The way you will check if the user wants to execute the step is by asking the question "{{$message}}".
IMPORTANT: Do NOT update the wording in the question stated above. If you need clarification, you will ask that question word for word.
If the user says something along the lines of "yes", "sure", "fine", "ok", then they are asking to start the step.
[RESPONSE]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/QUESTION/RESPONSE can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and can reliably tell if user would like to repeat the step for one more time, output TRUE or FALSE.
[END THOUGHT PROCESS]
Example:
[Goal]
{{$goal}}
[QUESTION]
{{$message}}
[RESPONSE]
yes
[THOUGHT]
Based on the response, the user wants to execute the current step.
[FINAL ANSWER]
TRUE
IMPORTANT REMINDER: your each response should contain at most one question. Do not provide more than one step.
Let's break down the problem step by step and think about the best approach.
Begin!
[Goal]
{{$goal}}
{{$agentScratchPad}}
[THOUGHT]
description: Given the chat history, determine if user would like to execute the previous task for one more time. If not concluded, generate the next message for follow up.
name: CheckRepeatStep
input_variables:
- name: goal
description: The goal of proposed step
- name: message
description: he message to display to the user
- name: agentScratchPad
description: The agent's scratch pad
execution_settings:
text-davinci-003:
temperature: 0.0
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[RESPONSE]"]
default:
temperature: 0.0
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[RESPONSE]"]
@@ -1,32 +0,0 @@
{
"schema": 1,
"name": "CheckStartStep",
"description": "Given the chat history, determine if user would like to begin executing the current task. If not concluded, generate the next message for follow up.",
"execution_settings": {
"default": {
"max_tokens": 400,
"temperature": 0,
"top_p": 1,
"presence_penalty": 0.0,
"frequency_penalty": 0.0,
"stop_sequences": ["[RESPONSE]"]
}
},
"input_variables": [
{
"name": "goal",
"description": "The goal of proposed step",
"default": ""
},
{
"name": "message",
"description": "The message to display to the user",
"default": ""
},
{
"name": "agentScratchPad",
"description": "The agent's scratch pad",
"default": ""
}
]
}
@@ -1,42 +0,0 @@
[INSTRUCTION]
Work with user to determine if he or she would like to execute the current step for the first time.
[THOUGHT PROCESS]
[Goal]
The goal of proposed step.
[THOUGHT]
To solve this problem, I should carefully analyze the question and response to identify if user is willing to begin the current action.
Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
[QUESTION]
If there is any ambiguity in the chat history, ask the follow up question to the user to get clarification on whether the user wants to repeat the previous step.
The way you will check if the user wants to execute the step is by asking the question "{{$message}}".
IMPORTANT: Do NOT update the wording in the question stated above. If you need clarification, you will ask that question word for word.
If the user says something along the lines of "yes", "sure", "fine", "ok", then they are asking to start the step.
[RESPONSE]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/QUESTION/RESPONSE can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and can reliably tell if user would like to repeat the step for one more time, output TRUE or FALSE.
[END THOUGHT PROCESS]
Example:
[Goal]
{{$goal}}
[QUESTION]
{{$message}}
[RESPONSE]
yes
[THOUGHT]
Based on the response, the user wants to execute the current step.
[FINAL ANSWER]
TRUE
IMPORTANT REMINDER: your each response should contain at most one question. Do not provide more than one step.
Let's break down the problem step by step and think about the best approach.
Begin!
[Goal]
{{$goal}}
{{$agentScratchPad}}
[THOUGHT]
@@ -0,0 +1,81 @@
template_format: semantic-kernel
template: |
[INSTRUCTION]
Answer the following questions as accurately as possible using the provided functions.
[AVAILABLE FUNCTIONS]
The function definitions below are in the following format:
<functionName>: <description>
- <parameterName>: <parameterDescription>
- ...
{{$functionDescriptions}}
[END AVAILABLE FUNCTIONS]
[USAGE INSTRUCTIONS]
To use the functions, specify a JSON blob representing an action. The JSON blob should contain fully qualified name of the function to use, and an "action_variables" key with a JSON object of string values to use when calling the function.
Do not call functions directly; they must be invoked through an action.
The "action_variables" values should match the defined [PARAMETERS] of the named "action" in [AVAILABLE FUNCTIONS].
Dictionary values in "action_variables" must be strings and represent the actual values to be passed to the function.
Ensure that the $JSON_BLOB contains only a SINGLE action; do NOT return multiple actions.
IMPORTANT: Use only the available functions listed in the [AVAILABLE FUNCTIONS] section. Do not attempt to use any other functions that are not specified.
The value of parameters should either by empty if the expectation is for the user to provide them and have not been provided yet, or derived from the agent scratchpad.
You are not allowed to ask user directly for more information.
Here is an example of a valid $JSON_BLOB:
{
"action": "FUNCTION.NAME",
"action_variables": {"INPUT": "some input", "PARAMETER_NAME": "some value", "PARAMETER_NAME_2": "42"}
}
[END USAGE INSTRUCTIONS]
[END INSTRUCTION]
[THOUGHT PROCESS]
[QUESTION]
The input question I must answer
[THOUGHT]
To solve this problem, I should carefully analyze the given question and identify the necessary steps. Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
If there is function which can be leveraged for validation, use it in ACTION before jumping into FINAL ANSWER.
[ACTION]
$JSON_BLOB
[OBSERVATION]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/ACTION/OBSERVATION can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and performed any required actions, if there is a suitable function for validation, provide the final answer in JSON in the following format:
{ "action": "$(last_action_name)", "action_variables": {"INPUT": "some input", "PARAMETER_NAME": "some value", "PARAMETER_NAME_2": "42"}}
If there is not a fitting function available to validate the result, I can provide the final answer in a clear and human-readable format.
[END THOUGHT PROCESS]
IMOPRTANT REMINDER: your each response should contain only one next step and only single one $JSON_BLOB. Do not provide more than one step.
Let's break down the problem step by step and think about the best approach.
Begin!
[QUESTION]
{{$question}}
{{$agentScratchPad}}
description: Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.
name: ReActEngine
input_variables:
- name: question
description: The question to answer
- name: agentScratchPad
description: The agent's scratch pad
- name: functionDescriptions
description: The manual of the agent's functions
execution_settings:
text-davinci-003:
temperature: 0.0
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[OBSERVATION]", "[Observation]", "[QUESTION]"]
default:
temperature: 0.1
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[OBSERVATION]", "[Observation]", "[QUESTION]"]
@@ -0,0 +1,104 @@
template_format: semantic-kernel
template: |
[INSTRUCTION]
Answer the following questions as accurately as possible using the provided functions.
[AVAILABLE FUNCTIONS]
The function definitions below are in the following format:
<functionName>: <description>
- <parameterName>: <parameterDescription>
- ...
[END AVAILABLE FUNCTIONS]
[USAGE INSTRUCTIONS]
To use the functions, specify a JSON blob representing an action. The JSON blob should contain fully qualified name of the function to use, and an "action_variables" key with a JSON object of string values to use when calling the function.
Do not call functions directly; they must be invoked through an action.
Here is an example of a valid $JSON_BLOB:
```
{
"action": "_Namespace_.FUNCTION.NAME",
"action_variables": {"PARAMETER_NAME_1": "some value", "PARAMETER_NAME_2": "42"}
}
```
The keys of "action_variables" should match the defined [PARAMETERS] of the named "action" in [AVAILABLE FUNCTIONS].
Dictionary values in "action_variables" must be strings and represent the actual values to be passed to the function.
Ensure that the $JSON_BLOB contains only a SINGLE action; do NOT return multiple actions.
IMPORTANT:
* Use only the available functions listed in the [AVAILABLE FUNCTIONS] section.
* Do not attempt to use any other functions that are not specified.
* The value of parameters should either by empty if the expectation is for the user to provide them and have not been provided yet, or derived from the agent scratchpad.
* You are not allowed to ask user directly for more information.
[END USAGE INSTRUCTIONS]
[END INSTRUCTION]
[THOUGHT PROCESS]
[QUESTION]
The input question I must answer
[THOUGHT]
To solve this problem, I should carefully analyze the given question and identify the necessary steps. Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
If there is function which can be leveraged for validation, use it in ACTION before jumping into FINAL ANSWER.
[ACTION]
$JSON_BLOB
[OBSERVATION]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/ACTION/OBSERVATION can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and performed any required actions, provide the final answer in a clear and human-readable format.
[END THOUGHT PROCESS]
Example:
[AVAILABLE FUNCTIONS]
AuthorPlugin.WritePoem: useful to write poem given a style and input
- input: input for the poem
- style: style of the poem, leave empty if not specified
[END AVAILABLE FUNCTIONS]
[QUESTION]
Write a poem about sun in Whitman's style.
[THOUGHT]
I should use WritePoem function for it.
[ACTION]
{
"action": "AuthorPlugin.WritePoem",
"action_variables": {
"input": "sun",
"stype": "Whitman",
}
}
IMPORTANT REMINDER: your each response should contain only one next step and only single one [ACTION] part. Do not provide more than one step.
Let's break down the problem step by step and think about the best approach.
Begin!
[AVAILABLE FUNCTIONS]
{{$functionDescriptions}}
[END AVAILABLE FUNCTIONS]
[QUESTION]
{{$question}}
{{$agentScratchPad}}
[THOUGHT]
description: Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.
name: ReActEngine
input_variables:
- name: question
description: The question to answer
- name: agentScratchPad
description: The agent's scratch pad
- name: functionDescriptions
description: The manual of the agent's functions
execution_settings:
text-davinci-003:
temperature: 0.0
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[OBSERVATION]", "[Observation]", "[QUESTION]", "[AVAILABLE FUNCTIONS]"]
default:
temperature: 0.1
top_p: 1.0
presence_penalty: 0.0
frequency_penalty: 0.0
max_tokens: 400
stop_sequences: ["[OBSERVATION]", "[Observation]", "[QUESTION]", "[AVAILABLE FUNCTIONS]"]
@@ -1,31 +0,0 @@
{
"schema": 1,
"description": "Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.",
"execution_settings": {
"default": {
"max_tokens": 400,
"temperature": 0.1,
"top_p": 1,
"presence_penalty": 0.0,
"frequency_penalty": 0.0,
"stop_sequences": ["[OBSERVATION]", "[Observation]", "[QUESTION]"]
}
},
"input_variables": [
{
"name": "question",
"description": "The question to answer",
"default": ""
},
{
"name": "agentScratchPad",
"description": "The agent's scratch pad",
"default": ""
},
{
"name": "functionDescriptions",
"description": "The manual of the agent's functions",
"default": ""
}
]
}
@@ -1,32 +0,0 @@
{
"schema": 1,
"description":
"Given a request or command or goal generate multi-step plan to reach the goal. After each step LLM is called to perform the reasoning for the next step.",
"execution_settings": {
"default": {
"max_tokens": 400,
"temperature": 0.1,
"top_p": 1,
"presence_penalty": 0.0,
"frequency_penalty": 0.0,
"stop_sequences": ["[OBSERVATION]"]
}
},
"input_variables": [
{
"name": "question",
"description": "The question to answer",
"default": ""
},
{
"name": "agentScratchPad",
"description": "The agent's scratch pad",
"default": ""
},
{
"name": "functionDescriptions",
"description": "The manual of the agent's functions",
"default": ""
}
]
}
@@ -1,55 +0,0 @@
[INSTRUCTION]
Answer the following questions as accurately as possible using the provided functions.
[AVAILABLE FUNCTIONS]
The function definitions below are in the following format:
<functionName>: <description>
- <parameterName>: <parameterDescription>
- ...
{{$functionDescriptions}}
[END AVAILABLE FUNCTIONS]
[USAGE INSTRUCTIONS]
To use the functions, specify a JSON blob representing an action. The JSON blob should contain fully qualified name of the function to use, and an "action_variables" key with a JSON object of string values to use when calling the function.
Do not call functions directly; they must be invoked through an action.
The "action_variables" values should match the defined [PARAMETERS] of the named "action" in [AVAILABLE FUNCTIONS].
Dictionary values in "action_variables" must be strings and represent the actual values to be passed to the function.
Ensure that the $JSON_BLOB contains only a SINGLE action; do NOT return multiple actions.
IMPORTANT: Use only the available functions listed in the [AVAILABLE FUNCTIONS] section. Do not attempt to use any other functions that are not specified.
The value of parameters should either by empty if the expectation is for the user to provide them and have not been provided yet, or derived from the agent scratchpad.
You are not allowed to ask user directly for more information.
Here is an example of a valid $JSON_BLOB:
{
"action": "FUNCTION.NAME",
"action_variables": {"INPUT": "some input", "PARAMETER_NAME": "some value", "PARAMETER_NAME_2": "42"}
}
[END USAGE INSTRUCTIONS]
[END INSTRUCTION]
[THOUGHT PROCESS]
[QUESTION]
The input question I must answer
[THOUGHT]
To solve this problem, I should carefully analyze the given question and identify the necessary steps. Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
If there is function which can be leveraged for validation, use it in ACTION before jumping into FINAL ANSWER.
[ACTION]
$JSON_BLOB
[OBSERVATION]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/ACTION/OBSERVATION can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and performed any required actions, if there is a suitable function for validation, provide the final answer in JSON in the following format:
{ "action": "$(last_action_name)", "action_variables": {"INPUT": "some input", "PARAMETER_NAME": "some value", "PARAMETER_NAME_2": "42"}}
If there is not a fitting function available to validate the result, I can provide the final answer in a clear and human-readable format.
[END THOUGHT PROCESS]
IMOPRTANT REMINDER: your each response should contain only one next step and only single one $JSON_BLOB. Do not provide more than one step.
Let's break down the problem step by step and think about the best approach.
Begin!
[Question]
{{$question}}
{{$agentScratchPad}}
@@ -1,78 +0,0 @@
[INSTRUCTION]
Answer the following questions as accurately as possible using the provided functions.
[AVAILABLE FUNCTIONS]
The function definitions below are in the following format:
<functionName>: <description>
- <parameterName>: <parameterDescription>
- ...
[END AVAILABLE FUNCTIONS]
[USAGE INSTRUCTIONS]
To use the functions, specify a JSON blob representing an action. The JSON blob should contain fully qualified name of the function to use, and an "action_variables" key with a JSON object of string values to use when calling the function.
Do not call functions directly; they must be invoked through an action.
Here is an example of a valid $JSON_BLOB:
```
{
"action": "_Namespace_.FUNCTION.NAME",
"action_variables": {"PARAMETER_NAME_1": "some value", "PARAMETER_NAME_2": "42"}
}
```
The keys of "action_variables" should match the defined [PARAMETERS] of the named "action" in [AVAILABLE FUNCTIONS].
Dictionary values in "action_variables" must be strings and represent the actual values to be passed to the function.
Ensure that the $JSON_BLOB contains only a SINGLE action; do NOT return multiple actions.
IMPORTANT:
* Use only the available functions listed in the [AVAILABLE FUNCTIONS] section.
* Do not attempt to use any other functions that are not specified.
* The value of parameters should either by empty if the expectation is for the user to provide them and have not been provided yet, or derived from the agent scratchpad.
* You are not allowed to ask user directly for more information.
[END USAGE INSTRUCTIONS]
[END INSTRUCTION]
[THOUGHT PROCESS]
[QUESTION]
The input question I must answer
[THOUGHT]
To solve this problem, I should carefully analyze the given question and identify the necessary steps. Any facts I discover earlier in my thought process should be repeated here to keep them readily available.
If there is function which can be leveraged for validation, use it in ACTION before jumping into FINAL ANSWER.
[ACTION]
$JSON_BLOB
[OBSERVATION]
The result of the action will be provided here. It could be result or error message of the action, or chat history between assistant and user to tackle the problem.
... (These THOUGHT/ACTION/OBSERVATION can repeat until the final answer is reached.)
[FINAL ANSWER]
Once I have gathered all the necessary observations and performed any required actions, provide the final answer in a clear and human-readable format.
[END THOUGHT PROCESS]
Example:
[AVAILABLE FUNCTIONS]
AuthorPlugin.WritePoem: useful to write poem given a style and input
- input: input for the poem
- style: style of the poem, leave empty if not specified
[END AVAILABLE FUNCTIONS]
[Question]
Write a poem about sun in Whitman's style.
[THOUGHT]
I should use WritePoem function for it.
[ACTION]
{
"action": "AuthorPlugin.WritePoem",
"action_variables": {
"input": "sun",
"stype": "Whitman",
}
}
IMPORTANT REMINDER: your each response should contain only one next step and only single one [ACTION] part. Do not provide more than one step.
Let's break down the problem step by step and think about the best approach.
Begin!
[AVAILABLE FUNCTIONS]
{{$functionDescriptions}}
[END AVAILABLE FUNCTIONS]
[Question]
{{$question}}
{{$agentScratchPad}}
[THOUGHT]
@@ -356,8 +356,11 @@ public static class OpenApiKernelExtensions
Verify.ValidFunctionName(operationId);
return operationId;
}
catch (KernelException)
catch (ArgumentException)
{
// The exception indicates that the operationId is not a valid function name.
// To comply with the SK Function name requirements, it needs to be converted or sanitized.
// Therefore, it should not be re-thrown, but rather swallowed to allow the conversion below.
}
// Tokenize operation id on forward and back slashes
@@ -225,6 +225,24 @@ public sealed class KernelOpenApiPluginExtensionsTests : IDisposable
Assert.Equal("fake-content", response.Content);
}
[Fact]
public async Task ItShouldSanitizeOperationNameAsync()
{
// Arrange
var openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV3_0.json");
using var content = OpenApiTestHelper.ModifyOpenApiDocument(openApiDocument, (doc) =>
{
doc["paths"]!["/secrets/{secret-name}"]!["get"]!["operationId"] = "issues/create-mile.stone";
});
// Act
var plugin = await this._kernel.ImportPluginFromOpenApiAsync("fakePlugin", content, this._executionParameters);
// Assert
Assert.True(plugin.TryGetFunction("IssuesCreatemilestone", out var _));
}
public void Dispose()
{
this._openApiDocument.Dispose();
@@ -37,11 +37,15 @@ public sealed class OpenAIToolsTests : IDisposable
kernel.ImportPluginFromType<TimeInformation>();
var invokedFunctions = new List<string>();
#pragma warning disable CS0618 // Events are deprecated
void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e)
{
invokedFunctions.Add(e.Function.Name);
}
kernel.FunctionInvoking += MyInvokingHandler;
#pragma warning restore CS0618 // Events are deprecated
// Act
OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };
@@ -60,11 +64,15 @@ public sealed class OpenAIToolsTests : IDisposable
kernel.ImportPluginFromType<TimeInformation>();
var invokedFunctions = new List<string>();
#pragma warning disable CS0618 // Events are deprecated
void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e)
{
invokedFunctions.Add($"{e.Function.Name}({string.Join(", ", e.Arguments)})");
}
kernel.FunctionInvoking += MyInvokingHandler;
#pragma warning restore CS0618 // Events are deprecated
// Act
OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Provides a collection of non-null items.
/// </summary>
[ExcludeFromCodeCoverage]
[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "This class is an internal utility.")]
internal sealed class NonNullCollection<T> : IList<T>, IReadOnlyList<T>
{
/// <summary>
/// The underlying list of items.
/// </summary>
private readonly List<T> _items;
/// <summary>
/// Initializes a new instance of the <see cref="NonNullCollection{T}"/> class.
/// </summary>
public NonNullCollection() => this._items = new();
/// <summary>
/// Initializes a new instance of the <see cref="NonNullCollection{T}"/> class.
/// </summary>
/// <param name="items">The initial collection of items to populate this collection.</param>
public NonNullCollection(IEnumerable<T> items)
{
Verify.NotNull(items);
this._items = new(items);
}
/// <summary>
/// Gets or sets the item at the specified index in the collection.
/// </summary>
/// <param name="index">The index of the item to get or set.</param>
/// <returns>The item at the specified index.</returns>
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="index"/> was not valid for this collection.</exception>
public T this[int index]
{
get => this._items[index];
set
{
Verify.NotNull(value);
this._items[index] = value;
}
}
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count => this._items.Count;
/// <summary>
/// Adds an item to the collection.
/// </summary>
/// <param name="item">The item to add.</param>
/// <exception cref="ArgumentNullException"><paramref name="item"/> is null.</exception>
public void Add(T item)
{
Verify.NotNull(item);
this._items.Add(item);
}
/// <summary>
/// Removes all items from the collection.
/// </summary>
public void Clear() => this._items.Clear();
/// <summary>
/// Determines whether an item is in the collection.
/// </summary>
/// <param name="item">The item to locate.</param>
/// <returns>True if the item is found in the collection; otherwise, false.</returns>
/// <exception cref="ArgumentNullException"><paramref name="item"/> is null.</exception>
public bool Contains(T item)
{
Verify.NotNull(item);
return this._items.Contains(item);
}
/// <summary>
/// Copies all of the items in the collection to an array, starting at the specified destination array index.
/// </summary>
/// <param name="array">The destination array into which the items should be copied.</param>
/// <param name="arrayIndex">The zero-based index into <paramref name="array"/> at which copying should begin.</param>
/// <exception cref="ArgumentNullException"><paramref name="array"/> is null.</exception>
/// <exception cref="ArgumentException">The number of items in the collection is greater than the available space from <paramref name="arrayIndex"/> to the end of <paramref name="array"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="arrayIndex"/> is less than 0.</exception>
public void CopyTo(T[] array, int arrayIndex) => this._items.CopyTo(array, arrayIndex);
/// <summary>
/// Searches for the specified item and returns the index of the first occurrence.
/// </summary>
/// <param name="item">The item to locate.</param>
/// <returns>The index of the first found occurrence of the specified item; -1 if the item could not be found.</returns>
public int IndexOf(T item)
{
Verify.NotNull(item);
return this._items.IndexOf(item);
}
/// <summary>
/// Inserts an item into the collection at the specified index.
/// </summary>
/// <param name="index">The index at which the item should be inserted.</param>
/// <param name="item">The item to insert.</param>
/// <exception cref="ArgumentNullException"><paramref name="item"/> is null.</exception>
public void Insert(int index, T item)
{
Verify.NotNull(item);
this._items.Insert(index, item);
}
/// <summary>
/// Removes the first occurrence of the specified item from the collection.
/// </summary>
/// <param name="item">The item to remove from the collection.</param>
/// <returns>True if the item was successfully removed; false if it wasn't located in the collection.</returns>
/// <exception cref="ArgumentNullException"><paramref name="item"/> is null.</exception>
public bool Remove(T item)
{
Verify.NotNull(item);
return this._items.Remove(item);
}
/// <summary>
/// Removes the item at the specified index from the collection.
/// </summary>
/// <param name="index">The index of the item to remove.</param>
public void RemoveAt(int index) => this._items.RemoveAt(index);
bool ICollection<T>.IsReadOnly => false;
IEnumerator IEnumerable.GetEnumerator() => this._items.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this._items.GetEnumerator();
}
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
@@ -10,7 +9,7 @@ namespace Microsoft.SemanticKernel;
/// Provides an <see cref="EventArgs"/> for cancelable operations related
/// to <see cref="Kernel"/>-based operations.
/// </summary>
[Experimental("SKEXP0004")]
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public abstract class CancelKernelEventArgs : KernelEventArgs
{
/// <summary>
@@ -1,14 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Provides a <see cref="CancelKernelEventArgs"/> used in events just after a function is invoked.
/// </summary>
[Experimental("SKEXP0004")]
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public sealed class FunctionInvokedEventArgs : CancelKernelEventArgs
{
/// <summary>
@@ -1,13 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Provides a <see cref="CancelKernelEventArgs"/> used in events just before a function is invoked.
/// </summary>
[Experimental("SKEXP0004")]
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public sealed class FunctionInvokingEventArgs : CancelKernelEventArgs
{
/// <summary>
@@ -2,12 +2,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>Provides an <see cref="EventArgs"/> for operations related to <see cref="Kernel"/>-based operations.</summary>
[Experimental("SKEXP0004")]
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public abstract class KernelEventArgs : EventArgs
{
/// <summary>
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
@@ -7,7 +8,7 @@ namespace Microsoft.SemanticKernel;
/// <summary>
/// Provides a <see cref="CancelKernelEventArgs"/> used in events raised just after a prompt has been rendered.
/// </summary>
[Experimental("SKEXP0004")]
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public sealed class PromptRenderedEventArgs : CancelKernelEventArgs
{
private string _renderedPrompt;
@@ -21,8 +22,7 @@ public sealed class PromptRenderedEventArgs : CancelKernelEventArgs
public PromptRenderedEventArgs(KernelFunction function, KernelArguments arguments, string renderedPrompt) :
base(function, arguments, metadata: null)
{
Verify.NotNull(renderedPrompt);
this._renderedPrompt = renderedPrompt;
this.RenderedPrompt = renderedPrompt;
}
/// <summary>Gets or sets the rendered prompt.</summary>
@@ -35,9 +35,10 @@ public sealed class PromptRenderedEventArgs : CancelKernelEventArgs
public string RenderedPrompt
{
get => this._renderedPrompt;
[MemberNotNull(nameof(_renderedPrompt))]
set
{
Verify.NotNull(value);
Verify.NotNullOrWhiteSpace(value);
this._renderedPrompt = value;
}
}
@@ -1,13 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using System;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Provides a <see cref="KernelEventArgs"/> used in events raised just before a prompt is rendered.
/// </summary>
[Experimental("SKEXP0004")]
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public sealed class PromptRenderingEventArgs : KernelEventArgs
{
/// <summary>
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Base class with data related to function invocation.
/// </summary>
[Experimental("SKEXP0004")]
public abstract class FunctionFilterContext
{
/// <summary>
/// Initializes a new instance of the <see cref="FunctionFilterContext"/> class.
/// </summary>
/// <param name="function">The <see cref="KernelFunction"/> with which this filter is associated.</param>
/// <param name="arguments">The arguments associated with the operation.</param>
/// <param name="metadata">A dictionary of metadata associated with the operation.</param>
internal FunctionFilterContext(KernelFunction function, KernelArguments arguments, IReadOnlyDictionary<string, object?>? metadata)
{
Verify.NotNull(function);
Verify.NotNull(arguments);
this.Function = function;
this.Arguments = arguments;
this.Metadata = metadata;
}
/// <summary>
/// Gets the <see cref="KernelFunction"/> with which this filter is associated.
/// </summary>
public KernelFunction Function { get; }
/// <summary>
/// Gets the arguments associated with the operation.
/// </summary>
public KernelArguments Arguments { get; }
/// <summary>
/// Gets a dictionary of metadata associated with the operation.
/// </summary>
public IReadOnlyDictionary<string, object?>? Metadata { get; }
/// <summary>
/// Gets or sets a value indicating whether the operation associated with
/// the filter should be canceled.
/// </summary>
/// <remarks>
/// The filter may set <see cref="Cancel"/> to true to indicate that the operation should
/// be canceled. If there are multiple filters registered, subsequent filters
/// may see and change a value set by a previous filter. The final result is what will
/// be considered by the component that triggers filter.
/// </remarks>
public bool Cancel { get; set; }
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Class with data related to function after invocation.
/// </summary>
[Experimental("SKEXP0004")]
public sealed class FunctionInvokedContext : FunctionFilterContext
{
/// <summary>
/// Initializes a new instance of the <see cref="FunctionInvokedContext"/> class.
/// </summary>
/// <param name="arguments">The arguments associated with the operation.</param>
/// <param name="result">The result of the function's invocation.</param>
public FunctionInvokedContext(KernelArguments arguments, FunctionResult result)
: base(result.Function, arguments, (result ?? throw new ArgumentNullException(nameof(result))).Metadata)
{
this.Result = result;
this.ResultValue = result.Value;
}
/// <summary>
/// Gets the result of the function's invocation.
/// </summary>
public FunctionResult Result { get; }
/// <summary>
/// Gets the raw result of the function's invocation.
/// </summary>
internal object? ResultValue { get; private set; }
/// <summary>
/// Sets an object to use as the overridden new result for the function's invocation.
/// </summary>
/// <param name="value">The value to use as the new result of the function's invocation.</param>
public void SetResultValue(object? value)
{
this.ResultValue = value;
}
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Class with data related to function before invocation.
/// </summary>
[Experimental("SKEXP0004")]
public sealed class FunctionInvokingContext : FunctionFilterContext
{
/// <summary>
/// Initializes a new instance of the <see cref="FunctionInvokingContext"/> class.
/// </summary>
/// <param name="function">The <see cref="KernelFunction"/> with which this filter is associated.</param>
/// <param name="arguments">The arguments associated with the operation.</param>
public FunctionInvokingContext(KernelFunction function, KernelArguments arguments)
: base(function, arguments, metadata: null)
{
}
}
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Interface for filtering actions during function invocation.
/// </summary>
[Experimental("SKEXP0004")]
public interface IFunctionFilter
{
/// <summary>
/// Method which is executed before function invocation.
/// </summary>
/// <param name="context">Data related to function before invocation.</param>
void OnFunctionInvoking(FunctionInvokingContext context);
/// <summary>
/// Method which is executed after function invocation.
/// </summary>
/// <param name="context">Data related to function after invocation.</param>
void OnFunctionInvoked(FunctionInvokedContext context);
}
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Interface for filtering actions during prompt rendering.
/// </summary>
[Experimental("SKEXP0004")]
public interface IPromptFilter
{
/// <summary>
/// Method which is executed before prompt rendering.
/// </summary>
/// <param name="context">Data related to prompt before rendering.</param>
void OnPromptRendering(PromptRenderingContext context);
/// <summary>
/// Method which is executed after prompt rendering.
/// </summary>
/// <param name="context">Data related to prompt after rendering.</param>
void OnPromptRendered(PromptRenderedContext context);
}
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Base class with data related to prompt rendering.
/// </summary>
[Experimental("SKEXP0004")]
public abstract class PromptFilterContext
{
/// <summary>
/// Initializes a new instance of the <see cref="PromptFilterContext"/> class.
/// </summary>
/// <param name="function">The <see cref="KernelFunction"/> with which this filter is associated.</param>
/// <param name="arguments">The arguments associated with the operation.</param>
/// <param name="metadata">A dictionary of metadata associated with the operation.</param>
internal PromptFilterContext(KernelFunction function, KernelArguments arguments, IReadOnlyDictionary<string, object?>? metadata)
{
Verify.NotNull(function);
Verify.NotNull(arguments);
this.Function = function;
this.Arguments = arguments;
this.Metadata = metadata;
}
/// <summary>
/// Gets the <see cref="KernelFunction"/> with which this filter is associated.
/// </summary>
public KernelFunction Function { get; }
/// <summary>
/// Gets the arguments associated with the operation.
/// </summary>
public KernelArguments Arguments { get; }
/// <summary>
/// Gets a dictionary of metadata associated with the operation.
/// </summary>
public IReadOnlyDictionary<string, object?>? Metadata { get; }
}
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Class with data related to prompt after rendering.
/// </summary>
[Experimental("SKEXP0004")]
public sealed class PromptRenderedContext : PromptFilterContext
{
private string _renderedPrompt;
/// <summary>
/// Initializes a new instance of the <see cref="PromptRenderedContext"/> class.
/// </summary>
/// <param name="function">The <see cref="KernelFunction"/> with which this filter is associated.</param>
/// <param name="arguments">The arguments associated with the operation.</param>
/// <param name="renderedPrompt">The prompt that was rendered by the associated operation.</param>
public PromptRenderedContext(KernelFunction function, KernelArguments arguments, string renderedPrompt)
: base(function, arguments, metadata: null)
{
this.RenderedPrompt = renderedPrompt;
}
/// <summary>
/// Gets or sets a value indicating whether the operation associated with
/// the filter should be canceled.
/// </summary>
/// <remarks>
/// The filter may set <see cref="Cancel"/> to true to indicate that the operation should
/// be canceled. If there are multiple filters registered, subsequent filters
/// may see and change a value set by a previous filter. The final result is what will
/// be considered by the component that triggers filter.
/// </remarks>
public bool Cancel { get; set; }
/// <summary>
/// Gets or sets the rendered prompt.
/// </summary>
/// <remarks>
/// The filter may view the rendered prompt and change it, if desired.
/// If there are multiple filters registered, subsequent filters may
/// overwrite a value set by a previous filter. The final result is what will
/// be the prompt used by the system.
/// </remarks>
public string RenderedPrompt
{
get => this._renderedPrompt;
[MemberNotNull(nameof(_renderedPrompt))]
set
{
Verify.NotNullOrWhiteSpace(value);
this._renderedPrompt = value;
}
}
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Class with data related to prompt before rendering.
/// </summary>
[Experimental("SKEXP0004")]
public sealed class PromptRenderingContext : PromptFilterContext
{
/// <summary>
/// Initializes a new instance of the <see cref="PromptRenderingContext"/> class.
/// </summary>
/// <param name="function">The <see cref="KernelFunction"/> with which this filter is associated.</param>
/// <param name="arguments">The arguments associated with the operation.</param>
public PromptRenderingContext(KernelFunction function, KernelArguments arguments)
: base(function, arguments, metadata: null)
{
}
}
@@ -133,27 +133,56 @@ public abstract class KernelFunction
cancellationToken.ThrowIfCancellationRequested();
// Invoke pre-invocation event handler. If it requests cancellation, throw.
if (kernel.OnFunctionInvoking(this, arguments)?.Cancel is true)
#pragma warning disable CS0618 // Events are deprecated
var invokingEventArgs = kernel.OnFunctionInvoking(this, arguments);
#pragma warning restore CS0618 // Events are deprecated
// Invoke pre-invocation filter. If it requests cancellation, throw.
var invokingContext = kernel.OnFunctionInvokingFilter(this, arguments);
if (invokingEventArgs?.Cancel is true)
{
throw new OperationCanceledException($"A {nameof(Kernel)}.{nameof(Kernel.FunctionInvoking)} event handler requested cancellation before function invocation.");
}
if (invokingContext?.Cancel is true)
{
throw new OperationCanceledException("A function filter requested cancellation before function invocation.");
}
// Invoke the function.
functionResult = await this.InvokeCoreAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
// Invoke the post-invocation event handler. If it requests cancellation, throw.
#pragma warning disable CS0618 // Events are deprecated
var invokedEventArgs = kernel.OnFunctionInvoked(this, arguments, functionResult);
#pragma warning restore CS0618 // Events are deprecated
// Invoke the post-invocation filter. If it requests cancellation, throw.
var invokedContext = kernel.OnFunctionInvokedFilter(arguments, functionResult);
if (invokedEventArgs is not null)
{
// Apply any changes from the event handlers to final result.
functionResult = new FunctionResult(this, invokedEventArgs.ResultValue, functionResult.Culture, invokedEventArgs.Metadata ?? functionResult.Metadata);
}
if (invokedContext is not null)
{
// Apply any changes from the function filters to final result.
functionResult = new FunctionResult(this, invokedContext.ResultValue, functionResult.Culture, invokedContext.Metadata ?? functionResult.Metadata);
}
if (invokedEventArgs?.Cancel is true)
{
throw new OperationCanceledException($"A {nameof(Kernel)}.{nameof(Kernel.FunctionInvoked)} event handler requested cancellation after function invocation.");
}
if (invokedContext?.Cancel is true)
{
throw new OperationCanceledException("A function filter requested cancellation after function invocation.");
}
logger.LogFunctionInvokedSuccess(this.Name);
logger.LogFunctionResultValue(functionResult.Value);
@@ -248,12 +277,23 @@ public abstract class KernelFunction
cancellationToken.ThrowIfCancellationRequested();
// Invoke pre-invocation event handler. If it requests cancellation, throw.
#pragma warning disable CS0618 // Events are deprecated
var invokingEventArgs = kernel.OnFunctionInvoking(this, arguments);
if (invokingEventArgs is not null && invokingEventArgs.Cancel)
#pragma warning restore CS0618 // Events are deprecated
// Invoke pre-invocation filter. If it requests cancellation, throw.
var invokingContext = kernel.OnFunctionInvokingFilter(this, arguments);
if (invokingEventArgs?.Cancel is true)
{
throw new OperationCanceledException($"A {nameof(Kernel)}.{nameof(Kernel.FunctionInvoking)} event handler requested cancellation before function invocation.");
}
if (invokingContext?.Cancel is true)
{
throw new OperationCanceledException("A function filter requested cancellation before function invocation.");
}
// Invoke the function and get its streaming enumerator.
enumerator = this.InvokeStreamingCoreAsync<TResult>(kernel, arguments, cancellationToken).GetAsyncEnumerator(cancellationToken);
@@ -291,7 +331,7 @@ public abstract class KernelFunction
}
}
// The FunctionInvoked hook is not used when streaming.
// The FunctionInvoked hook and filter are not used when streaming.
}
finally
{
+166 -49
View File
@@ -32,6 +32,10 @@ public sealed class Kernel
private CultureInfo _culture = CultureInfo.InvariantCulture;
/// <summary>The collection of plugins, initialized via the constructor or lazily-initialized on first access via <see cref="Plugins"/>.</summary>
private KernelPluginCollection? _plugins;
/// <summary>The collection of function filters, initialized via the constructor or lazily-initialized on first access via <see cref="Plugins"/>.</summary>
private NonNullCollection<IFunctionFilter>? _functionFilters;
/// <summary>The collection of prompt filters, initialized via the constructor or lazily-initialized on first access via <see cref="Plugins"/>.</summary>
private NonNullCollection<IPromptFilter>? _promptFilters;
/// <summary>
/// Initializes a new instance of <see cref="Kernel"/>.
@@ -54,6 +58,7 @@ public sealed class Kernel
// Store the provided plugins. If there weren't any, look in DI to see if there's a plugin collection.
this._plugins = plugins ?? this.Services.GetService<KernelPluginCollection>();
if (this._plugins is null)
{
// Otherwise, enumerate any plugins that may have been registered directly.
@@ -67,6 +72,22 @@ public sealed class Kernel
this._plugins = new(e);
}
}
// Enumerate any function filters that may have been registered.
IEnumerable<IFunctionFilter> functionFilters = this.Services.GetServices<IFunctionFilter>();
if (functionFilters is not ICollection<IFunctionFilter> functionFilterCollection || functionFilterCollection.Count != 0)
{
this._functionFilters = new(functionFilters);
}
// Enumerate any prompt filters that may have been registered.
IEnumerable<IPromptFilter> promptFilters = this.Services.GetServices<IPromptFilter>();
if (promptFilters is not ICollection<IPromptFilter> promptFilterCollection || promptFilterCollection.Count != 0)
{
this._promptFilters = new(promptFilters);
}
}
/// <summary>Creates a builder for constructing <see cref="Kernel"/> instances.</summary>
@@ -116,6 +137,24 @@ public sealed class Kernel
Interlocked.CompareExchange(ref this._plugins, new KernelPluginCollection(), null) ??
this._plugins;
/// <summary>
/// Gets the collection of function filters available through the kernel.
/// </summary>
[Experimental("SKEXP0004")]
public IList<IFunctionFilter> FunctionFilters =>
this._functionFilters ??
Interlocked.CompareExchange(ref this._functionFilters, new NonNullCollection<IFunctionFilter>(), null) ??
this._functionFilters;
/// <summary>
/// Gets the collection of function filters available through the kernel.
/// </summary>
[Experimental("SKEXP0004")]
public IList<IPromptFilter> PromptFilters =>
this._promptFilters ??
Interlocked.CompareExchange(ref this._promptFilters, new NonNullCollection<IPromptFilter>(), null) ??
this._promptFilters;
/// <summary>
/// Gets the service provider used to query for services available through the kernel.
/// </summary>
@@ -166,30 +205,6 @@ public sealed class Kernel
Interlocked.CompareExchange(ref this._data, new Dictionary<string, object?>(), null) ??
this._data;
/// <summary>
/// Provides an event that's raised prior to a function's invocation.
/// </summary>
[Experimental("SKEXP0004")]
public event EventHandler<FunctionInvokingEventArgs>? FunctionInvoking;
/// <summary>
/// Provides an event that's raised after a function's invocation.
/// </summary>
[Experimental("SKEXP0004")]
public event EventHandler<FunctionInvokedEventArgs>? FunctionInvoked;
/// <summary>
/// Provides an event that's raised prior to a prompt being rendered.
/// </summary>
[Experimental("SKEXP0004")]
public event EventHandler<PromptRenderingEventArgs>? PromptRendering;
/// <summary>
/// Provides an event that's raised after a prompt is rendered.
/// </summary>
[Experimental("SKEXP0004")]
public event EventHandler<PromptRenderedEventArgs>? PromptRendered;
#region GetServices
/// <summary>Gets a required service from the <see cref="Services"/> provider.</summary>
/// <typeparam name="T">Specifies the type of the service to get.</typeparam>
@@ -264,58 +279,80 @@ public sealed class Kernel
#endregion
#region Internal Event Helpers
#region Internal Filtering
[Experimental("SKEXP0004")]
internal FunctionInvokingEventArgs? OnFunctionInvoking(KernelFunction function, KernelArguments arguments)
internal FunctionInvokingContext? OnFunctionInvokingFilter(KernelFunction function, KernelArguments arguments)
{
FunctionInvokingEventArgs? eventArgs = null;
if (this.FunctionInvoking is { } functionInvoking)
FunctionInvokingContext? context = null;
if (this._functionFilters is { Count: > 0 })
{
eventArgs = new(function, arguments);
functionInvoking.Invoke(this, eventArgs);
context = new(function, arguments);
for (int i = 0; i < this._functionFilters.Count; i++)
{
this._functionFilters[i].OnFunctionInvoking(context);
}
}
return eventArgs;
return context;
}
[Experimental("SKEXP0004")]
internal FunctionInvokedEventArgs? OnFunctionInvoked(KernelFunction function, KernelArguments arguments, FunctionResult result)
internal FunctionInvokedContext? OnFunctionInvokedFilter(KernelArguments arguments, FunctionResult result)
{
FunctionInvokedEventArgs? eventArgs = null;
if (this.FunctionInvoked is { } functionInvoked)
FunctionInvokedContext? context = null;
if (this._functionFilters is { Count: > 0 })
{
eventArgs = new(function, arguments, result);
functionInvoked.Invoke(this, eventArgs);
context = new(arguments, result);
for (int i = 0; i < this._functionFilters.Count; i++)
{
this._functionFilters[i].OnFunctionInvoked(context);
}
}
return eventArgs;
return context;
}
[Experimental("SKEXP0004")]
internal PromptRenderingEventArgs? OnPromptRendering(KernelFunction function, KernelArguments arguments)
internal PromptRenderingContext? OnPromptRenderingFilter(KernelFunction function, KernelArguments arguments)
{
PromptRenderingEventArgs? eventArgs = null;
if (this.PromptRendering is { } promptRendering)
PromptRenderingContext? context = null;
if (this._promptFilters is { Count: > 0 })
{
eventArgs = new(function, arguments);
promptRendering.Invoke(this, eventArgs);
context = new(function, arguments);
for (int i = 0; i < this._promptFilters.Count; i++)
{
this._promptFilters[i].OnPromptRendering(context);
}
}
return eventArgs;
return context;
}
[Experimental("SKEXP0004")]
internal PromptRenderedEventArgs? OnPromptRendered(KernelFunction function, KernelArguments arguments, string renderedPrompt)
internal PromptRenderedContext? OnPromptRenderedFilter(KernelFunction function, KernelArguments arguments, string renderedPrompt)
{
PromptRenderedEventArgs? eventArgs = null;
if (this.PromptRendered is { } promptRendered)
PromptRenderedContext? context = null;
if (this._promptFilters is { Count: > 0 })
{
eventArgs = new(function, arguments, renderedPrompt);
promptRendered.Invoke(this, eventArgs);
context = new(function, arguments, renderedPrompt);
for (int i = 0; i < this._promptFilters.Count; i++)
{
this._promptFilters[i].OnPromptRendered(context);
}
}
return eventArgs;
return context;
}
#endregion
#region InvokeAsync
@@ -523,4 +560,84 @@ public sealed class Kernel
return function.InvokeStreamingAsync<T>(this, arguments, cancellationToken);
}
#endregion
#region Obsolete
/// <summary>
/// Provides an event that's raised prior to a function's invocation.
/// </summary>
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public event EventHandler<FunctionInvokingEventArgs>? FunctionInvoking;
/// <summary>
/// Provides an event that's raised after a function's invocation.
/// </summary>
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public event EventHandler<FunctionInvokedEventArgs>? FunctionInvoked;
/// <summary>
/// Provides an event that's raised prior to a prompt being rendered.
/// </summary>
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public event EventHandler<PromptRenderingEventArgs>? PromptRendering;
/// <summary>
/// Provides an event that's raised after a prompt is rendered.
/// </summary>
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
public event EventHandler<PromptRenderedEventArgs>? PromptRendered;
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
internal FunctionInvokingEventArgs? OnFunctionInvoking(KernelFunction function, KernelArguments arguments)
{
FunctionInvokingEventArgs? eventArgs = null;
if (this.FunctionInvoking is { } functionInvoking)
{
eventArgs = new(function, arguments);
functionInvoking.Invoke(this, eventArgs);
}
return eventArgs;
}
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
internal FunctionInvokedEventArgs? OnFunctionInvoked(KernelFunction function, KernelArguments arguments, FunctionResult result)
{
FunctionInvokedEventArgs? eventArgs = null;
if (this.FunctionInvoked is { } functionInvoked)
{
eventArgs = new(function, arguments, result);
functionInvoked.Invoke(this, eventArgs);
}
return eventArgs;
}
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
internal PromptRenderingEventArgs? OnPromptRendering(KernelFunction function, KernelArguments arguments)
{
PromptRenderingEventArgs? eventArgs = null;
if (this.PromptRendering is { } promptRendering)
{
eventArgs = new(function, arguments);
promptRendering.Invoke(this, eventArgs);
}
return eventArgs;
}
[Obsolete("Events are deprecated in favor of filters. Example in dotnet/samples/KernelSyntaxExamples/Getting_Started/Step7_Observability.cs of Semantic Kernel repository.")]
internal PromptRenderedEventArgs? OnPromptRendered(KernelFunction function, KernelArguments arguments, string renderedPrompt)
{
PromptRenderedEventArgs? eventArgs = null;
if (this.PromptRendered is { } promptRendered)
{
eventArgs = new(function, arguments, renderedPrompt);
promptRendered.Invoke(this, eventArgs);
}
return eventArgs;
}
#endregion
}
@@ -122,28 +122,36 @@ internal sealed class KernelFunctionFromPrompt : KernelFunction
{
this.AddDefaultValues(arguments);
(var aiService, var executionSettings, var renderedPrompt, var renderedEventArgs) = await this.RenderPromptAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
if (renderedEventArgs?.Cancel is true)
var result = await this.RenderPromptAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
#pragma warning disable CS0612 // Events are deprecated
if (result.RenderedEventArgs?.Cancel is true)
{
throw new OperationCanceledException($"A {nameof(Kernel)}.{nameof(Kernel.PromptRendered)} event handler requested cancellation before function invocation.");
throw new OperationCanceledException($"A {nameof(Kernel)}.{nameof(Kernel.PromptRendered)} event handler requested cancellation after prompt rendering.");
}
#pragma warning restore CS0612 // Events are deprecated
if (result.RenderedContext?.Cancel is true)
{
throw new OperationCanceledException("A prompt filter requested cancellation after prompt rendering.");
}
if (aiService is IChatCompletionService chatCompletion)
if (result.AIService is IChatCompletionService chatCompletion)
{
var chatContent = await chatCompletion.GetChatMessageContentAsync(renderedPrompt, executionSettings, kernel, cancellationToken).ConfigureAwait(false);
var chatContent = await chatCompletion.GetChatMessageContentAsync(result.RenderedPrompt, result.ExecutionSettings, kernel, cancellationToken).ConfigureAwait(false);
this.CaptureUsageDetails(chatContent.ModelId, chatContent.Metadata, this._logger);
return new FunctionResult(this, chatContent, kernel.Culture, chatContent.Metadata);
}
if (aiService is ITextGenerationService textGeneration)
if (result.AIService is ITextGenerationService textGeneration)
{
var textContent = await textGeneration.GetTextContentWithDefaultParserAsync(renderedPrompt, executionSettings, kernel, cancellationToken).ConfigureAwait(false);
var textContent = await textGeneration.GetTextContentWithDefaultParserAsync(result.RenderedPrompt, result.ExecutionSettings, kernel, cancellationToken).ConfigureAwait(false);
this.CaptureUsageDetails(textContent.ModelId, textContent.Metadata, this._logger);
return new FunctionResult(this, textContent, kernel.Culture, textContent.Metadata);
}
// The service selector didn't find an appropriate service. This should only happen with a poorly implemented selector.
throw new NotSupportedException($"The AI service {aiService.GetType()} is not supported. Supported services are {typeof(IChatCompletionService)} and {typeof(ITextGenerationService)}");
throw new NotSupportedException($"The AI service {result.AIService.GetType()} is not supported. Supported services are {typeof(IChatCompletionService)} and {typeof(ITextGenerationService)}");
}
protected override async IAsyncEnumerable<TResult> InvokeStreamingCoreAsync<TResult>(
@@ -153,25 +161,34 @@ internal sealed class KernelFunctionFromPrompt : KernelFunction
{
this.AddDefaultValues(arguments);
(var aiService, var executionSettings, var renderedPrompt, var renderedEventArgs) = await this.RenderPromptAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
if (renderedEventArgs?.Cancel ?? false)
var result = await this.RenderPromptAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
#pragma warning disable CS0612 // Events are deprecated
if (result.RenderedEventArgs?.Cancel is true)
{
yield break;
}
#pragma warning restore CS0612 // Events are deprecated
if (result.RenderedContext?.Cancel is true)
{
yield break;
}
IAsyncEnumerable<StreamingKernelContent>? asyncReference = null;
if (aiService is IChatCompletionService chatCompletion)
if (result.AIService is IChatCompletionService chatCompletion)
{
asyncReference = chatCompletion.GetStreamingChatMessageContentsAsync(renderedPrompt, executionSettings, kernel, cancellationToken);
asyncReference = chatCompletion.GetStreamingChatMessageContentsAsync(result.RenderedPrompt, result.ExecutionSettings, kernel, cancellationToken);
}
else if (aiService is ITextGenerationService textGeneration)
else if (result.AIService is ITextGenerationService textGeneration)
{
asyncReference = textGeneration.GetStreamingTextContentsWithDefaultParserAsync(renderedPrompt, executionSettings, kernel, cancellationToken);
asyncReference = textGeneration.GetStreamingTextContentsWithDefaultParserAsync(result.RenderedPrompt, result.ExecutionSettings, kernel, cancellationToken);
}
else
{
// The service selector didn't find an appropriate service. This should only happen with a poorly implemented selector.
throw new NotSupportedException($"The AI service {aiService.GetType()} is not supported. Supported services are {typeof(IChatCompletionService)} and {typeof(ITextGenerationService)}");
throw new NotSupportedException($"The AI service {result.AIService.GetType()} is not supported. Supported services are {typeof(IChatCompletionService)} and {typeof(ITextGenerationService)}");
}
await foreach (var content in asyncReference)
@@ -256,7 +273,7 @@ internal sealed class KernelFunctionFromPrompt : KernelFunction
}
}
private async Task<(IAIService, PromptExecutionSettings?, string, PromptRenderedEventArgs?)> RenderPromptAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken)
private async Task<PromptRenderingResult> RenderPromptAsync(Kernel kernel, KernelArguments arguments, CancellationToken cancellationToken)
{
var serviceSelector = kernel.ServiceSelector;
IAIService? aiService;
@@ -277,7 +294,11 @@ internal sealed class KernelFunctionFromPrompt : KernelFunction
Verify.NotNull(aiService);
#pragma warning disable CS0618 // Events are deprecated
kernel.OnPromptRendering(this, arguments);
#pragma warning restore CS0618 // Events are deprecated
kernel.OnPromptRenderingFilter(this, arguments);
var renderedPrompt = await this._promptTemplate.RenderAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
@@ -286,21 +307,42 @@ internal sealed class KernelFunctionFromPrompt : KernelFunction
this._logger.LogTrace("Rendered prompt: {Prompt}", renderedPrompt);
}
#pragma warning disable CS0618 // Events are deprecated
var renderedEventArgs = kernel.OnPromptRendered(this, arguments, renderedPrompt);
if (renderedEventArgs is not null &&
renderedEventArgs.Cancel is false &&
!renderedEventArgs.Cancel &&
renderedEventArgs.RenderedPrompt != renderedPrompt)
{
renderedPrompt = renderedEventArgs.RenderedPrompt;
if (this._logger.IsEnabled(LogLevel.Trace))
{
this._logger.LogTrace("Rendered prompt changed by handler: {Prompt}", renderedEventArgs.RenderedPrompt);
this._logger.LogTrace("Rendered prompt changed by event handler: {Prompt}", renderedEventArgs.RenderedPrompt);
}
}
#pragma warning restore CS0618 // Events are deprecated
var renderedContext = kernel.OnPromptRenderedFilter(this, arguments, renderedPrompt);
if (renderedContext is not null &&
!renderedContext.Cancel &&
renderedContext.RenderedPrompt != renderedPrompt)
{
renderedPrompt = renderedContext.RenderedPrompt;
if (this._logger.IsEnabled(LogLevel.Trace))
{
this._logger.LogTrace("Rendered prompt changed by prompt filter: {Prompt}", renderedContext.RenderedPrompt);
}
}
return (aiService, executionSettings, renderedPrompt, renderedEventArgs);
return new(aiService, renderedPrompt)
{
ExecutionSettings = executionSettings,
RenderedEventArgs = renderedEventArgs,
RenderedContext = renderedContext
};
}
/// <summary>Create a random, valid function name.</summary>
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel.Services;
namespace Microsoft.SemanticKernel;
/// <summary>
/// Contains result after prompt rendering process.
/// </summary>
internal sealed class PromptRenderingResult
{
public IAIService AIService { get; set; }
public string RenderedPrompt { get; set; }
public PromptExecutionSettings? ExecutionSettings { get; set; }
#pragma warning disable CS0618 // Events are deprecated
public PromptRenderedEventArgs? RenderedEventArgs { get; set; }
#pragma warning restore CS0618 // Events are deprecated
public PromptRenderedContext? RenderedContext { get; set; }
public PromptRenderingResult(IAIService aiService, string renderedPrompt)
{
this.AIService = aiService;
this.RenderedPrompt = renderedPrompt;
}
}
@@ -6,6 +6,8 @@ using Xunit;
namespace SemanticKernel.UnitTests.Events;
#pragma warning disable CS0618 // Events are deprecated
public class FunctionInvokedEventArgsTests
{
[Fact]
@@ -0,0 +1,657 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.TextGeneration;
using Moq;
using Xunit;
namespace SemanticKernel.UnitTests.Filters;
public class KernelFilterTests
{
[Fact]
public async Task PreInvocationFunctionFilterIsTriggeredAsync()
{
// Arrange
var functionInvocations = 0;
var filterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var kernel = this.GetKernelWithFilters(onFunctionInvoking: (context) =>
{
filterInvocations++;
});
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(1, functionInvocations);
Assert.Equal(1, filterInvocations);
}
[Fact]
public async Task PreInvocationFunctionFilterChangesArgumentAsync()
{
// Arrange
const string OriginalInput = "OriginalInput";
const string NewInput = "NewInput";
var kernel = this.GetKernelWithFilters(onFunctionInvoking: (context) =>
{
context.Arguments["originalInput"] = NewInput;
});
var function = KernelFunctionFactory.CreateFromMethod((string originalInput) => originalInput);
// Act
var result = await kernel.InvokeAsync(function, new() { ["originalInput"] = OriginalInput });
// Assert
Assert.Equal(NewInput, result.GetValue<string>());
}
[Fact]
public async Task PreInvocationFunctionFilterCancellationWorksCorrectlyAsync()
{
// Arrange
var functionInvocations = 0;
var preFilterInvocations = 0;
var postFilterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var kernel = this.GetKernelWithFilters(
onFunctionInvoking: (context) =>
{
preFilterInvocations++;
context.Cancel = true;
},
onFunctionInvoked: (context) =>
{
postFilterInvocations++;
});
// Act
var exception = await Assert.ThrowsAsync<KernelFunctionCanceledException>(() => kernel.InvokeAsync(function));
// Assert
Assert.Equal(1, preFilterInvocations);
Assert.Equal(0, functionInvocations);
Assert.Equal(0, postFilterInvocations);
Assert.Same(function, exception.Function);
Assert.Null(exception.FunctionResult);
}
[Fact]
public async Task PreInvocationFunctionFilterCancellationWorksCorrectlyOnStreamingAsync()
{
// Arrange
var functionInvocations = 0;
var filterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var kernel = this.GetKernelWithFilters(onFunctionInvoking: (context) =>
{
filterInvocations++;
context.Cancel = true;
});
// Act
IAsyncEnumerable<StreamingKernelContent> enumerable = function.InvokeStreamingAsync<StreamingKernelContent>(kernel);
IAsyncEnumerator<StreamingKernelContent> enumerator = enumerable.GetAsyncEnumerator();
Assert.Equal(0, filterInvocations);
var exception = await Assert.ThrowsAsync<KernelFunctionCanceledException>(async () => await enumerator.MoveNextAsync());
// Assert
Assert.Equal(1, filterInvocations);
Assert.Equal(0, functionInvocations);
Assert.Same(function, exception.Function);
Assert.Same(kernel, exception.Kernel);
Assert.Null(exception.FunctionResult);
}
[Fact]
public async Task PostInvocationFunctionFilterIsTriggeredAsync()
{
// Arrange
var functionInvocations = 0;
var filterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var kernel = this.GetKernelWithFilters(onFunctionInvoked: (context) =>
{
filterInvocations++;
});
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(1, functionInvocations);
Assert.Equal(1, filterInvocations);
}
[Fact]
public async Task PostInvocationFunctionFilterReturnsModifiedResultAsync()
{
// Arrange
const int OriginalResult = 42;
const int NewResult = 84;
var function = KernelFunctionFactory.CreateFromMethod(() => OriginalResult);
var kernel = this.GetKernelWithFilters(onFunctionInvoked: (context) =>
{
context.SetResultValue(NewResult);
});
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(NewResult, result.GetValue<int>());
}
[Fact]
public async Task PostInvocationFunctionFilterCancellationWorksCorrectlyAsync()
{
// Arrange
const int Result = 42;
var function = KernelFunctionFactory.CreateFromMethod(() => Result);
var args = new KernelArguments() { { "a", "b" } };
var kernel = this.GetKernelWithFilters(onFunctionInvoked: (context) =>
{
context.Cancel = true;
});
// Act
var exception = await Assert.ThrowsAsync<KernelFunctionCanceledException>(() => kernel.InvokeAsync(function, args));
// Assert
Assert.Same(kernel, exception.Kernel);
Assert.Same(function, exception.Function);
Assert.Same(args, exception.Arguments);
Assert.NotNull(exception.FunctionResult);
Assert.Equal(Result, exception.FunctionResult.GetValue<int>());
}
[Fact]
public async Task PostInvocationFunctionFilterCancellationWithModifiedResultAsync()
{
// Arrange
const int OriginalResult = 42;
const int NewResult = 84;
var function = KernelFunctionFactory.CreateFromMethod(() => OriginalResult);
var args = new KernelArguments() { { "a", "b" } };
var kernel = this.GetKernelWithFilters(onFunctionInvoked: (context) =>
{
context.SetResultValue(NewResult);
context.Cancel = true;
});
// Act
var exception = await Assert.ThrowsAsync<KernelFunctionCanceledException>(() => kernel.InvokeAsync(function, args));
// Assert
Assert.Same(kernel, exception.Kernel);
Assert.Same(function, exception.Function);
Assert.Same(args, exception.Arguments);
Assert.NotNull(exception.FunctionResult);
Assert.Equal(NewResult, exception.FunctionResult.GetValue<int>());
}
[Fact]
public async Task PostInvocationFunctionFilterIsNotTriggeredOnStreamingAsync()
{
// Arrange
var functionInvocations = 0;
var preFilterInvocations = 0;
var postFilterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var kernel = this.GetKernelWithFilters(
onFunctionInvoking: (context) =>
{
preFilterInvocations++;
},
onFunctionInvoked: (context) =>
{
postFilterInvocations++;
});
// Act
await foreach (var chunk in kernel.InvokeStreamingAsync(function))
{
}
// Assert
Assert.Equal(1, functionInvocations);
Assert.Equal(1, preFilterInvocations);
Assert.Equal(0, postFilterInvocations);
}
[Fact]
public async Task FunctionFiltersWithPromptsWorkCorrectlyAsync()
{
// Arrange
var preFilterInvocations = 0;
var postFilterInvocations = 0;
var mockTextGeneration = this.GetMockTextGeneration();
var kernel = this.GetKernelWithFilters(textGenerationService: mockTextGeneration.Object,
onFunctionInvoking: (context) =>
{
preFilterInvocations++;
},
onFunctionInvoked: (context) =>
{
postFilterInvocations++;
});
var function = KernelFunctionFactory.CreateFromPrompt("Write a simple phrase about UnitTests");
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(1, preFilterInvocations);
Assert.Equal(1, postFilterInvocations);
mockTextGeneration.Verify(m => m.GetTextContentsAsync(It.IsAny<string>(), It.IsAny<PromptExecutionSettings>(), It.IsAny<Kernel>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
}
[Fact]
public async Task PromptFiltersAreNotTriggeredForMethodsAsync()
{
// Arrange
var functionInvocations = 0;
var preFilterInvocations = 0;
var postFilterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var kernel = this.GetKernelWithFilters(
onPromptRendering: (context) =>
{
preFilterInvocations++;
},
onPromptRendered: (context) =>
{
postFilterInvocations++;
});
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(1, functionInvocations);
Assert.Equal(0, preFilterInvocations);
Assert.Equal(0, postFilterInvocations);
}
[Fact]
public async Task PromptFiltersAreTriggeredForPromptsAsync()
{
// Arrange
var preFilterInvocations = 0;
var postFilterInvocations = 0;
var mockTextGeneration = this.GetMockTextGeneration();
var function = KernelFunctionFactory.CreateFromPrompt("Prompt");
var kernel = this.GetKernelWithFilters(textGenerationService: mockTextGeneration.Object,
onPromptRendering: (context) =>
{
preFilterInvocations++;
},
onPromptRendered: (context) =>
{
postFilterInvocations++;
});
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(1, preFilterInvocations);
Assert.Equal(1, postFilterInvocations);
}
[Fact]
public async Task PromptFiltersAreTriggeredForPromptsStreamingAsync()
{
// Arrange
var preFilterInvocations = 0;
var postFilterInvocations = 0;
var mockTextGeneration = this.GetMockTextGeneration();
var function = KernelFunctionFactory.CreateFromPrompt("Prompt");
var kernel = this.GetKernelWithFilters(textGenerationService: mockTextGeneration.Object,
onPromptRendering: (context) =>
{
preFilterInvocations++;
},
onPromptRendered: (context) =>
{
postFilterInvocations++;
});
// Act
await foreach (var chunk in kernel.InvokeStreamingAsync(function))
{
}
// Assert
Assert.Equal(1, preFilterInvocations);
Assert.Equal(1, postFilterInvocations);
}
[Fact]
public async Task PostInvocationPromptFilterChangesRenderedPromptAsync()
{
// Arrange
var mockTextGeneration = this.GetMockTextGeneration();
var function = KernelFunctionFactory.CreateFromPrompt("Prompt");
var kernel = this.GetKernelWithFilters(textGenerationService: mockTextGeneration.Object,
onPromptRendered: (context) =>
{
context.RenderedPrompt += " - updated from filter";
});
// Act
var result = await kernel.InvokeAsync(function);
// Assert
mockTextGeneration.Verify(m => m.GetTextContentsAsync("Prompt - updated from filter", It.IsAny<PromptExecutionSettings>(), It.IsAny<Kernel>(), It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task PostInvocationPromptFilterCancellationWorksCorrectlyAsync()
{
// Arrange
var mockTextGeneration = this.GetMockTextGeneration();
var function = KernelFunctionFactory.CreateFromPrompt("Prompt");
var kernel = this.GetKernelWithFilters(textGenerationService: mockTextGeneration.Object,
onPromptRendered: (context) =>
{
context.Cancel = true;
});
// Act
var exception = await Assert.ThrowsAsync<KernelFunctionCanceledException>(() => kernel.InvokeAsync(function));
// Assert
Assert.Same(function, exception.Function);
Assert.Same(kernel, exception.Kernel);
Assert.Null(exception.FunctionResult);
}
[Fact]
public async Task FunctionAndPromptFiltersAreExecutedInCorrectOrderAsync()
{
// Arrange
var builder = Kernel.CreateBuilder();
var mockTextGeneration = this.GetMockTextGeneration();
var function = KernelFunctionFactory.CreateFromPrompt("Prompt");
var executionOrder = new List<string>();
var functionFilter1 = new FakeFunctionFilter(
(context) => executionOrder.Add("FunctionFilter1-Invoking"),
(context) => executionOrder.Add("FunctionFilter1-Invoked"));
var functionFilter2 = new FakeFunctionFilter(
(context) => executionOrder.Add("FunctionFilter2-Invoking"),
(context) => executionOrder.Add("FunctionFilter2-Invoked"));
var promptFilter1 = new FakePromptFilter(
(context) => executionOrder.Add("PromptFilter1-Rendering"),
(context) => executionOrder.Add("PromptFilter1-Rendered"));
var promptFilter2 = new FakePromptFilter(
(context) => executionOrder.Add("PromptFilter2-Rendering"),
(context) => executionOrder.Add("PromptFilter2-Rendered"));
builder.Services.AddSingleton<IFunctionFilter>(functionFilter1);
builder.Services.AddSingleton<IFunctionFilter>(functionFilter2);
builder.Services.AddSingleton<IPromptFilter>(promptFilter1);
builder.Services.AddSingleton<IPromptFilter>(promptFilter2);
builder.Services.AddSingleton<ITextGenerationService>(mockTextGeneration.Object);
var kernel = builder.Build();
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal("FunctionFilter1-Invoking", executionOrder[0]);
Assert.Equal("FunctionFilter2-Invoking", executionOrder[1]);
Assert.Equal("PromptFilter1-Rendering", executionOrder[2]);
Assert.Equal("PromptFilter2-Rendering", executionOrder[3]);
Assert.Equal("PromptFilter1-Rendered", executionOrder[4]);
Assert.Equal("PromptFilter2-Rendered", executionOrder[5]);
Assert.Equal("FunctionFilter1-Invoked", executionOrder[6]);
Assert.Equal("FunctionFilter2-Invoked", executionOrder[7]);
}
[Fact]
public async Task MultipleFunctionFiltersCancellationWorksCorrectlyAsync()
{
// Arrange
var functionInvocations = 0;
var filterInvocations = 0;
var function = KernelFunctionFactory.CreateFromMethod(() => functionInvocations++);
var functionFilter1 = new FakeFunctionFilter(onFunctionInvoking: (context) =>
{
filterInvocations++;
context.Cancel = true;
});
var functionFilter2 = new FakeFunctionFilter(onFunctionInvoking: (context) =>
{
Assert.True(context.Cancel);
filterInvocations++;
context.Cancel = false;
});
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<IFunctionFilter>(functionFilter1);
builder.Services.AddSingleton<IFunctionFilter>(functionFilter2);
var kernel = builder.Build();
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal(1, functionInvocations);
Assert.Equal(2, filterInvocations);
}
[Fact]
public async Task DifferentWaysOfAddingFunctionFiltersWorkCorrectlyAsync()
{
// Arrange
var function = KernelFunctionFactory.CreateFromMethod(() => "Result");
var executionOrder = new List<string>();
var functionFilter1 = new FakeFunctionFilter((context) => executionOrder.Add("FunctionFilter1-Invoking"));
var functionFilter2 = new FakeFunctionFilter((context) => executionOrder.Add("FunctionFilter2-Invoking"));
var builder = Kernel.CreateBuilder();
// Act
// Case #1 - Add filter to services
builder.Services.AddSingleton<IFunctionFilter>(functionFilter1);
var kernel = builder.Build();
// Case #2 - Add filter to kernel
kernel.FunctionFilters.Add(functionFilter2);
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal("FunctionFilter1-Invoking", executionOrder[0]);
Assert.Equal("FunctionFilter2-Invoking", executionOrder[1]);
}
[Fact]
public async Task DifferentWaysOfAddingPromptFiltersWorkCorrectlyAsync()
{
// Arrange
var mockTextGeneration = this.GetMockTextGeneration();
var function = KernelFunctionFactory.CreateFromPrompt("Prompt");
var executionOrder = new List<string>();
var promptFilter1 = new FakePromptFilter((context) => executionOrder.Add("PromptFilter1-Rendering"));
var promptFilter2 = new FakePromptFilter((context) => executionOrder.Add("PromptFilter2-Rendering"));
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<ITextGenerationService>(mockTextGeneration.Object);
// Act
// Case #1 - Add filter to services
builder.Services.AddSingleton<IPromptFilter>(promptFilter1);
var kernel = builder.Build();
// Case #2 - Add filter to kernel
kernel.PromptFilters.Add(promptFilter2);
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal("PromptFilter1-Rendering", executionOrder[0]);
Assert.Equal("PromptFilter2-Rendering", executionOrder[1]);
}
[Fact]
public async Task InsertFilterInMiddleOfPipelineTriggersFiltersInCorrectOrderAsync()
{
// Arrange
var function = KernelFunctionFactory.CreateFromMethod(() => "Result");
var executionOrder = new List<string>();
var functionFilter1 = new FakeFunctionFilter(
(context) => executionOrder.Add("FunctionFilter1-Invoking"),
(context) => executionOrder.Add("FunctionFilter1-Invoked"));
var functionFilter2 = new FakeFunctionFilter(
(context) => executionOrder.Add("FunctionFilter2-Invoking"),
(context) => executionOrder.Add("FunctionFilter2-Invoked"));
var functionFilter3 = new FakeFunctionFilter(
(context) => executionOrder.Add("FunctionFilter3-Invoking"),
(context) => executionOrder.Add("FunctionFilter3-Invoked"));
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<IFunctionFilter>(functionFilter1);
builder.Services.AddSingleton<IFunctionFilter>(functionFilter2);
var kernel = builder.Build();
kernel.FunctionFilters.Insert(1, functionFilter3);
// Act
var result = await kernel.InvokeAsync(function);
// Assert
Assert.Equal("FunctionFilter1-Invoking", executionOrder[0]);
Assert.Equal("FunctionFilter3-Invoking", executionOrder[1]);
Assert.Equal("FunctionFilter2-Invoking", executionOrder[2]);
Assert.Equal("FunctionFilter1-Invoked", executionOrder[3]);
Assert.Equal("FunctionFilter3-Invoked", executionOrder[4]);
Assert.Equal("FunctionFilter2-Invoked", executionOrder[5]);
}
private Kernel GetKernelWithFilters(
Action<FunctionInvokingContext>? onFunctionInvoking = null,
Action<FunctionInvokedContext>? onFunctionInvoked = null,
Action<PromptRenderingContext>? onPromptRendering = null,
Action<PromptRenderedContext>? onPromptRendered = null,
ITextGenerationService? textGenerationService = null)
{
var builder = Kernel.CreateBuilder();
var functionFilter = new FakeFunctionFilter(onFunctionInvoking, onFunctionInvoked);
var promptFilter = new FakePromptFilter(onPromptRendering, onPromptRendered);
// Add function filter before kernel construction
builder.Services.AddSingleton<IFunctionFilter>(functionFilter);
if (textGenerationService is not null)
{
builder.Services.AddSingleton<ITextGenerationService>(textGenerationService);
}
var kernel = builder.Build();
// Add prompt filter after kernel construction
kernel.PromptFilters.Add(promptFilter);
return kernel;
}
private Mock<ITextGenerationService> GetMockTextGeneration()
{
var mockTextGeneration = new Mock<ITextGenerationService>();
mockTextGeneration
.Setup(m => m.GetTextContentsAsync(It.IsAny<string>(), It.IsAny<PromptExecutionSettings>(), It.IsAny<Kernel>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<TextContent> { new("result text") });
mockTextGeneration
.Setup(s => s.GetStreamingTextContentsAsync(It.IsAny<string>(), It.IsAny<PromptExecutionSettings>(), It.IsAny<Kernel>(), It.IsAny<CancellationToken>()))
.Returns(new List<StreamingTextContent>() { new("result chunk") }.ToAsyncEnumerable());
return mockTextGeneration;
}
private sealed class FakeFunctionFilter(
Action<FunctionInvokingContext>? onFunctionInvoking = null,
Action<FunctionInvokedContext>? onFunctionInvoked = null) : IFunctionFilter
{
private readonly Action<FunctionInvokingContext>? _onFunctionInvoking = onFunctionInvoking;
private readonly Action<FunctionInvokedContext>? _onFunctionInvoked = onFunctionInvoked;
public void OnFunctionInvoked(FunctionInvokedContext context) =>
this._onFunctionInvoked?.Invoke(context);
public void OnFunctionInvoking(FunctionInvokingContext context) =>
this._onFunctionInvoking?.Invoke(context);
}
private sealed class FakePromptFilter(
Action<PromptRenderingContext>? onPromptRendering = null,
Action<PromptRenderedContext>? onPromptRendered = null) : IPromptFilter
{
private readonly Action<PromptRenderingContext>? _onPromptRendering = onPromptRendering;
private readonly Action<PromptRenderedContext>? _onPromptRendered = onPromptRendered;
public void OnPromptRendered(PromptRenderedContext context) =>
this._onPromptRendered?.Invoke(context);
public void OnPromptRendering(PromptRenderingContext context) =>
this._onPromptRendering?.Invoke(context);
}
}
@@ -87,6 +87,7 @@ public class FunctionFromMethodTests
var invokedCalled = false;
var invokingCalled = false;
#pragma warning disable CS0618 // Events are deprecated
kernel.FunctionInvoking += (object? sender, FunctionInvokingEventArgs e) =>
{
invokingCalled = true;
@@ -97,6 +98,7 @@ public class FunctionFromMethodTests
{
invokedCalled = true;
};
#pragma warning restore CS0618 // Events are deprecated
// Act
await foreach (var chunk in sut.InvokeStreamingAsync<StreamingKernelContent>(kernel))
@@ -116,11 +118,14 @@ public class FunctionFromMethodTests
var sut = KernelFunctionFactory.CreateFromMethod(() => "any");
bool invokingCalled = false;
#pragma warning disable CS0618 // Type or member is obsolete
kernel.FunctionInvoking += (object? sender, FunctionInvokingEventArgs e) =>
{
invokingCalled = true;
e.Cancel = true;
};
#pragma warning restore CS0618 // Type or member is obsolete
// Act
IAsyncEnumerable<StreamingKernelContent> enumerable = sut.InvokeStreamingAsync<StreamingKernelContent>(kernel);
@@ -142,11 +147,13 @@ public class FunctionFromMethodTests
var kernel = new Kernel();
var sut = KernelFunctionFactory.CreateFromMethod(() => "any");
#pragma warning disable CS0618 // Type or member is obsolete
kernel.FunctionInvoked += (object? sender, FunctionInvokedEventArgs e) =>
{
// This will have no effect on streaming
e.Cancel = true;
};
#pragma warning restore CS0618 // Type or member is obsolete
var chunkCount = 0;
@@ -592,6 +592,7 @@ public class KernelFunctionFromPromptTests
var mockTextCompletion = new Mock<ITextGenerationService>();
mockTextCompletion.Setup(m => m.GetTextContentsAsync(It.IsAny<string>(), It.IsAny<PromptExecutionSettings>(), It.IsAny<Kernel>(), It.IsAny<CancellationToken>())).ReturnsAsync(new List<TextContent> { mockTextContent });
#pragma warning disable CS0618 // Events are deprecated
void MyRenderedHandler(object? sender, PromptRenderedEventArgs e)
{
e.RenderedPrompt += " USE SHORT, CLEAR, COMPLETE SENTENCES.";
@@ -601,6 +602,7 @@ public class KernelFunctionFromPromptTests
builder.Services.AddKeyedSingleton<ITextGenerationService>("service", mockTextCompletion.Object);
Kernel kernel = builder.Build();
kernel.PromptRendered += MyRenderedHandler;
#pragma warning restore CS0618 // Events are deprecated
KernelFunction function = KernelFunctionFactory.CreateFromPrompt("Prompt");
@@ -16,7 +16,7 @@ using Microsoft.SemanticKernel.TextGeneration;
using Moq;
using Xunit;
// ReSharper disable StringLiteralTypo
#pragma warning disable CS0618 // Events are deprecated
namespace SemanticKernel.UnitTests;
@@ -583,6 +583,8 @@ public class KernelTests
.AddSingleton(new HttpClient())
#pragma warning restore CA2000
.AddSingleton(loggerFactory.Object)
.AddSingleton<IFunctionFilter>(new MyFunctionFilter())
.AddSingleton<IPromptFilter>(new MyPromptFilter())
.BuildServiceProvider();
var plugin = KernelPluginFactory.CreateFromFunctions("plugin1");
var plugins = new KernelPluginCollection() { plugin };
@@ -598,6 +600,7 @@ public class KernelTests
Assert.Equal(kernel1.Data["key"], kernel2.Data["key"]);
Assert.NotSame(kernel1.Plugins, kernel2.Plugins);
Assert.Equal(kernel1.Plugins, kernel2.Plugins);
this.AssertFilters(kernel1, kernel2);
// Minimally configured kernel
Kernel kernel3 = new();
@@ -609,6 +612,7 @@ public class KernelTests
Assert.Empty(kernel4.Data);
Assert.NotSame(kernel1.Plugins, kernel2.Plugins);
Assert.Empty(kernel4.Plugins);
this.AssertFilters(kernel3, kernel4);
}
[Fact]
@@ -654,6 +658,29 @@ public class KernelTests
return mockTextCompletion;
}
private void AssertFilters(Kernel kernel1, Kernel kernel2)
{
var functionFilters1 = kernel1.GetAllServices<IFunctionFilter>().ToArray();
var promptFilters1 = kernel1.GetAllServices<IPromptFilter>().ToArray();
var functionFilters2 = kernel2.GetAllServices<IFunctionFilter>().ToArray();
var promptFilters2 = kernel2.GetAllServices<IPromptFilter>().ToArray();
Assert.Equal(functionFilters1.Length, functionFilters2.Length);
for (var i = 0; i < functionFilters1.Length; i++)
{
Assert.Same(functionFilters1[i], functionFilters2[i]);
}
Assert.Equal(promptFilters1.Length, promptFilters2.Length);
for (var i = 0; i < promptFilters1.Length; i++)
{
Assert.Same(promptFilters1[i], promptFilters2[i]);
}
}
public class MyPlugin
{
[KernelFunction, Description("Return any value.")]
@@ -675,4 +702,22 @@ public class KernelTests
Assert.NotNull(kernel.Plugins);
}
}
private sealed class MyFunctionFilter : IFunctionFilter
{
public void OnFunctionInvoked(FunctionInvokedContext context)
{ }
public void OnFunctionInvoking(FunctionInvokingContext context)
{ }
}
private sealed class MyPromptFilter : IPromptFilter
{
public void OnPromptRendered(PromptRenderedContext context)
{ }
public void OnPromptRendering(PromptRenderingContext context)
{ }
}
}
@@ -400,6 +400,7 @@ public class CodeBlockTests
}
);
#pragma warning disable CS0618 // Events are deprecated
kernel.PromptRendering += (object? sender, PromptRenderingEventArgs e) =>
{
Assert.Equal(FooValue, e.Arguments[parameterName]);
@@ -409,6 +410,7 @@ public class CodeBlockTests
{
Assert.Equal(FooValue, e.Arguments[parameterName]);
};
#pragma warning restore CS0618 // Events are deprecated
var codeBlock = new CodeBlock(blockList, "");
await codeBlock.RenderCodeAsync(kernel);
@@ -444,6 +446,7 @@ public class CodeBlockTests
}
);
#pragma warning disable CS0618 // Events are deprecated
kernel.PromptRendering += (object? sender, PromptRenderingEventArgs e) =>
{
Assert.Equal(FooValue, e.Arguments["foo"]);
@@ -455,6 +458,7 @@ public class CodeBlockTests
Assert.Equal(FooValue, e.Arguments["foo"]);
Assert.Equal(FooValue, e.Arguments["x11"]);
};
#pragma warning restore CS0618 // Events are deprecated
var codeBlock = new CodeBlock(blockList, "");
await codeBlock.RenderCodeAsync(kernel, arguments);
+1 -1
View File
@@ -38,7 +38,7 @@
"OPENAI",
"pydantic",
"retrywrites",
"skfunction",
"kernelfunction",
"skprompt",
"templating",
"vectordb"
+7 -7
View File
@@ -170,7 +170,7 @@ from this field is sufficient to have these types of classes as valid Pydantic f
any class using them as attributes to be serialized.
```python
from semantic_kernel.sk_pydantic import PydanticField
from semantic_kernel.kernel_pydantic import PydanticField
class B(PydanticField): ... # correct, B is still an ABC because PydanticField subclasses ABC
class B(PydanticField, ABC): ... # Also correct
@@ -223,13 +223,13 @@ class A:
self.d = d
```
You would convert this to a Pydantic class by subclassing from the `SKBaseModel` class.
You would convert this to a Pydantic class by subclassing from the `KernelBaseModel` class.
```python
from pydantic import Field
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
class A(SKBaseModel):
class A(KernelBaseModel):
# The notation for the fields is similar to dataclasses.
a: int
b: float
@@ -255,14 +255,14 @@ class A:
self.c = c
```
You can uses the `SKGenericModel` to convert these to pydantic serializable classes.
You can use the `KernelBaseModel` to convert these to pydantic serializable classes.
```python
from typing import Generic
from semantic_kernel.sk_pydantic import SKGenericModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
class A(SKGenericModel, Generic[T1, T2]):
class A(KernelBaseModel, Generic[T1, T2]):
# T1 and T2 must be specified in the Generic argument otherwise, pydantic will
# NOT be able to serialize this class
a: int
+1 -1
View File
@@ -16,7 +16,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -25,7 +25,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -89,7 +89,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -55,7 +55,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
+36 -35
View File
@@ -11,9 +11,9 @@
"\n",
"It makes use of the collection of native and semantic functions that have been registered to the kernel and using AI, will formulate a plan to execute the given ask.\n",
"\n",
"From our own testing, planner works best with more powerful models like `gpt4` but sometimes you might get working plans with cheaper models like `gpt-35-turbo`. We encourage you to implement your own versions of the planner and use different models that fit your user needs. \n",
"From our own testing, planner works best with more powerful models like `gpt4` but sometimes you might get working plans with cheaper models like `gpt-35-turbo`. We encourage you to implement your own versions of the planner and use different models that fit your user needs.\n",
"\n",
"Read more about planner [here](https://aka.ms/sk/concepts/planner)"
"Read more about planner [here](https://aka.ms/sk/concepts/planner)\n"
]
},
{
@@ -23,7 +23,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -63,7 +63,7 @@
"id": "4ff28070",
"metadata": {},
"source": [
"## It all begins with an ask"
"## It all begins with an ask\n"
]
},
{
@@ -84,9 +84,10 @@
"metadata": {},
"source": [
"### Providing plugins to the planner\n",
"The planner needs to know what plugins are available to it. Here we'll give it access to the `SummarizePlugin` and `WriterPlugin` we have defined on disk. This will include many semantic functions, of which the planner will intelligently choose a subset. \n",
"\n",
"You can also include native functions as well. Here we'll add the TextPlugin."
"The planner needs to know what plugins are available to it. Here we'll give it access to the `SummarizePlugin` and `WriterPlugin` we have defined on disk. This will include many semantic functions, of which the planner will intelligently choose a subset.\n",
"\n",
"You can also include native functions as well. Here we'll add the TextPlugin.\n"
]
},
{
@@ -109,7 +110,7 @@
"id": "deff5675",
"metadata": {},
"source": [
"Define your ASK. What do you want the Kernel to do?"
"Define your ASK. What do you want the Kernel to do?\n"
]
},
{
@@ -117,7 +118,7 @@
"id": "eee6fe7b",
"metadata": {},
"source": [
"# Basic Planner"
"# Basic Planner\n"
]
},
{
@@ -125,7 +126,7 @@
"id": "590a22f2",
"metadata": {},
"source": [
" Let's start by taking a look at a basic planner. The `BasicPlanner` produces a JSON-based plan that aims to solve the provided ask sequentially and evaluated in order."
"Let's start by taking a look at a basic planner. The `BasicPlanner` produces a JSON-based plan that aims to solve the provided ask sequentially and evaluated in order.\n"
]
},
{
@@ -167,7 +168,7 @@
"source": [
"You can see that the Planner took my ask and converted it into an JSON-based plan detailing how the AI would go about solving this task, making use of the plugins that the Kernel has available to it.\n",
"\n",
"As you can see in the above plan, the AI has determined which functions to call in order to fulfill the user ask. The output of each step of the plan becomes the input to the next function."
"As you can see in the above plan, the AI has determined which functions to call in order to fulfill the user ask. The output of each step of the plan becomes the input to the next function.\n"
]
},
{
@@ -175,7 +176,7 @@
"id": "cd4df0c2",
"metadata": {},
"source": [
"Let's also define an inline plugin and have it be available to the Planner. Be sure to give it a function name and plugin name."
"Let's also define an inline plugin and have it be available to the Planner. Be sure to give it a function name and plugin name.\n"
]
},
{
@@ -204,7 +205,7 @@
"id": "5057cf9b",
"metadata": {},
"source": [
"Let's update our ask using this new plugin"
"Let's update our ask using this new plugin\n"
]
},
{
@@ -237,7 +238,7 @@
"id": "b67a052e",
"metadata": {},
"source": [
"### Executing the plan"
"### Executing the plan\n"
]
},
{
@@ -245,7 +246,7 @@
"id": "3b839c90",
"metadata": {},
"source": [
"Now that we have a plan, let's try to execute it! The Planner has a function called `execute_plan`."
"Now that we have a plan, let's try to execute it! The Planner has a function called `execute_plan`.\n"
]
},
{
@@ -273,7 +274,7 @@
"id": "e8a9b6b7",
"metadata": {},
"source": [
"# The Plan Object Model"
"# The Plan Object Model\n"
]
},
{
@@ -283,7 +284,7 @@
"source": [
"To build more advanced planners, we need to introduce a proper Plan object that can contain all the necessary state and information needed for high quality plans.\n",
"\n",
"To see what that object model is, look at (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/plan.py)"
"To see what that object model is, look at (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/plan.py)\n"
]
},
{
@@ -291,7 +292,7 @@
"id": "0a0cb2a2",
"metadata": {},
"source": [
"# Sequential Planner"
"# Sequential Planner\n"
]
},
{
@@ -299,7 +300,7 @@
"id": "a1c66d83",
"metadata": {},
"source": [
"The sequential planner is an XML-based step-by-step planner. You can see the prompt used for it here (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/sequential_planner/Plugins/SequentialPlanning/skprompt.txt)"
"The sequential planner is an XML-based step-by-step planner. You can see the prompt used for it here (https://github.com/microsoft/semantic-kernel/blob/main/python/semantic_kernel/planning/sequential_planner/Plugins/SequentialPlanning/skprompt.txt)\n"
]
},
{
@@ -329,7 +330,7 @@
"id": "ee2f462b",
"metadata": {},
"source": [
"To see the steps that the Sequential Planner will take, we can iterate over them and print their descriptions"
"To see the steps that the Sequential Planner will take, we can iterate over them and print their descriptions\n"
]
},
{
@@ -348,7 +349,7 @@
"id": "4db5f844",
"metadata": {},
"source": [
"Let's ask the sequential planner to execute the plan."
"Let's ask the sequential planner to execute the plan.\n"
]
},
{
@@ -376,7 +377,7 @@
"id": "d6487c75",
"metadata": {},
"source": [
"# Action Planner"
"# Action Planner\n"
]
},
{
@@ -384,7 +385,7 @@
"id": "b045e26b",
"metadata": {},
"source": [
"The action planner takes in a list of functions and the goal, and outputs a **single** function to use that is appropriate to meet that goal."
"The action planner takes in a list of functions and the goal, and outputs a **single** function to use that is appropriate to meet that goal.\n"
]
},
{
@@ -404,7 +405,7 @@
"id": "53b1f296",
"metadata": {},
"source": [
"Let's add more plugins to the kernel"
"Let's add more plugins to the kernel\n"
]
},
{
@@ -472,7 +473,7 @@
"id": "789b651a",
"metadata": {},
"source": [
"# Stepwise Planner"
"# Stepwise Planner\n"
]
},
{
@@ -482,7 +483,7 @@
"source": [
"Stepwise Planner is based off the paper from MRKL (Modular Reasoning, Knowledge and Language) and is similar to other papers like ReACT (Reasoning and Acting in Language Models). At the core, the stepwise planner allows for the AI to form \"thoughts\" and \"observations\" and execute actions based off those to achieve a user's goal. This continues until all required functions are complete and a final output is generated.\n",
"\n",
"See a video walkthrough of Stepwise Planner [here.](https://youtu.be/DG_Ge1v0c4Q?si=T1CHaAm1vV0mWRHu)"
"See a video walkthrough of Stepwise Planner [here.](https://youtu.be/DG_Ge1v0c4Q?si=T1CHaAm1vV0mWRHu)\n"
]
},
{
@@ -507,7 +508,7 @@
"\n",
"Make sure you have a Bing Search API key in your `.env` file\n",
"\n",
"(https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)"
"(https://www.microsoft.com/en-us/bing/apis/bing-web-search-api)\n"
]
},
{
@@ -522,21 +523,21 @@
" A search engine plugin.\n",
" \"\"\"\n",
"\n",
" from semantic_kernel.orchestration.sk_context import SKContext\n",
" from semantic_kernel.orchestration.kernel_context import KernelContext\n",
" from semantic_kernel.plugin_definition import (\n",
" sk_function,\n",
" sk_function_context_parameter,\n",
" kernel_function,\n",
" kernel_function_context_parameter,\n",
" )\n",
"\n",
" def __init__(self, connector) -> None:\n",
" self._connector = connector\n",
"\n",
" @sk_function(description=\"Performs a web search for a given query\", name=\"searchAsync\")\n",
" @sk_function_context_parameter(\n",
" @kernel_function(description=\"Performs a web search for a given query\", name=\"searchAsync\")\n",
" @kernel_function_context_parameter(\n",
" name=\"query\",\n",
" description=\"The search query\",\n",
" )\n",
" async def search_async(self, query: str, context: SKContext) -> str:\n",
" async def search_async(self, query: str, context: KernelContext) -> str:\n",
" query = query or context.variables.get(\"query\")[1]\n",
" result = await self._connector.search_async(query, num_results=5, offset=0)\n",
" return str(result)"
@@ -561,7 +562,7 @@
"id": "effdf3ab",
"metadata": {},
"source": [
"Let's also add a couple more plugins"
"Let's also add a couple more plugins\n"
]
},
{
@@ -593,7 +594,7 @@
"id": "50699ec3",
"metadata": {},
"source": [
"Now let's do a more complicated ask that will require planner to make a call to Bing to get the latest information."
"Now let's do a more complicated ask that will require planner to make a call to Bing to get the latest information.\n"
]
},
{
@@ -633,7 +634,7 @@
"id": "cb40370d",
"metadata": {},
"source": [
"Let's see the steps that the AI took to get to the answer."
"Let's see the steps that the AI took to get to the answer.\n"
]
},
{
+26 -25
View File
@@ -9,16 +9,16 @@
"# Building Semantic Memory with Embeddings\n",
"\n",
"So far, we've mostly been treating the kernel as a stateless orchestration engine.\n",
"We send text into a model API and receive text out. \n",
"We send text into a model API and receive text out.\n",
"\n",
"In a [previous notebook](04-context-variables-chat.ipynb), we used `context variables` to pass in additional\n",
"text into prompts to enrich them with more context. This allowed us to create a basic chat experience. \n",
"text into prompts to enrich them with more context. This allowed us to create a basic chat experience.\n",
"\n",
"However, if you solely relied on context variables, you would quickly realize that eventually your prompt\n",
"would grow so large that you would run into the model's token limit. What we need is a way to persist state\n",
"and build both short-term and long-term memory to empower even more intelligent applications. \n",
"and build both short-term and long-term memory to empower even more intelligent applications.\n",
"\n",
"To do this, we dive into the key concept of `Semantic Memory` in the Semantic Kernel. "
"To do this, we dive into the key concept of `Semantic Memory` in the Semantic Kernel.\n"
]
},
{
@@ -28,7 +28,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -58,7 +58,7 @@
"In order to use memory, we need to instantiate the Kernel with a Memory Storage\n",
"and an Embedding service. In this example, we make use of the `VolatileMemoryStore` which can be thought of as a temporary in-memory storage. This memory is not written to disk and is only available during the app session.\n",
"\n",
"When developing your app you will have the option to plug in persistent storage like Azure AI Search, Azure Cosmos Db, PostgreSQL, SQLite, etc. Semantic Memory allows also to index external data sources, without duplicating all the information as you will see further down in this notebook."
"When developing your app you will have the option to plug in persistent storage like Azure AI Search, Azure Cosmos Db, PostgreSQL, SQLite, etc. Semantic Memory allows also to index external data sources, without duplicating all the information as you will see further down in this notebook.\n"
]
},
{
@@ -100,7 +100,7 @@
"source": [
"At its core, Semantic Memory is a set of data structures that allow you to store the meaning of text that come from different data sources, and optionally to store the source text too. These texts can be from the web, e-mail providers, chats, a database, or from your local directory, and are hooked up to the Semantic Kernel through data source connectors.\n",
"\n",
"The texts are embedded or compressed into a vector of floats representing mathematically the texts' contents and meaning. You can read more about embeddings [here](https://aka.ms/sk/embeddings)."
"The texts are embedded or compressed into a vector of floats representing mathematically the texts' contents and meaning. You can read more about embeddings [here](https://aka.ms/sk/embeddings).\n"
]
},
{
@@ -110,7 +110,8 @@
"metadata": {},
"source": [
"### Manually adding memories\n",
"Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`"
"\n",
"Let's create some initial memories \"About Me\". We can add memories to our `VolatileMemoryStore` by using `SaveInformationAsync`\n"
]
},
{
@@ -152,7 +153,7 @@
"id": "2calf857",
"metadata": {},
"source": [
"Let's try searching the memory:"
"Let's try searching the memory:\n"
]
},
{
@@ -193,7 +194,7 @@
"metadata": {},
"source": [
"Let's now revisit the our chat sample from the [previous notebook](04-context-variables-chat.ipynb).\n",
"If you remember, we used context variables to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!"
"If you remember, we used context variables to fill the prompt with a `history` that continuously got populated as we chatted with the bot. Let's add also memory to it!\n"
]
},
{
@@ -205,7 +206,7 @@
"This is done by using the `TextMemoryPlugin` which exposes the `recall` native function.\n",
"\n",
"`recall` takes an input ask and performs a similarity search on the contents that have\n",
"been embedded in the Memory Store and returns the most relevant memory. "
"been embedded in the Memory Store and returns the most relevant memory.\n"
]
},
{
@@ -217,7 +218,7 @@
"source": [
"async def setup_chat_with_memory(\n",
" kernel: sk.Kernel,\n",
") -> Tuple[sk.SKFunctionBase, sk.SKContext]:\n",
") -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:\n",
" sk_prompt = \"\"\"\n",
" ChatBot can have a conversation with you about any topic.\n",
" It can give explicit instructions or say 'I don't know' if\n",
@@ -258,7 +259,7 @@
"id": "1ac62457",
"metadata": {},
"source": [
"The `RelevanceParam` is used in memory search and is a measure of the relevance score from 0.0 to 1.0, where 1.0 means a perfect match. We encourage users to experiment with different values."
"The `RelevanceParam` is used in memory search and is a measure of the relevance score from 0.0 to 1.0, where 1.0 means a perfect match. We encourage users to experiment with different values.\n"
]
},
{
@@ -267,7 +268,7 @@
"id": "645b55a1",
"metadata": {},
"source": [
"Now that we've included our memories, let's chat!"
"Now that we've included our memories, let's chat!\n"
]
},
{
@@ -277,7 +278,7 @@
"metadata": {},
"outputs": [],
"source": [
"async def chat(kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext) -> bool:\n",
"async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:\n",
" try:\n",
" user_input = input(\"User:> \")\n",
" context[\"user_input\"] = user_input\n",
@@ -332,7 +333,7 @@
"\n",
"Many times in your applications you'll want to bring in external documents into your memory. Let's see how we can do this using our VolatileMemoryStore.\n",
"\n",
"Let's first get some data using some of the links in the Semantic Kernel repo."
"Let's first get some data using some of the links in the Semantic Kernel repo.\n"
]
},
{
@@ -366,7 +367,7 @@
"id": "75f3ea5e",
"metadata": {},
"source": [
"Now let's add these files to our VolatileMemoryStore using `SaveReferenceAsync`. We'll separate these memories from the chat memories by putting them in a different collection."
"Now let's add these files to our VolatileMemoryStore using `SaveReferenceAsync`. We'll separate these memories from the chat memories by putting them in a different collection.\n"
]
},
{
@@ -448,7 +449,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The implementation of Semantic Kernel allows to easily swap memory store for another. Here, we will re-use the functions we initially created for `VolatileMemoryStore` with our new external Vector Store leveraging Azure AI Search"
"The implementation of Semantic Kernel allows to easily swap memory store for another. Here, we will re-use the functions we initially created for `VolatileMemoryStore` with our new external Vector Store leveraging Azure AI Search\n"
]
},
{
@@ -464,7 +465,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We can see that our function created an \"About Me\" index and that our five pieces of information have been indexed (note that it can take a few minutes for the UI to reflect the document count and storage size)."
"We can see that our function created an \"About Me\" index and that our five pieces of information have been indexed (note that it can take a few minutes for the UI to reflect the document count and storage size).\n"
]
},
{
@@ -476,14 +477,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"![image.png](attachment:image.png)"
"![image.png](attachment:image.png)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And we can see that embeddings have been conveniently created to allow for semantic search."
"And we can see that embeddings have been conveniently created to allow for semantic search.\n"
]
},
{
@@ -495,14 +496,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"![image.png](attachment:image.png)"
"![image.png](attachment:image.png)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's now try to query from Azure AI Search!"
"Let's now try to query from Azure AI Search!\n"
]
},
{
@@ -518,7 +519,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We have laid the foundation which will allow us to store an arbitrary amount of data in an external Vector Store above and beyond what could fit in memory at the expense of a little more latency."
"We have laid the foundation which will allow us to store an arbitrary amount of data in an external Vector Store above and beyond what could fit in memory at the expense of a little more latency.\n"
]
}
],
@@ -538,7 +539,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
"version": "3.9.13"
}
},
"nbformat": 4,
@@ -20,7 +20,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0\n",
"!python -m pip install semantic-kernel==0.4.6.dev0\n",
"\n",
"# Note that additional dependencies are required for the Hugging Face connectors:\n",
"!python -m pip install torch==2.0.0\n",
@@ -6,7 +6,7 @@
"id": "3c93ac5b",
"metadata": {},
"source": [
"# Running Native Functions"
"# Running Native Functions\n"
]
},
{
@@ -21,13 +21,13 @@
"\n",
"This can be useful in a few scenarios:\n",
"\n",
"* Writing logic around how to run a prompt that changes the prompt's outcome.\n",
"* Using external data sources to gather data to concatenate into your prompt.\n",
"* Validating user input data prior to sending it to the LLM prompt.\n",
"- Writing logic around how to run a prompt that changes the prompt's outcome.\n",
"- Using external data sources to gather data to concatenate into your prompt.\n",
"- Validating user input data prior to sending it to the LLM prompt.\n",
"\n",
"Native functions are defined using standard Python code. The structure is simple, but not well documented at this point.\n",
"\n",
"The following examples are intended to help guide new users towards successful native & semantic function use with the SK Python framework."
"The following examples are intended to help guide new users towards successful native & semantic function use with the SK Python framework.\n"
]
},
{
@@ -36,7 +36,7 @@
"id": "d90b0c13",
"metadata": {},
"source": [
"Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):"
"Prepare a semantic kernel instance first, loading also the AI service settings defined in the [Setup notebook](00-getting-started.ipynb):\n"
]
},
{
@@ -46,7 +46,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -85,7 +85,7 @@
"id": "186767f8",
"metadata": {},
"source": [
"Let's create a **native** function that gives us a random number between 3 and a user input as the upper limit. We'll use this number to create 3-x paragraphs of text when passed to a semantic function."
"Let's create a **native** function that gives us a random number between 3 and a user input as the upper limit. We'll use this number to create 3-x paragraphs of text when passed to a semantic function.\n"
]
},
{
@@ -94,7 +94,7 @@
"id": "589733c5",
"metadata": {},
"source": [
"First, let's create our native function."
"First, let's create our native function.\n"
]
},
{
@@ -105,7 +105,7 @@
"outputs": [],
"source": [
"import random\n",
"from semantic_kernel.plugin_definition import sk_function\n",
"from semantic_kernel.plugin_definition import kernel_function\n",
"\n",
"\n",
"class GenerateNumberPlugin:\n",
@@ -113,7 +113,7 @@
" Description: Generate a number between 3-x.\n",
" \"\"\"\n",
"\n",
" @sk_function(\n",
" @kernel_function(\n",
" description=\"Generate a random number between 3-x\",\n",
" name=\"GenerateNumberThreeOrHigher\",\n",
" )\n",
@@ -140,7 +140,7 @@
"id": "f26b90c4",
"metadata": {},
"source": [
"Next, let's create a semantic function that accepts a number as `{{$input}}` and generates that number of paragraphs about two Corgis on an adventure. `$input` is a default variable semantic functions can use. "
"Next, let's create a semantic function that accepts a number as `{{$input}}` and generates that number of paragraphs about two Corgis on an adventure. `$input` is a default variable semantic functions can use.\n"
]
},
{
@@ -215,11 +215,11 @@
"source": [
"## Context Variables\n",
"\n",
"That works! But let's expand on our example to make it more generic. \n",
"That works! But let's expand on our example to make it more generic.\n",
"\n",
"For the native function, we'll introduce the lower limit variable. This means that a user will input two numbers and the number generator function will pick a number between the first and second input.\n",
"\n",
"We'll make use of the `semantic_kernel.ContextVariables` class to do hold these variables."
"We'll make use of the `semantic_kernel.ContextVariables` class to do hold these variables.\n"
]
},
{
@@ -258,7 +258,7 @@
"id": "091f45e4",
"metadata": {},
"source": [
"Let's start with the native function. Notice that we're also adding `@sk_function_context_parameter` decorators to the function here to provide context about what variables need to be provided to the function, and any defaults for those inputs. Using the `@sk_function_context_parameter` decorator provides the name, description and default values for a function's inputs to the [planner.](./05-using-the-planner.ipynb)"
"Let's start with the native function. Notice that we're also adding `@kernel_function_context_parameter` decorators to the function here to provide context about what variables need to be provided to the function, and any defaults for those inputs. Using the `@kernel_function_context_parameter` decorator provides the name, description and default values for a function's inputs to the [planner.](./05-using-the-planner.ipynb)\n"
]
},
{
@@ -269,8 +269,8 @@
"outputs": [],
"source": [
"import random\n",
"from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter\n",
"from semantic_kernel import SKContext\n",
"from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter\n",
"from semantic_kernel import KernelContext\n",
"\n",
"\n",
"class GenerateNumberPlugin:\n",
@@ -278,13 +278,13 @@
" Description: Generate a number between a min and a max.\n",
" \"\"\"\n",
"\n",
" @sk_function(\n",
" @kernel_function(\n",
" description=\"Generate a random number between min and max\",\n",
" name=\"GenerateNumber\",\n",
" )\n",
" @sk_function_context_parameter(name=\"min\", description=\"Minimum number of paragraphs.\")\n",
" @sk_function_context_parameter(name=\"max\", description=\"Maximum number of paragraphs.\", default_value=\"10\")\n",
" def generate_number(self, context: SKContext) -> str:\n",
" @kernel_function_context_parameter(name=\"min\", description=\"Minimum number of paragraphs.\")\n",
" @kernel_function_context_parameter(name=\"max\", description=\"Maximum number of paragraphs.\", default_value=\"10\")\n",
" def generate_number(self, context: KernelContext) -> str:\n",
" \"\"\"\n",
" Generate a number between min-max\n",
" Example:\n",
@@ -319,7 +319,7 @@
"id": "6ad068d6",
"metadata": {},
"source": [
"Now let's also allow the semantic function to take in additional arguments. In this case, we're going to allow the our CorgiStory function to be written in a specified language. We'll need to provide a `paragraph_count` and a `language`."
"Now let's also allow the semantic function to take in additional arguments. In this case, we're going to allow the our CorgiStory function to be written in a specified language. We'll need to provide a `paragraph_count` and a `language`.\n"
]
},
{
@@ -356,7 +356,7 @@
"id": "fdce1872",
"metadata": {},
"source": [
"Now we can call this using our `invoke` function by passing in our `context_variables` in the `variables` parameter. "
"Now we can call this using our `invoke` function by passing in our `context_variables` in the `variables` parameter.\n"
]
},
{
@@ -375,7 +375,7 @@
"id": "c8778bad",
"metadata": {},
"source": [
"Let's add a paragraph count to our context variables "
"Let's add a paragraph count to our context variables\n"
]
},
{
@@ -429,7 +429,7 @@
"\n",
"We will make our CorgiStory semantic function call a native function `GenerateNames` which will return names for our Corgi characters.\n",
"\n",
"We do this using the syntax `{{plugin_name.function_name}}`. You can read more about our prompte templating syntax [here](../../../docs/PROMPT_TEMPLATE_LANGUAGE.md). "
"We do this using the syntax `{{plugin_name.function_name}}`. You can read more about our prompte templating syntax [here](../../../docs/PROMPT_TEMPLATE_LANGUAGE.md).\n"
]
},
{
@@ -440,7 +440,7 @@
"outputs": [],
"source": [
"import random\n",
"from semantic_kernel.plugin_definition import sk_function\n",
"from semantic_kernel.plugin_definition import kernel_function\n",
"\n",
"\n",
"class GenerateNamesPlugin:\n",
@@ -449,9 +449,9 @@
" \"\"\"\n",
"\n",
" # The default function name will be the name of the function itself, however you can override this\n",
" # by setting the name=<name override> in the @sk_function decorator. In this case, we're using\n",
" # by setting the name=<name override> in the @kernel_function decorator. In this case, we're using\n",
" # the same name as the function name for simplicity.\n",
" @sk_function(description=\"Generate character names\", name=\"generate_names\")\n",
" @kernel_function(description=\"Generate character names\", name=\"generate_names\")\n",
" def generate_names(self) -> str:\n",
" \"\"\"\n",
" Generate two names.\n",
@@ -563,7 +563,7 @@
"\n",
"- We've learned how to create native and semantic functions and register them to the kernel\n",
"- We've seen how we can use context variables to pass in more custom variables into our prompt\n",
"- We've seen how we can call native functions within semantic function prompts. \n"
"- We've seen how we can call native functions within semantic function prompts.\n"
]
}
],
@@ -82,7 +82,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -25,7 +25,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
@@ -17,7 +17,7 @@
"metadata": {},
"outputs": [],
"source": [
"!python -m pip install semantic-kernel==0.4.5.dev0"
"!python -m pip install semantic-kernel==0.4.6.dev0"
]
},
{
+26 -24
View File
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# Introduction"
"# Introduction\n"
]
},
{
@@ -13,21 +13,21 @@
"source": [
"This notebook shows how to replace the `VolatileMemoryStore` memory storage used in a [previous notebook](./06-memory-and-embeddings.ipynb) with a `WeaviateMemoryStore`.\n",
"\n",
"`WeaviateMemoryStore` is an example of a persistent (i.e. long-term) memory store backed by the Weaviate vector database."
"`WeaviateMemoryStore` is an example of a persistent (i.e. long-term) memory store backed by the Weaviate vector database.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# About Weaviate"
"# About Weaviate\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[Weaviate](https://weaviate.io/) is an open-source vector database designed to scale seamlessly into billions of data objects. This implementation supports hybrid search out-of-the-box (meaning it will perform better for keyword searches)."
"[Weaviate](https://weaviate.io/) is an open-source vector database designed to scale seamlessly into billions of data objects. This implementation supports hybrid search out-of-the-box (meaning it will perform better for keyword searches).\n"
]
},
{
@@ -79,7 +79,7 @@
" To configure a self-hosted instance with Kubernetes, follow Weaviate's [documentation](https://weaviate.io/developers/weaviate/installation/kubernetes).|\n",
"\n",
"- **Embedded** - start a weaviate instance right from your application code using the client library\n",
" \n",
"\n",
" This code snippet shows how to instantiate an embedded weaviate instance and upload a document:\n",
"\n",
" ```python\n",
@@ -97,15 +97,15 @@
"\n",
" client.data_object.create(data_obj, \"Wine\")\n",
" ```\n",
" \n",
" Refer to the [documentation](https://weaviate.io/developers/weaviate/installation/embedded) for more details about this deployment method."
"\n",
" Refer to the [documentation](https://weaviate.io/developers/weaviate/installation/embedded) for more details about this deployment method.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Setup"
"# Setup\n"
]
},
{
@@ -124,8 +124,9 @@
"metadata": {},
"source": [
"## OS-specific notes:\n",
"* if you run into SSL errors when connecting to OpenAI on macOS, see this issue for a [potential solution](https://github.com/microsoft/semantic-kernel/issues/627#issuecomment-1580912248)\n",
"* on Windows, you may need to run Docker Desktop as administrator"
"\n",
"- if you run into SSL errors when connecting to OpenAI on macOS, see this issue for a [potential solution](https://github.com/microsoft/semantic-kernel/issues/627#issuecomment-1580912248)\n",
"- on Windows, you may need to run Docker Desktop as administrator\n"
]
},
{
@@ -150,9 +151,10 @@
"metadata": {},
"source": [
"First, we instantiate the Weaviate memory store. Uncomment ONE of the options below, depending on how you want to use Weaviate:\n",
"* from a Docker instance\n",
"* from WCS\n",
"* directly from the client (embedded Weaviate), which works on Linux only at the moment"
"\n",
"- from a Docker instance\n",
"- from WCS\n",
"- directly from the client (embedded Weaviate), which works on Linux only at the moment\n"
]
},
{
@@ -190,7 +192,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Then, we register the memory store to the kernel:"
"Then, we register the memory store to the kernel:\n"
]
},
{
@@ -220,7 +222,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's create some initial memories \"About Me\". We can add memories to our weaviate memory store by using `save_information_async`"
"Let's create some initial memories \"About Me\". We can add memories to our weaviate memory store by using `save_information_async`\n"
]
},
{
@@ -253,7 +255,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Searching is done through `search_async`:"
"Searching is done through `search_async`:\n"
]
},
{
@@ -281,7 +283,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's see the results of the functions:"
"Let's see the results of the functions:\n"
]
},
{
@@ -301,7 +303,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Here's how to use the weaviate memory store in a chat application:"
"Here's how to use the weaviate memory store in a chat application:\n"
]
},
{
@@ -312,7 +314,7 @@
"source": [
"async def setup_chat_with_memory(\n",
" kernel: sk.Kernel,\n",
") -> Tuple[sk.SKFunctionBase, sk.SKContext]:\n",
") -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:\n",
" sk_prompt = \"\"\"\n",
" ChatBot can have a conversation with you about any topic.\n",
" It can give explicit instructions or say 'I don't know' if\n",
@@ -353,7 +355,7 @@
"metadata": {},
"outputs": [],
"source": [
"async def chat(kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext) -> bool:\n",
"async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:\n",
" try:\n",
" user_input = input(\"User:> \")\n",
" context[\"user_input\"] = user_input\n",
@@ -394,14 +396,14 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# Adding documents to your memory"
"# Adding documents to your memory\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a dictionary to hold some files. The key is the hyperlink to the file and the value is the file's content:"
"Create a dictionary to hold some files. The key is the hyperlink to the file and the value is the file's content:\n"
]
},
{
@@ -432,7 +434,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Use `save_reference_async` to save the file:"
"Use `save_reference_async` to save the file:\n"
]
},
{
@@ -461,7 +463,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Use `search_async` to ask a question:"
"Use `search_async` to ask a question:\n"
]
},
{
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "semantic-kernel"
version = "0.4.5.dev"
version = "0.4.6.dev"
description = "Semantic Kernel Python SDK"
authors = ["Microsoft <[email protected]>"]
readme = "pip/README.md"
@@ -79,7 +79,7 @@ filter = {"exclude_plugin": ["ChatBot"]}
functions = get_function_calling_object(kernel, filter)
async def chat(context: sk.SKContext) -> Tuple[bool, sk.SKContext]:
async def chat(context: sk.KernelContext) -> Tuple[bool, sk.KernelContext]:
try:
user_input = input("User:> ")
context.variables["user_input"] = user_input
@@ -74,7 +74,7 @@ function_config = sk.SemanticFunctionConfig(prompt_config, prompt_template)
chat_function = kernel.register_semantic_function("ChatBot", "Chat", function_config)
async def chat(context: sk.SKContext) -> Tuple[bool, sk.SKContext]:
async def chat(context: sk.KernelContext) -> Tuple[bool, sk.KernelContext]:
try:
user_input = input("User:> ")
context.variables["user_input"] = user_input
@@ -42,7 +42,7 @@ async def search_memory_examples(kernel: sk.Kernel) -> None:
async def setup_chat_with_memory(
kernel: sk.Kernel,
) -> Tuple[sk.SKFunctionBase, sk.SKContext]:
) -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:
"""
When using Google PaLM to chat with memories, a chat prompt template is
essential; otherwise, the kernel will send text prompts to the Google PaLM
@@ -90,7 +90,7 @@ async def setup_chat_with_memory(
return chat_func, context
async def chat(kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext) -> bool:
async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:
try:
user_input = input("User:> ")
context["user_input"] = user_input
@@ -35,7 +35,7 @@ async def search_memory_examples(kernel: sk.Kernel) -> None:
async def setup_chat_with_memory(
kernel: sk.Kernel,
) -> Tuple[sk.SKFunctionBase, sk.SKContext]:
) -> Tuple[sk.KernelFunctionBase, sk.KernelContext]:
sk_prompt = """
ChatBot can have a conversation with you about any topic.
It can give explicit instructions or say 'I don't know' if
@@ -70,7 +70,7 @@ async def setup_chat_with_memory(
return chat_func, context
async def chat(kernel: sk.Kernel, chat_func: sk.SKFunctionBase, context: sk.SKContext) -> bool:
async def chat(kernel: sk.Kernel, chat_func: sk.KernelFunctionBase, context: sk.KernelContext) -> bool:
try:
user_input = input("User:> ")
context["user_input"] = user_input
+4 -4
View File
@@ -3,8 +3,8 @@
from semantic_kernel import core_plugins, memory
from semantic_kernel.kernel import Kernel
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.semantic_functions.chat_prompt_template import ChatPromptTemplate
from semantic_kernel.semantic_functions.prompt_template import PromptTemplate
from semantic_kernel.semantic_functions.prompt_template_config import (
@@ -48,8 +48,8 @@ __all__ = [
"ChatPromptTemplate",
"SemanticFunctionConfig",
"ContextVariables",
"SKFunctionBase",
"SKContext",
"KernelFunctionBase",
"KernelContext",
"memory",
"core_plugins",
"setup_logging",
@@ -2,10 +2,10 @@ from typing import Any, Dict, Optional
from pydantic import Field
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
class AIRequestSettings(SKBaseModel):
class AIRequestSettings(KernelBaseModel):
"""Base class for AI request settings.
Can be used by itself or as a base class for other request settings. The methods are used to create specific request settings objects based on the keys in the extension_data field, this way you can create a generic AIRequestSettings object in your application, which get's mapped into the keys of the request settings that each services returns by using the service.get_request_settings() method.
@@ -11,10 +11,10 @@ else:
from pydantic import StringConstraints
from semantic_kernel.connectors.ai.ai_request_settings import AIRequestSettings
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
class AIServiceClientBase(SKBaseModel, ABC):
class AIServiceClientBase(KernelBaseModel, ABC):
"""Base class for all AI Services.
Has a ai_model_id, any other fields have to be defined by the subclasses.
@@ -75,7 +75,9 @@ class ContentFilterAIException(AIException):
self._param = inner_exception.param
inner_error = inner_exception.body.get("innererror", {})
self._content_filter_code = ContentFilterCodes(inner_error.get("code", ""))
self._content_filter_code = ContentFilterCodes(
inner_error.get("code", ContentFilterCodes.RESPONSIBLE_AI_POLICY_VIOLATION.value)
)
self._content_filter_result = dict(
[
key,
@@ -2,11 +2,11 @@
import json
from typing import Dict, Tuple
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.sk_pydantic import SKBaseModel
class FunctionCall(SKBaseModel):
class FunctionCall(KernelBaseModel):
"""Class to hold a function call response."""
name: str
@@ -7,7 +7,7 @@ from pydantic.dataclasses import dataclass
from semantic_kernel.connectors.ai.open_ai.request_settings.open_ai_request_settings import (
OpenAIChatRequestSettings,
)
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
logger = logging.getLogger(__name__)
@@ -72,7 +72,7 @@ class AzureDataSources:
# @dataclass
class ExtraBody(SKBaseModel):
class ExtraBody(KernelBaseModel):
data_sources: Optional[List[AzureDataSources]] = Field(None, alias="dataSources")
input_language: Optional[str] = Field(None, serialization_alias="inputLanguage")
output_language: Optional[str] = Field(None, serialization_alias="outputLanguage")
@@ -39,7 +39,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_completion_base
OpenAITextCompletionBase,
)
from semantic_kernel.connectors.ai.open_ai.utils import _parse_choices, _parse_message
from semantic_kernel.sk_pydantic import HttpsUrl
from semantic_kernel.kernel_pydantic import HttpsUrl
logger: logging.Logger = logging.getLogger(__name__)
@@ -17,7 +17,7 @@ from semantic_kernel.connectors.ai.open_ai.services.open_ai_handler import (
OpenAIModelTypes,
)
from semantic_kernel.connectors.telemetry import APP_INFO
from semantic_kernel.sk_pydantic import HttpsUrl
from semantic_kernel.kernel_pydantic import HttpsUrl
logger: logging.Logger = logging.getLogger(__name__)
@@ -6,17 +6,17 @@ from typing import Any, Dict, List, Optional, Tuple
from openai.types.chat import ChatCompletion
from semantic_kernel import Kernel, SKContext
from semantic_kernel import Kernel, KernelContext
from semantic_kernel.connectors.ai.open_ai.models.chat.function_call import FunctionCall
from semantic_kernel.connectors.ai.open_ai.semantic_functions.open_ai_chat_prompt_template import (
OpenAIChatPromptTemplate,
)
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
logger: logging.Logger = logging.getLogger(__name__)
def _describe_function(function: SKFunctionBase) -> Dict[str, str]:
def _describe_function(function: KernelFunctionBase) -> Dict[str, str]:
"""Create the object used for function_calling.
Assumes that arguments for semantic functions are optional, for native functions required.
@@ -105,14 +105,14 @@ async def execute_function_call(kernel: Kernel, function_call: FunctionCall, log
async def chat_completion_with_function_call(
kernel: Kernel,
context: SKContext,
context: KernelContext,
chat_plugin_name: Optional[str] = None,
chat_function_name: Optional[str] = None,
chat_function: Optional[SKFunctionBase] = None,
chat_function: Optional[KernelFunctionBase] = None,
*,
log: Optional[Any] = None,
**kwargs: Dict[str, Any],
) -> SKContext:
) -> KernelContext:
"""Perform a chat completion with auto-executing function calling.
This is a recursive function that will execute the chat function multiple times,
@@ -1,4 +1,4 @@
from semantic_kernel.connectors.openapi.sk_openapi import register_openapi_plugin
from semantic_kernel.connectors.openapi.kernel_openapi import register_openapi_plugin
__all__ = [
"register_openapi_plugin",
@@ -10,13 +10,13 @@ from openapi_core.contrib.requests import RequestsOpenAPIRequest
from openapi_core.exceptions import OpenAPIError
from prance import ResolvingParser
from semantic_kernel import Kernel, SKContext
from semantic_kernel import Kernel, KernelContext
from semantic_kernel.connectors.ai.open_ai.const import (
USER_AGENT,
)
from semantic_kernel.connectors.telemetry import HTTP_USER_AGENT
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
logger: logging.Logger = logging.getLogger(__name__)
@@ -243,7 +243,7 @@ Registers a plugin with the kernel that can run OpenAPI operations.
:param kernel: The kernel to register the plugin with
:param plugin_name: The name of the plugin
:param openapi_document: The OpenAPI document to register. Can be a filename or URL
:return: A dictionary of SKFunctions keyed by operationId
:return: A dictionary of KernelFunctions keyed by operationId
"""
@@ -251,7 +251,7 @@ def register_openapi_plugin(
kernel: Kernel,
plugin_name: str,
openapi_document: str,
) -> Dict[str, SKFunctionBase]:
) -> Dict[str, KernelFunctionBase]:
parser = OpenApiParser()
parsed_doc = parser.parse(openapi_document)
operations = parser.create_rest_api_operations(parsed_doc)
@@ -260,19 +260,19 @@ def register_openapi_plugin(
plugin = {}
def create_run_operation_function(runner: OpenApiRunner, operation: RestApiOperation):
@sk_function(
@kernel_function(
description=operation.summary if operation.summary else operation.description,
name=operation_id,
)
@sk_function_context_parameter(name="path_params", description="A dictionary of path parameters")
@sk_function_context_parameter(name="query_params", description="A dictionary of query parameters")
@sk_function_context_parameter(name="headers", description="A dictionary of headers")
@sk_function_context_parameter(name="request_body", description="A dictionary of the request body")
async def run_openapi_operation(sk_context: SKContext) -> str:
path_params = sk_context.variables.get("path_params")
query_params = sk_context.variables.get("query_params")
headers = sk_context.variables.get("headers")
request_body = sk_context.variables.get("request_body")
@kernel_function_context_parameter(name="path_params", description="A dictionary of path parameters")
@kernel_function_context_parameter(name="query_params", description="A dictionary of query parameters")
@kernel_function_context_parameter(name="headers", description="A dictionary of headers")
@kernel_function_context_parameter(name="request_body", description="A dictionary of the request body")
async def run_openapi_operation(kernel_context: KernelContext) -> str:
path_params = kernel_context.variables.get("path_params")
query_params = kernel_context.variables.get("query_params")
headers = kernel_context.variables.get("headers")
request_body = kernel_context.variables.get("request_body")
response = await runner.run_operation(
operation,
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from semantic_kernel.kernel import Kernel
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
class ConversationSummaryPlugin:
@@ -11,7 +11,7 @@ class ConversationSummaryPlugin:
Semantic plugin that enables conversations summarization.
"""
from semantic_kernel.plugin_definition import sk_function
from semantic_kernel.plugin_definition import kernel_function
# The max tokens to process in a single semantic function call.
_max_tokens = 1024
@@ -36,18 +36,18 @@ class ConversationSummaryPlugin:
top_p=0.5,
)
@sk_function(
@kernel_function(
description="Given a long conversation transcript, summarize the conversation.",
name="SummarizeConversation",
input_description="A long conversation transcript.",
)
async def summarize_conversation_async(self, input: str, context: "SKContext") -> "SKContext":
async def summarize_conversation_async(self, input: str, context: "KernelContext") -> "KernelContext":
"""
Given a long conversation transcript, summarize the conversation.
:param input: A long conversation transcript.
:param context: The SKContext for function execution.
:return: SKContext with the summarized conversation result.
:param context: The KernelContext for function execution.
:return: KernelContext with the summarized conversation result.
"""
from semantic_kernel.text import text_chunker
from semantic_kernel.text.function_extension import (
@@ -5,14 +5,14 @@ import typing as t
import aiofiles
from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
if t.TYPE_CHECKING:
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
class FileIOPlugin(SKBaseModel):
class FileIOPlugin(KernelBaseModel):
"""
Description: Read and write from a file.
@@ -25,7 +25,7 @@ class FileIOPlugin(SKBaseModel):
{{file.writeAsync}}
"""
@sk_function(
@kernel_function(
description="Read a file",
name="readAsync",
input_description="Path of the source file",
@@ -49,13 +49,13 @@ class FileIOPlugin(SKBaseModel):
content = await fp.read()
return content
@sk_function(
@kernel_function(
description="Write a file",
name="writeAsync",
)
@sk_function_context_parameter(name="path", description="Destination path")
@sk_function_context_parameter(name="content", description="File content")
async def write_async(self, context: "SKContext") -> None:
@kernel_function_context_parameter(name="path", description="Destination path")
@kernel_function_context_parameter(name="content", description="File content")
async def write_async(self, context: "KernelContext") -> None:
"""
Write a file
@@ -5,14 +5,14 @@ import typing as t
import aiohttp
from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
if t.TYPE_CHECKING:
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
class HttpPlugin(SKBaseModel):
class HttpPlugin(KernelBaseModel):
"""
A plugin that provides HTTP functionality.
@@ -27,7 +27,7 @@ class HttpPlugin(SKBaseModel):
{{http.deleteAsync $url}}
"""
@sk_function(description="Makes a GET request to a uri", name="getAsync")
@kernel_function(description="Makes a GET request to a uri", name="getAsync")
async def get_async(self, url: str) -> str:
"""
Sends an HTTP GET request to the specified URI and returns
@@ -44,9 +44,9 @@ class HttpPlugin(SKBaseModel):
async with session.get(url, raise_for_status=True) as response:
return await response.text()
@sk_function(description="Makes a POST request to a uri", name="postAsync")
@sk_function_context_parameter(name="body", description="The body of the request")
async def post_async(self, url: str, context: "SKContext") -> str:
@kernel_function(description="Makes a POST request to a uri", name="postAsync")
@kernel_function_context_parameter(name="body", description="The body of the request")
async def post_async(self, url: str, context: "KernelContext") -> str:
"""
Sends an HTTP POST request to the specified URI and returns
the response body as a string.
@@ -67,9 +67,9 @@ class HttpPlugin(SKBaseModel):
async with session.post(url, headers=headers, data=data, raise_for_status=True) as response:
return await response.text()
@sk_function(description="Makes a PUT request to a uri", name="putAsync")
@sk_function_context_parameter(name="body", description="The body of the request")
async def put_async(self, url: str, context: "SKContext") -> str:
@kernel_function(description="Makes a PUT request to a uri", name="putAsync")
@kernel_function_context_parameter(name="body", description="The body of the request")
async def put_async(self, url: str, context: "KernelContext") -> str:
"""
Sends an HTTP PUT request to the specified URI and returns
the response body as a string.
@@ -89,7 +89,7 @@ class HttpPlugin(SKBaseModel):
async with session.put(url, headers=headers, data=data, raise_for_status=True) as response:
return await response.text()
@sk_function(description="Makes a DELETE request to a uri", name="deleteAsync")
@kernel_function(description="Makes a DELETE request to a uri", name="deleteAsync")
async def delete_async(self, url: str) -> str:
"""
Sends an HTTP DELETE request to the specified URI and returns
@@ -1,14 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
import typing as t
from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
if t.TYPE_CHECKING:
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
class MathPlugin(SKBaseModel):
class MathPlugin(KernelBaseModel):
"""
Description: MathPlugin provides a set of functions to make Math calculations.
@@ -16,21 +16,21 @@ class MathPlugin(SKBaseModel):
kernel.import_plugin(MathPlugin(), plugin_name="math")
Examples:
{{math.Add}} => Returns the sum of initial_value_text and Amount (provided in the SKContext)
{{math.Add}} => Returns the sum of initial_value_text and Amount (provided in the KernelContext)
"""
@sk_function(
@kernel_function(
description="Adds value to a value",
name="Add",
input_description="The value to add",
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name="Amount",
description="Amount to add",
type="number",
required=True,
)
def add(self, initial_value_text: str, context: "SKContext") -> str:
def add(self, initial_value_text: str, context: "KernelContext") -> str:
"""
Returns the Addition result of initial and amount values provided.
@@ -40,18 +40,18 @@ class MathPlugin(SKBaseModel):
"""
return MathPlugin.add_or_subtract(initial_value_text, context, add=True)
@sk_function(
@kernel_function(
description="Subtracts value to a value",
name="Subtract",
input_description="The value to subtract",
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name="Amount",
description="Amount to subtract",
type="number",
required=True,
)
def subtract(self, initial_value_text: str, context: "SKContext") -> str:
def subtract(self, initial_value_text: str, context: "KernelContext") -> str:
"""
Returns the difference of numbers provided.
@@ -62,7 +62,7 @@ class MathPlugin(SKBaseModel):
return MathPlugin.add_or_subtract(initial_value_text, context, add=False)
@staticmethod
def add_or_subtract(initial_value_text: str, context: "SKContext", add: bool) -> str:
def add_or_subtract(initial_value_text: str, context: "KernelContext", add: bool) -> str:
"""
Helper function to perform addition or subtraction based on the add flag.
@@ -4,16 +4,16 @@ import logging
import typing as t
from typing import ClassVar
from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
if t.TYPE_CHECKING:
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
logger: logging.Logger = logging.getLogger(__name__)
class TextMemoryPlugin(SKBaseModel):
class TextMemoryPlugin(KernelBaseModel):
COLLECTION_PARAM: ClassVar[str] = "collection"
RELEVANCE_PARAM: ClassVar[str] = "relevance"
KEY_PARAM: ClassVar[str] = "key"
@@ -23,32 +23,32 @@ class TextMemoryPlugin(SKBaseModel):
DEFAULT_LIMIT: ClassVar[int] = "1"
# @staticmethod
@sk_function(
@kernel_function(
description="Recall a fact from the long term memory",
name="recall",
input_description="The information to retrieve",
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name=COLLECTION_PARAM,
description="The collection to search for information",
default_value=DEFAULT_COLLECTION,
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name=RELEVANCE_PARAM,
description="The relevance score, from 0.0 to 1.0; 1.0 means perfect match",
default_value=DEFAULT_RELEVANCE,
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name=LIMIT_PARAM,
description="The maximum number of relevant memories to recall.",
default_value=DEFAULT_LIMIT,
)
async def recall_async(self, ask: str, context: "SKContext") -> str:
async def recall_async(self, ask: str, context: "KernelContext") -> str:
"""
Recall a fact from the long term memory.
Example:
sk_context["input"] = "what is the capital of France?"
context["input"] = "what is the capital of France?"
{{memory.recall $input}} => "Paris"
Args:
@@ -90,27 +90,27 @@ class TextMemoryPlugin(SKBaseModel):
return results[0].text if limit == 1 else json.dumps([r.text for r in results])
@sk_function(
@kernel_function(
description="Save information to semantic memory",
name="save",
input_description="The information to save",
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name=COLLECTION_PARAM,
description="The collection to save the information",
default_value=DEFAULT_COLLECTION,
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name=KEY_PARAM,
description="The unique key to associate with the information",
)
async def save_async(self, text: str, context: "SKContext") -> None:
async def save_async(self, text: str, context: "KernelContext") -> None:
"""
Save a fact to the long term memory.
Example:
sk_context["input"] = "the capital of France is Paris"
sk_context[TextMemoryPlugin.KEY_PARAM] = "countryInfo1"
context["input"] = "the capital of France is Paris"
context[TextMemoryPlugin.KEY_PARAM] = "countryInfo1"
{{memory.save $input}}
Args:
@@ -1,10 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.plugin_definition import sk_function
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function
class TextPlugin(SKBaseModel):
class TextPlugin(KernelBaseModel):
"""
TextPlugin provides a set of functions to manipulate strings.
@@ -12,73 +12,73 @@ class TextPlugin(SKBaseModel):
kernel.import_plugin(TextPlugin(), plugin_name="text")
Examples:
SKContext["input"] = " hello world "
KernelContext["input"] = " hello world "
{{text.trim $input}} => "hello world"
SKContext["input"] = " hello world "
KernelContext["input"] = " hello world "
{{text.trimStart $input} => "hello world "
SKContext["input"] = " hello world "
KernelContext["input"] = " hello world "
{{text.trimEnd $input} => " hello world"
SKContext["input"] = "hello world"
KernelContext["input"] = "hello world"
{{text.uppercase $input}} => "HELLO WORLD"
SKContext["input"] = "HELLO WORLD"
KernelContext["input"] = "HELLO WORLD"
{{text.lowercase $input}} => "hello world"
"""
@sk_function(description="Trim whitespace from the start and end of a string.")
@kernel_function(description="Trim whitespace from the start and end of a string.")
def trim(self, text: str) -> str:
"""
Trim whitespace from the start and end of a string.
Example:
SKContext["input"] = " hello world "
KernelContext["input"] = " hello world "
{{text.trim $input}} => "hello world"
"""
return text.strip()
@sk_function(description="Trim whitespace from the start of a string.")
@kernel_function(description="Trim whitespace from the start of a string.")
def trim_start(self, text: str) -> str:
"""
Trim whitespace from the start of a string.
Example:
SKContext["input"] = " hello world "
KernelContext["input"] = " hello world "
{{text.trim $input}} => "hello world "
"""
return text.lstrip()
@sk_function(description="Trim whitespace from the end of a string.")
@kernel_function(description="Trim whitespace from the end of a string.")
def trim_end(self, text: str) -> str:
"""
Trim whitespace from the end of a string.
Example:
SKContext["input"] = " hello world "
KernelContext["input"] = " hello world "
{{text.trim $input}} => " hello world"
"""
return text.rstrip()
@sk_function(description="Convert a string to uppercase.")
@kernel_function(description="Convert a string to uppercase.")
def uppercase(self, text: str) -> str:
"""
Convert a string to uppercase.
Example:
SKContext["input"] = "hello world"
KernelContext["input"] = "hello world"
{{text.uppercase $input}} => "HELLO WORLD"
"""
return text.upper()
@sk_function(description="Convert a string to lowercase.")
@kernel_function(description="Convert a string to lowercase.")
def lowercase(self, text: str) -> str:
"""
Convert a string to lowercase.
Example:
SKContext["input"] = "HELLO WORLD"
KernelContext["input"] = "HELLO WORLD"
{{text.lowercase $input}} => "hello world"
"""
return text.lower()
@@ -2,11 +2,11 @@
import datetime
from semantic_kernel.plugin_definition import sk_function
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function
class TimePlugin(SKBaseModel):
class TimePlugin(KernelBaseModel):
"""
Description: TimePlugin provides a set of functions
to get the current time and date.
@@ -38,7 +38,7 @@ class TimePlugin(SKBaseModel):
{{time.timeZoneName}} => PST
"""
@sk_function(description="Get the current date.")
@kernel_function(description="Get the current date.")
def date(self) -> str:
"""
Get the current date
@@ -49,7 +49,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%A, %d %B, %Y")
@sk_function(description="Get the current date.")
@kernel_function(description="Get the current date.")
def today(self) -> str:
"""
Get the current date
@@ -59,7 +59,7 @@ class TimePlugin(SKBaseModel):
"""
return self.date()
@sk_function(description="Get the current date in iso format.")
@kernel_function(description="Get the current date in iso format.")
def iso_date(self) -> str:
"""
Get the current date in iso format
@@ -70,7 +70,7 @@ class TimePlugin(SKBaseModel):
today = datetime.date.today()
return today.isoformat()
@sk_function(description="Get the current date and time in the local time zone")
@kernel_function(description="Get the current date and time in the local time zone")
def now(self) -> str:
"""
Get the current date and time in the local time zone"
@@ -81,7 +81,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%A, %B %d, %Y %I:%M %p")
@sk_function(description="Get the current date and time in UTC", name="utcNow")
@kernel_function(description="Get the current date and time in UTC", name="utcNow")
def utc_now(self) -> str:
"""
Get the current date and time in UTC
@@ -92,7 +92,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.utcnow()
return now.strftime("%A, %B %d, %Y %I:%M %p")
@sk_function(description="Get the current time in the local time zone")
@kernel_function(description="Get the current time in the local time zone")
def time(self) -> str:
"""
Get the current time in the local time zone
@@ -103,7 +103,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%I:%M:%S %p")
@sk_function(description="Get the current year")
@kernel_function(description="Get the current year")
def year(self) -> str:
"""
Get the current year
@@ -114,7 +114,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%Y")
@sk_function(description="Get the current month")
@kernel_function(description="Get the current month")
def month(self) -> str:
"""
Get the current month
@@ -125,7 +125,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%B")
@sk_function(description="Get the current month number")
@kernel_function(description="Get the current month number")
def month_number(self) -> str:
"""
Get the current month number
@@ -136,7 +136,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%m")
@sk_function(description="Get the current day")
@kernel_function(description="Get the current day")
def day(self) -> str:
"""
Get the current day of the month
@@ -147,7 +147,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%d")
@sk_function(description="Get the current day of the week", name="dayOfWeek")
@kernel_function(description="Get the current day of the week", name="dayOfWeek")
def day_of_week(self) -> str:
"""
Get the current day of the week
@@ -158,7 +158,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%A")
@sk_function(description="Get the current hour")
@kernel_function(description="Get the current hour")
def hour(self) -> str:
"""
Get the current hour
@@ -169,7 +169,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%I %p")
@sk_function(description="Get the current hour number", name="hourNumber")
@kernel_function(description="Get the current hour number", name="hourNumber")
def hour_number(self) -> str:
"""
Get the current hour number
@@ -180,7 +180,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%H")
@sk_function(description="Get the current minute")
@kernel_function(description="Get the current minute")
def minute(self) -> str:
"""
Get the current minute
@@ -191,7 +191,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%M")
@sk_function(description="Get the date of offset from today by a provided number of days")
@kernel_function(description="Get the date of offset from today by a provided number of days")
def days_ago(self, days: str) -> str:
"""
Get the date a provided number of days in the past
@@ -202,13 +202,13 @@ class TimePlugin(SKBaseModel):
The date of the offset day.
Example:
SKContext["input"] = "3"
KernelContext["input"] = "3"
{{time.days_ago $input}} => Sunday, 7 May, 2023
"""
d = datetime.date.today() - datetime.timedelta(days=int(days))
return d.strftime("%A, %d %B, %Y")
@sk_function(
@kernel_function(
description="""Get the date of the last day matching the supplied week day name in English.
Example: Che giorno era 'Martedi' scorso -> dateMatchingLastDayName 'Tuesday' => Tuesday,
16 May, 2023"""
@@ -223,7 +223,7 @@ class TimePlugin(SKBaseModel):
The date of the matching day.
Example:
SKContext["input"] = "Sunday"
KernelContext["input"] = "Sunday"
{{time.date_matching_last_day_name $input}} => Sunday, 7 May, 2023
"""
d = datetime.date.today()
@@ -233,7 +233,7 @@ class TimePlugin(SKBaseModel):
return d.strftime("%A, %d %B, %Y")
raise ValueError("day_name is not recognized")
@sk_function(description="Get the seconds on the current minute")
@kernel_function(description="Get the seconds on the current minute")
def second(self) -> str:
"""
Get the seconds on the current minute
@@ -244,7 +244,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%S")
@sk_function(description="Get the current time zone offset", name="timeZoneOffset")
@kernel_function(description="Get the current time zone offset", name="timeZoneOffset")
def time_zone_offset(self) -> str:
"""
Get the current time zone offset
@@ -255,7 +255,7 @@ class TimePlugin(SKBaseModel):
now = datetime.datetime.now()
return now.strftime("%z")
@sk_function(description="Get the current time zone name", name="timeZoneName")
@kernel_function(description="Get the current time zone name", name="timeZoneName")
def time_zone_name(self) -> str:
"""
Get the current time zone name
@@ -2,11 +2,11 @@
import asyncio
from semantic_kernel.plugin_definition import sk_function
from semantic_kernel.sk_pydantic import SKBaseModel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.plugin_definition import kernel_function
class WaitPlugin(SKBaseModel):
class WaitPlugin(KernelBaseModel):
"""
WaitPlugin provides a set of functions to wait for a certain amount of time.
@@ -17,7 +17,7 @@ class WaitPlugin(SKBaseModel):
{{wait.seconds 5}} => Wait for 5 seconds
"""
@sk_function(description="Wait for a certain number of seconds.")
@kernel_function(description="Wait for a certain number of seconds.")
async def wait(self, seconds_text: str) -> None:
try:
seconds = max(float(seconds_text), 0)
@@ -1,10 +1,10 @@
import typing as t
from semantic_kernel.connectors.search_engine.connector import ConnectorBase
from semantic_kernel.plugin_definition import sk_function, sk_function_context_parameter
from semantic_kernel.plugin_definition import kernel_function, kernel_function_context_parameter
if t.TYPE_CHECKING:
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
class WebSearchEnginePlugin:
@@ -19,7 +19,7 @@ class WebSearchEnginePlugin:
{{WebSearch.SearchAsync "What is semantic kernel?"}}
=> Returns the first `num_results` number of results for the given search query
and ignores the first `offset` number of results
(num_results and offset are specified in SKContext)
(num_results and offset are specified in KernelContext)
"""
_connector: "ConnectorBase"
@@ -27,18 +27,18 @@ class WebSearchEnginePlugin:
def __init__(self, connector: "ConnectorBase") -> None:
self._connector = connector
@sk_function(description="Performs a web search for a given query", name="searchAsync")
@sk_function_context_parameter(
@kernel_function(description="Performs a web search for a given query", name="searchAsync")
@kernel_function_context_parameter(
name="num_results",
description="The number of search results to return",
default_value="1",
)
@sk_function_context_parameter(
@kernel_function_context_parameter(
name="offset",
description="The number of search results to skip",
default_value="0",
)
async def search_async(self, query: str, context: "SKContext") -> str:
async def search_async(self, query: str, context: "KernelContext") -> str:
"""
Returns the search results of the query provided.
Returns `num_results` results and ignores the first `offset`.
@@ -1,9 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.events.sk_events_args import SKEventArgs
from semantic_kernel.events.kernel_events_args import KernelEventArgs
class FunctionInvokedEventArgs(SKEventArgs):
class FunctionInvokedEventArgs(KernelEventArgs):
def __init__(self, function_view, context):
super().__init__(function_view, context)
self._repeat_requested = False
@@ -1,9 +1,9 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.events.sk_events_args import SKEventArgs
from semantic_kernel.events.kernel_events_args import KernelEventArgs
class FunctionInvokingEventArgs(SKEventArgs):
class FunctionInvokingEventArgs(KernelEventArgs):
def __init__(self, function_view, context):
super().__init__(function_view, context)
self._skip_requested = False
@@ -1,11 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.plugin_definition.function_view import FunctionView
class SKEventArgs:
def __init__(self, function_view: FunctionView, context: SKContext):
class KernelEventArgs:
def __init__(self, function_view: FunctionView, context: KernelContext):
if context is None or function_view is None:
raise ValueError("function_view and context cannot be None")
+31 -30
View File
@@ -26,9 +26,9 @@ from semantic_kernel.memory.null_memory import NullMemory
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory
from semantic_kernel.memory.semantic_text_memory_base import SemanticTextMemoryBase
from semantic_kernel.orchestration.context_variables import ContextVariables
from semantic_kernel.orchestration.sk_context import SKContext
from semantic_kernel.orchestration.sk_function import SKFunction
from semantic_kernel.orchestration.sk_function_base import SKFunctionBase
from semantic_kernel.orchestration.kernel_context import KernelContext
from semantic_kernel.orchestration.kernel_function import KernelFunction
from semantic_kernel.orchestration.kernel_function_base import KernelFunctionBase
from semantic_kernel.plugin_definition.function_view import FunctionView
from semantic_kernel.plugin_definition.plugin_collection import PluginCollection
from semantic_kernel.plugin_definition.plugin_collection_base import (
@@ -110,7 +110,7 @@ class Kernel:
plugin_name: Optional[str],
function_name: str,
function_config: SemanticFunctionConfig,
) -> SKFunctionBase:
) -> KernelFunctionBase:
if plugin_name is None or plugin_name == "":
plugin_name = PluginCollection.GLOBAL_PLUGIN
assert plugin_name is not None # for type checker
@@ -126,14 +126,14 @@ class Kernel:
def register_native_function(
self,
plugin_name: Optional[str],
sk_function: Callable,
) -> SKFunctionBase:
if not hasattr(sk_function, "__sk_function__"):
kernel_function: Callable,
) -> KernelFunctionBase:
if not hasattr(kernel_function, "__kernel_function__"):
raise KernelException(
KernelException.ErrorCodes.InvalidFunctionType,
"sk_function argument must be decorated with @sk_function",
"kernel_function argument must be decorated with @kernel_function",
)
function_name = sk_function.__sk_function_name__
function_name = kernel_function.__kernel_function_name__
if plugin_name is None or plugin_name == "":
plugin_name = PluginCollection.GLOBAL_PLUGIN
@@ -142,7 +142,7 @@ class Kernel:
validate_plugin_name(plugin_name)
validate_function_name(function_name)
function = SKFunction.from_native_method(sk_function, plugin_name)
function = KernelFunction.from_native_method(kernel_function, plugin_name)
if self.plugins.has_function(plugin_name, function_name):
raise KernelException(
@@ -158,7 +158,7 @@ class Kernel:
async def run_stream_async(
self,
*functions: Any,
input_context: Optional[SKContext] = None,
input_context: Optional[KernelContext] = None,
input_vars: Optional[ContextVariables] = None,
input_str: Optional[str] = None,
):
@@ -196,7 +196,7 @@ class Kernel:
variables = variables.merge_or_overwrite(new_vars=input_vars, overwrite=False)
else:
variables = ContextVariables()
context = SKContext(
context = KernelContext(
variables,
self._memory,
self._plugin_collection.read_only_plugin_collection,
@@ -223,11 +223,11 @@ class Kernel:
async def run_async(
self,
*functions: Any,
input_context: Optional[SKContext] = None,
input_context: Optional[KernelContext] = None,
input_vars: Optional[ContextVariables] = None,
input_str: Optional[str] = None,
**kwargs: Dict[str, Any],
) -> SKContext:
) -> KernelContext:
# if the user passed in a context, prioritize it, but merge with any other inputs
if input_context is not None:
context = input_context
@@ -251,7 +251,7 @@ class Kernel:
variables = variables.merge_or_overwrite(new_vars=input_vars, overwrite=False)
else:
variables = ContextVariables()
context = SKContext(
context = KernelContext(
variables,
self._memory,
self._plugin_collection.read_only_plugin_collection,
@@ -260,8 +260,9 @@ class Kernel:
pipeline_step = 0
for func in functions:
while True:
assert isinstance(func, SKFunctionBase), (
"All func arguments to Kernel.run*(inputs, func1, func2, ...) " "must be SKFunctionBase instances"
assert isinstance(func, KernelFunctionBase), (
"All func arguments to Kernel.run*(inputs, func1, func2, ...) "
"must be KernelFunctionBase instances"
)
if context.error_occurred:
@@ -333,7 +334,7 @@ class Kernel:
return context
def func(self, plugin_name: str, function_name: str) -> SKFunctionBase:
def func(self, plugin_name: str, function_name: str) -> KernelFunctionBase:
if self.plugins.has_native_function(plugin_name, function_name):
return self.plugins.get_native_function(plugin_name, function_name)
@@ -368,14 +369,14 @@ class Kernel:
def register_memory_store(self, memory_store: MemoryStoreBase) -> None:
self.use_memory(memory_store)
def create_new_context(self, variables: Optional[ContextVariables] = None) -> SKContext:
return SKContext(
def create_new_context(self, variables: Optional[ContextVariables] = None) -> KernelContext:
return KernelContext(
ContextVariables() if not variables else variables,
self._memory,
self.plugins,
)
def on_function_invoking(self, function_view: FunctionView, context: SKContext) -> FunctionInvokingEventArgs:
def on_function_invoking(self, function_view: FunctionView, context: KernelContext) -> FunctionInvokingEventArgs:
if self._function_invoking_handlers:
args = FunctionInvokingEventArgs(function_view, context)
for handler in self._function_invoking_handlers.values():
@@ -383,7 +384,7 @@ class Kernel:
return args
return None
def on_function_invoked(self, function_view: FunctionView, context: SKContext) -> FunctionInvokedEventArgs:
def on_function_invoked(self, function_view: FunctionView, context: KernelContext) -> FunctionInvokedEventArgs:
if self._function_invoked_handlers:
args = FunctionInvokedEventArgs(function_view, context)
for handler in self._function_invoked_handlers.values():
@@ -391,7 +392,7 @@ class Kernel:
return args
return None
def import_plugin(self, plugin_instance: Any, plugin_name: str = "") -> Dict[str, SKFunctionBase]:
def import_plugin(self, plugin_instance: Any, plugin_name: str = "") -> Dict[str, KernelFunctionBase]:
if plugin_name.strip() == "":
plugin_name = PluginCollection.GLOBAL_PLUGIN
logger.debug(f"Importing plugin {plugin_name} into the global namespace")
@@ -407,10 +408,10 @@ class Kernel:
# Read every method from the plugin instance
for _, candidate in candidates:
# If the method is a semantic function, register it
if not hasattr(candidate, "__sk_function__"):
if not hasattr(candidate, "__kernel_function__"):
continue
functions.append(SKFunction.from_native_method(candidate, plugin_name))
functions.append(KernelFunction.from_native_method(candidate, plugin_name))
logger.debug(f"Methods imported: {len(functions)}")
@@ -627,7 +628,7 @@ class Kernel:
plugin_name: str,
function_name: str,
function_config: SemanticFunctionConfig,
) -> SKFunctionBase:
) -> KernelFunctionBase:
function_type = function_config.prompt_template_config.type
if not function_type == "completion":
raise AIException(
@@ -635,7 +636,7 @@ class Kernel:
f"Function type not supported: {function_type}",
)
function = SKFunction.from_semantic_config(plugin_name, function_name, function_config)
function = KernelFunction.from_semantic_config(plugin_name, function_name, function_config)
function.request_settings.update_from_ai_request_settings(
function_config.prompt_template_config.execution_settings
)
@@ -698,7 +699,7 @@ class Kernel:
def import_native_plugin_from_directory(
self, parent_directory: str, plugin_directory_name: str
) -> Dict[str, SKFunctionBase]:
) -> Dict[str, KernelFunctionBase]:
MODULE_NAME = "native_function"
validate_plugin_name(plugin_directory_name)
@@ -727,7 +728,7 @@ class Kernel:
def import_semantic_plugin_from_directory(
self, parent_directory: str, plugin_directory_name: str
) -> Dict[str, SKFunctionBase]:
) -> Dict[str, KernelFunctionBase]:
CONFIG_FILE = "config.json"
PROMPT_FILE = "skprompt.txt"
@@ -775,7 +776,7 @@ class Kernel:
plugin_name: Optional[str] = None,
description: Optional[str] = None,
**kwargs: Any,
) -> "SKFunctionBase":
) -> "KernelFunctionBase":
function_name = function_name if function_name is not None else f"f_{str(uuid4()).replace('-', '_')}"
config = PromptTemplateConfig(

Some files were not shown because too many files have changed in this diff Show More