chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.InMemory;
|
||||
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
|
||||
using Microsoft.SemanticKernel.Services;
|
||||
|
||||
namespace Optimization;
|
||||
|
||||
/// <summary>
|
||||
/// This example shows how to use FrugalGPT techniques to reduce cost and improve LLM-related task performance.
|
||||
/// More information here: https://arxiv.org/abs/2305.05176.
|
||||
/// </summary>
|
||||
public sealed class FrugalGPTWithFilters(ITestOutputHelper output) : BaseTest(output)
|
||||
{
|
||||
/// <summary>
|
||||
/// One of the FrugalGPT techniques is to reduce prompt size when using few-shot prompts.
|
||||
/// If prompt contains a lof of examples to help LLM to provide the best result, it's possible to send only a couple of them to reduce amount of tokens.
|
||||
/// Vector similarity can be used to pick the best examples from example set for specific request.
|
||||
/// Following example shows how to optimize email classification request by reducing prompt size with vector similarity search.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ReducePromptSizeAsync()
|
||||
{
|
||||
// Define email classification examples with email body and labels.
|
||||
var examples = new List<string>
|
||||
{
|
||||
"Hey, just checking in to see how you're doing! - Personal",
|
||||
"Can you pick up some groceries on your way back home? We need milk and bread. - Personal, Tasks",
|
||||
"Happy Birthday! Wishing you a fantastic day filled with love and joy. - Personal",
|
||||
"Let's catch up over coffee this Saturday. It's been too long! - Personal, Events",
|
||||
"Please review the attached document and provide your feedback by EOD. - Work",
|
||||
"Our team meeting is scheduled for 10 AM tomorrow in the main conference room. - Work",
|
||||
"The quarterly financial report is due next Monday. Ensure all data is updated. - Work, Tasks",
|
||||
"Can you send me the latest version of the project plan? Thanks! - Work",
|
||||
"You're invited to our annual summer picnic! RSVP by June 25th. - Events",
|
||||
"Join us for a webinar on digital marketing trends this Thursday at 3 PM. - Events",
|
||||
"Save the date for our charity gala on September 15th. We hope to see you there! - Events",
|
||||
"Don't miss our customer appreciation event next week. Sign up now! - Events, Notifications",
|
||||
"Your order has been shipped and will arrive by June 20th. - Notifications",
|
||||
"We've updated our policies. Please review the changes. - Notifications",
|
||||
"Your username was successfully changed. If this wasn't you, contact support immediately. - Notifications",
|
||||
"The system upgrade will occur this weekend. - Notifications, Work",
|
||||
"Don't forget to submit your timesheet by 5 PM today. - Tasks, Work",
|
||||
"Pick up the dry cleaning before they close at 7 PM. - Tasks",
|
||||
"Complete the online training module by the end of the week. - Tasks, Work",
|
||||
"Send out the meeting invites for next week's project kickoff. - Tasks, Work"
|
||||
};
|
||||
|
||||
// Initialize kernel with chat completion and embedding generation services.
|
||||
// It's possible to combine different models from different AI providers to achieve the lowest token usage.
|
||||
var kernel = Kernel.CreateBuilder()
|
||||
.AddOpenAIChatCompletion(
|
||||
modelId: "gpt-4",
|
||||
apiKey: TestConfiguration.OpenAI.ApiKey)
|
||||
.AddOpenAIEmbeddingGenerator(
|
||||
modelId: "text-embedding-3-small",
|
||||
apiKey: TestConfiguration.OpenAI.ApiKey)
|
||||
.Build();
|
||||
|
||||
// Initialize few-shot prompt.
|
||||
var function = kernel.CreateFunctionFromPrompt(
|
||||
new()
|
||||
{
|
||||
Template =
|
||||
"""
|
||||
Available classification labels: Personal, Work, Events, Notifications, Tasks
|
||||
Email classification examples:
|
||||
{{#each Examples}}
|
||||
{{this}}
|
||||
{{/each}}
|
||||
|
||||
Email body to classify:
|
||||
{{Request}}
|
||||
""",
|
||||
TemplateFormat = "handlebars"
|
||||
},
|
||||
new HandlebarsPromptTemplateFactory()
|
||||
);
|
||||
|
||||
// Define arguments with few-shot examples and actual email for classification.
|
||||
var arguments = new KernelArguments
|
||||
{
|
||||
["Examples"] = examples,
|
||||
["Request"] = "Your dentist appointment is tomorrow at 10 AM. Please remember to bring your insurance card."
|
||||
};
|
||||
|
||||
// Invoke defined function to see initial result.
|
||||
var result = await kernel.InvokeAsync(function, arguments);
|
||||
|
||||
Console.WriteLine(result); // Personal, Notifications
|
||||
Console.WriteLine(result.Metadata?["Usage"]?.AsJson()); // Total tokens: ~430
|
||||
|
||||
// Add few-shot prompt optimization filter.
|
||||
// The filter uses in-memory store for vector similarity search and text embedding generation service to generate embeddings.
|
||||
var vectorStore = new InMemoryVectorStore();
|
||||
var embeddingGenerator = kernel.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();
|
||||
|
||||
// Register optimization filter.
|
||||
kernel.PromptRenderFilters.Add(new FewShotPromptOptimizationFilter(vectorStore, embeddingGenerator));
|
||||
|
||||
// Get result again and compare the usage.
|
||||
result = await kernel.InvokeAsync(function, arguments);
|
||||
|
||||
Console.WriteLine(result); // Personal, Notifications
|
||||
Console.WriteLine(result.Metadata?["Usage"]?.AsJson()); // Total tokens: ~150
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LLM cascade technique allows to use multiple LLMs sequentially starting from cheaper model,
|
||||
/// evaluate LLM result and return it in case it meets the quality criteria. Otherwise, proceed with next LLM in queue,
|
||||
/// until the result will be acceptable.
|
||||
/// Following example uses mock result generation and evaluation for demonstration purposes.
|
||||
/// Result evaluation examples including BERTScore, BLEU, METEOR and COMET metrics can be found here:
|
||||
/// https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/Demos/QualityCheck.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task LLMCascadeAsync()
|
||||
{
|
||||
// Create kernel builder.
|
||||
var builder = Kernel.CreateBuilder();
|
||||
|
||||
// Register chat completion services for demonstration purposes.
|
||||
// This registration is similar to AddAzureOpenAIChatCompletion and AddOpenAIChatCompletion methods.
|
||||
builder.Services.AddSingleton<IChatCompletionService>(new MockChatCompletionService("model1", "Hi there! I'm doing well, thank you! How about yourself?"));
|
||||
builder.Services.AddSingleton<IChatCompletionService>(new MockChatCompletionService("model2", "Hello! I'm great, thanks for asking. How are you doing today?"));
|
||||
builder.Services.AddSingleton<IChatCompletionService>(new MockChatCompletionService("model3", "Hey! I'm fine, thanks. How's your day going so far?"));
|
||||
|
||||
// Register LLM cascade filter with model execution order, acceptance criteria for result and service for output.
|
||||
// In real use-cases, execution order should start from cheaper to more expensive models.
|
||||
// If first model will produce acceptable result, then it will be returned immediately.
|
||||
builder.Services.AddSingleton<IFunctionInvocationFilter>(new LLMCascadeFilter(
|
||||
modelExecutionOrder: ["model1", "model2", "model3"],
|
||||
acceptanceCriteria: result => result.Contains("Hey!"),
|
||||
output: this.Output));
|
||||
|
||||
// Build kernel.
|
||||
var kernel = builder.Build();
|
||||
|
||||
// Send a request.
|
||||
var result = await kernel.InvokePromptAsync("Hi, how are you today?");
|
||||
|
||||
Console.WriteLine($"\nFinal result: {result}");
|
||||
|
||||
// Output:
|
||||
// Executing request with model: model1
|
||||
// Result from model1: Hi there! I'm doing well, thank you! How about yourself?
|
||||
// Result does not meet the acceptance criteria, moving to the next model.
|
||||
|
||||
// Executing request with model: model2
|
||||
// Result from model2: Hello! I'm great, thanks for asking. How are you doing today?
|
||||
// Result does not meet the acceptance criteria, moving to the next model.
|
||||
|
||||
// Executing request with model: model3
|
||||
// Result from model3: Hey! I'm fine, thanks. How's your day going so far?
|
||||
// Returning result as it meets the acceptance criteria.
|
||||
|
||||
// Final result: Hey! I'm fine, thanks. How's your day going so far?
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Few-shot prompt optimization filter which takes all examples from kernel arguments and selects first <see cref="TopN"/> examples,
|
||||
/// which are similar to original request.
|
||||
/// </summary>
|
||||
private sealed class FewShotPromptOptimizationFilter(
|
||||
VectorStore vectorStore,
|
||||
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator) : IPromptRenderFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of examples to use which are similar to original request.
|
||||
/// </summary>
|
||||
private const int TopN = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Collection name to use in vector store.
|
||||
/// </summary>
|
||||
private const string CollectionName = "examples";
|
||||
|
||||
public async Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next)
|
||||
{
|
||||
// Get examples and original request from arguments.
|
||||
var examples = context.Arguments["Examples"] as List<string>;
|
||||
var request = context.Arguments["Request"] as string;
|
||||
|
||||
if (examples is { Count: > 0 } && !string.IsNullOrEmpty(request))
|
||||
{
|
||||
var exampleRecords = new List<ExampleRecord>();
|
||||
|
||||
// Generate embedding for each example.
|
||||
var embeddings = (await embeddingGenerator.GenerateAsync(examples));
|
||||
|
||||
// Create vector store record instances with example text and embedding.
|
||||
for (var i = 0; i < examples.Count; i++)
|
||||
{
|
||||
exampleRecords.Add(new ExampleRecord
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Example = examples[i],
|
||||
ExampleEmbedding = embeddings[i].Vector
|
||||
});
|
||||
}
|
||||
|
||||
// Create collection and upsert all vector store records for search.
|
||||
// It's possible to do it only once and re-use the same examples for future requests.
|
||||
var collection = vectorStore.GetCollection<string, ExampleRecord>(CollectionName);
|
||||
await collection.EnsureCollectionExistsAsync(context.CancellationToken);
|
||||
|
||||
await collection.UpsertAsync(exampleRecords, cancellationToken: context.CancellationToken);
|
||||
|
||||
// Generate embedding for original request.
|
||||
var requestEmbedding = await embeddingGenerator.GenerateAsync(request, cancellationToken: context.CancellationToken);
|
||||
|
||||
// Find top N examples which are similar to original request.
|
||||
var topNExamples = (await collection.SearchAsync(requestEmbedding, top: TopN, cancellationToken: context.CancellationToken)
|
||||
.ToListAsync(context.CancellationToken)).Select(l => l.Record).ToList();
|
||||
|
||||
// Override arguments to use only top N examples, which will be sent to LLM.
|
||||
context.Arguments["Examples"] = topNExamples.Select(l => l.Example);
|
||||
}
|
||||
|
||||
// Continue prompt rendering operation.
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example of LLM cascade filter which will invoke a function using multiple LLMs in specific order,
|
||||
/// until the result will meet specified acceptance criteria.
|
||||
/// </summary>
|
||||
private sealed class LLMCascadeFilter(
|
||||
List<string> modelExecutionOrder,
|
||||
Predicate<string> acceptanceCriteria,
|
||||
ITestOutputHelper output) : IFunctionInvocationFilter
|
||||
{
|
||||
public async Task OnFunctionInvocationAsync(Microsoft.SemanticKernel.FunctionInvocationContext context, Func<Microsoft.SemanticKernel.FunctionInvocationContext, Task> next)
|
||||
{
|
||||
// Get registered chat completion services from kernel.
|
||||
var registeredServices = context.Kernel
|
||||
.GetAllServices<IChatCompletionService>()
|
||||
.Select(service => (ModelId: service.GetModelId()!, Service: service));
|
||||
|
||||
// Define order of execution.
|
||||
var order = modelExecutionOrder
|
||||
.Select((value, index) => new { Value = value, Index = index })
|
||||
.ToDictionary(k => k.Value, v => v.Index);
|
||||
|
||||
// Sort services by specified order.
|
||||
var orderedServices = registeredServices.OrderBy(service => order[service.ModelId]);
|
||||
|
||||
// Try to invoke a function with each service and check the result.
|
||||
foreach (var service in orderedServices)
|
||||
{
|
||||
// Define execution settings with model ID.
|
||||
context.Arguments.ExecutionSettings = new Dictionary<string, PromptExecutionSettings>
|
||||
{
|
||||
{ PromptExecutionSettings.DefaultServiceId, new() { ModelId = service.ModelId } }
|
||||
};
|
||||
|
||||
output.WriteLine($"Executing request with model: {service.ModelId}");
|
||||
|
||||
// Invoke a function.
|
||||
await next(context);
|
||||
|
||||
// Get a result.
|
||||
var result = context.Result.ToString()!;
|
||||
|
||||
output.WriteLine($"Result from {service.ModelId}: {result}");
|
||||
|
||||
// Check if result meets specified acceptance criteria.
|
||||
// If yes, stop execution loop, so last result will be returned.
|
||||
if (acceptanceCriteria(result))
|
||||
{
|
||||
output.WriteLine("Returning result as it meets the acceptance criteria.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, proceed with next model.
|
||||
output.WriteLine("Result does not meet the acceptance criteria, moving to the next model.\n");
|
||||
}
|
||||
|
||||
// If LLMs didn't return acceptable result, the last result will be returned.
|
||||
// It's also possible to throw an exception in such cases if needed.
|
||||
// throw new Exception("Models didn't return a result that meets the acceptance criteria").
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock chat completion service for demonstration purposes.
|
||||
/// </summary>
|
||||
private sealed class MockChatCompletionService(string modelId, string mockResult) : IChatCompletionService
|
||||
{
|
||||
public IReadOnlyDictionary<string, object?> Attributes => new Dictionary<string, object?> { { AIServiceExtensions.ModelIdKey, modelId } };
|
||||
|
||||
public Task<IReadOnlyList<ChatMessageContent>> GetChatMessageContentsAsync(
|
||||
ChatHistory chatHistory,
|
||||
PromptExecutionSettings? executionSettings = null,
|
||||
Kernel? kernel = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<ChatMessageContent>>([new ChatMessageContent(AuthorRole.Assistant, mockResult)]);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<StreamingChatMessageContent> GetStreamingChatMessageContentsAsync(
|
||||
ChatHistory chatHistory,
|
||||
PromptExecutionSettings? executionSettings = null,
|
||||
Kernel? kernel = null,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
yield return new StreamingChatMessageContent(AuthorRole.Assistant, mockResult);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ExampleRecord
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string Id { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string Example { get; set; }
|
||||
|
||||
[VectorStoreVector(1536)]
|
||||
public ReadOnlyMemory<float> ExampleEmbedding { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.VectorData;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.ChatCompletion;
|
||||
using Microsoft.SemanticKernel.Connectors.OpenAI;
|
||||
|
||||
namespace Optimization;
|
||||
|
||||
/// <summary>
|
||||
/// Single kernel instance may have multiple imported plugins/functions. It's possible to enable automatic function calling,
|
||||
/// so AI model will decide which functions to call for specific request.
|
||||
/// In case there are a lot of plugins/functions in application, some of them (or all of them) need to be shared with the model.
|
||||
/// This example shows how to use different plugin/function selection strategies, to share with AI only those functions,
|
||||
/// which are related to specific request.
|
||||
/// This technique should decrease token usage, as fewer functions will be shared with AI.
|
||||
/// It also helps to handle the scenario with a general purpose chat experience for a large enterprise,
|
||||
/// where there are so many plugins, that it's impossible to share all of them with AI model in a single request.
|
||||
/// </summary>
|
||||
public sealed class PluginSelectionWithFilters(ITestOutputHelper output) : BaseTest(output)
|
||||
{
|
||||
/// <summary>
|
||||
/// This method shows how to select best functions to share with AI using vector similarity search.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task UsingVectorSearchWithKernelAsync()
|
||||
{
|
||||
// Initialize kernel with chat completion and embedding generation services.
|
||||
// It's possible to combine different models from different AI providers to achieve the lowest token usage.
|
||||
var builder = Kernel
|
||||
.CreateBuilder()
|
||||
.AddOpenAIChatCompletion("gpt-4", TestConfiguration.OpenAI.ApiKey)
|
||||
.AddOpenAIEmbeddingGenerator("text-embedding-3-small", TestConfiguration.OpenAI.ApiKey);
|
||||
|
||||
// Add logging.
|
||||
var logger = this.LoggerFactory.CreateLogger<PluginSelectionWithFilters>();
|
||||
builder.Services.AddSingleton<ILogger>(logger);
|
||||
|
||||
// Add vector store to keep functions and search for the most relevant ones for specific request.
|
||||
builder.Services.AddInMemoryVectorStore();
|
||||
|
||||
// Add helper components defined in this example.
|
||||
builder.Services.AddSingleton<IFunctionProvider, FunctionProvider>();
|
||||
builder.Services.AddSingleton<IFunctionKeyProvider, FunctionKeyProvider>();
|
||||
builder.Services.AddSingleton<IPluginStore, PluginStore>();
|
||||
|
||||
var kernel = builder.Build();
|
||||
|
||||
// Import plugins with different features.
|
||||
kernel.ImportPluginFromType<TimePlugin>();
|
||||
kernel.ImportPluginFromType<WeatherPlugin>();
|
||||
kernel.ImportPluginFromType<EmailPlugin>();
|
||||
kernel.ImportPluginFromType<NewsPlugin>();
|
||||
kernel.ImportPluginFromType<CalendarPlugin>();
|
||||
|
||||
// Get registered plugin store to save information about plugins.
|
||||
var pluginStore = kernel.GetRequiredService<IPluginStore>();
|
||||
|
||||
// Save information about kernel plugins in plugin store.
|
||||
const string CollectionName = "functions";
|
||||
|
||||
await pluginStore.SaveAsync(CollectionName, kernel.Plugins);
|
||||
|
||||
// Enable automatic function calling by default.
|
||||
var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
|
||||
|
||||
// Define kernel arguments with specific request.
|
||||
var kernelArguments = new KernelArguments(executionSettings) { ["Request"] = "Provide latest headlines" };
|
||||
|
||||
// Invoke the request without plugin selection filter first for comparison purposes.
|
||||
Console.WriteLine("Run without filter:");
|
||||
var result = await kernel.InvokePromptAsync("{{$Request}}", kernelArguments);
|
||||
|
||||
Console.WriteLine(result);
|
||||
Console.WriteLine(result.Metadata?["Usage"]?.AsJson()); // All functions were shared with AI. Total tokens: ~250
|
||||
|
||||
// Define plugin selection filter.
|
||||
var filter = new PluginSelectionFilter(
|
||||
functionProvider: kernel.GetRequiredService<IFunctionProvider>(),
|
||||
logger: kernel.GetRequiredService<ILogger>(),
|
||||
collectionName: CollectionName,
|
||||
numberOfBestFunctions: 1);
|
||||
|
||||
// Add filter to kernel.
|
||||
kernel.FunctionInvocationFilters.Add(filter);
|
||||
|
||||
// Invoke the request with plugin selection filter.
|
||||
Console.WriteLine("\nRun with filter:");
|
||||
|
||||
// FunctionChoiceBehavior.Auto() is used here as well as defined above.
|
||||
// In case there will be related functions found for specific request, the FunctionChoiceBehavior will be updated in filter to
|
||||
// FunctionChoiceBehavior.Auto(functions) - this will allow to share only related set of functions with AI.
|
||||
result = await kernel.InvokePromptAsync("{{$Request}}", kernelArguments);
|
||||
|
||||
Console.WriteLine(result);
|
||||
Console.WriteLine(result.Metadata?["Usage"]?.AsJson()); // Just one function was shared with AI. Total tokens: ~150
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsingVectorSearchWithChatCompletionAsync()
|
||||
{
|
||||
// Initialize kernel with chat completion and embedding generation services.
|
||||
// It's possible to combine different models from different AI providers to achieve the lowest token usage.
|
||||
var builder = Kernel
|
||||
.CreateBuilder()
|
||||
.AddOpenAIChatCompletion("gpt-4", TestConfiguration.OpenAI.ApiKey)
|
||||
.AddOpenAIEmbeddingGenerator("text-embedding-3-small", TestConfiguration.OpenAI.ApiKey);
|
||||
|
||||
// Add logging.
|
||||
var logger = this.LoggerFactory.CreateLogger<PluginSelectionWithFilters>();
|
||||
builder.Services.AddSingleton<ILogger>(logger);
|
||||
|
||||
// Add vector store to keep functions and search for the most relevant ones for specific request.
|
||||
builder.Services.AddInMemoryVectorStore();
|
||||
|
||||
// Add helper components defined in this example.
|
||||
builder.Services.AddSingleton<IFunctionProvider, FunctionProvider>();
|
||||
builder.Services.AddSingleton<IFunctionKeyProvider, FunctionKeyProvider>();
|
||||
builder.Services.AddSingleton<IPluginStore, PluginStore>();
|
||||
|
||||
var kernel = builder.Build();
|
||||
|
||||
// Import plugins with different features.
|
||||
kernel.ImportPluginFromType<TimePlugin>();
|
||||
kernel.ImportPluginFromType<WeatherPlugin>();
|
||||
kernel.ImportPluginFromType<EmailPlugin>();
|
||||
kernel.ImportPluginFromType<NewsPlugin>();
|
||||
kernel.ImportPluginFromType<CalendarPlugin>();
|
||||
|
||||
// Get registered plugin store to save information about plugins.
|
||||
var pluginStore = kernel.GetRequiredService<IPluginStore>();
|
||||
|
||||
// Store information about kernel plugins in plugin store.
|
||||
const string CollectionName = "functions";
|
||||
|
||||
await pluginStore.SaveAsync(CollectionName, kernel.Plugins);
|
||||
|
||||
// Enable automatic function calling by default.
|
||||
var executionSettings = new OpenAIPromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
|
||||
|
||||
// Get function provider and find best functions for specified prompt.
|
||||
var functionProvider = kernel.GetRequiredService<IFunctionProvider>();
|
||||
|
||||
const string Prompt = "Provide latest headlines";
|
||||
|
||||
var bestFunctions = await functionProvider.GetBestFunctionsAsync(CollectionName, Prompt, kernel.Plugins, numberOfBestFunctions: 1);
|
||||
|
||||
// If any found, update execution settings to share only selected functions.
|
||||
if (bestFunctions.Count > 0)
|
||||
{
|
||||
bestFunctions.ForEach(function
|
||||
=> logger.LogInformation("Best function found: {PluginName}-{FunctionName}", function.PluginName, function.Name));
|
||||
|
||||
// Share only selected functions with AI.
|
||||
executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(bestFunctions);
|
||||
}
|
||||
|
||||
// Get chat completion service and execute a request.
|
||||
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
|
||||
|
||||
var chatHistory = new ChatHistory();
|
||||
chatHistory.AddUserMessage(Prompt);
|
||||
|
||||
var result = await chatCompletionService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel);
|
||||
|
||||
Console.WriteLine(result);
|
||||
Console.WriteLine(result.Metadata?["Usage"]?.AsJson()); // Just one function was shared with AI. Total tokens: ~150
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter which performs vector similarity search on imported functions in <see cref="Kernel"/>
|
||||
/// to select the best ones to share with AI.
|
||||
/// </summary>
|
||||
private sealed class PluginSelectionFilter(
|
||||
IFunctionProvider functionProvider,
|
||||
ILogger logger,
|
||||
string collectionName,
|
||||
int numberOfBestFunctions) : IFunctionInvocationFilter
|
||||
{
|
||||
public async Task OnFunctionInvocationAsync(Microsoft.SemanticKernel.FunctionInvocationContext context, Func<Microsoft.SemanticKernel.FunctionInvocationContext, Task> next)
|
||||
{
|
||||
var request = GetRequestArgument(context.Arguments);
|
||||
|
||||
// Execute plugin selection logic for "InvokePrompt" function only, as main entry point.
|
||||
if (context.Function.Name.Contains(nameof(KernelExtensions.InvokePromptAsync)) && !string.IsNullOrWhiteSpace(request))
|
||||
{
|
||||
// Get imported plugins in kernel.
|
||||
var plugins = context.Kernel.Plugins;
|
||||
|
||||
// Find best functions for original request.
|
||||
var bestFunctions = await functionProvider.GetBestFunctionsAsync(collectionName, request, plugins, numberOfBestFunctions);
|
||||
|
||||
// If any found, update execution settings and execute the request.
|
||||
if (bestFunctions.Count > 0)
|
||||
{
|
||||
bestFunctions.ForEach(function
|
||||
=> logger.LogInformation("Best function found: {PluginName}-{FunctionName}", function.PluginName, function.Name));
|
||||
|
||||
var updatedExecutionSettings = GetExecutionSettings(context.Arguments, bestFunctions);
|
||||
|
||||
if (updatedExecutionSettings is not null)
|
||||
{
|
||||
// Update execution settings.
|
||||
context.Arguments.ExecutionSettings = updatedExecutionSettings;
|
||||
|
||||
// Execute the request.
|
||||
await next(context);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, execute a request with default logic, where all plugins will be shared.
|
||||
await next(context);
|
||||
}
|
||||
|
||||
private static Dictionary<string, PromptExecutionSettings>? GetExecutionSettings(KernelArguments arguments, List<KernelFunction> functions)
|
||||
{
|
||||
var promptExecutionSettings = arguments.ExecutionSettings?[PromptExecutionSettings.DefaultServiceId];
|
||||
|
||||
if (promptExecutionSettings is not null and OpenAIPromptExecutionSettings openAIPromptExecutionSettings)
|
||||
{
|
||||
// Share only selected functions with AI.
|
||||
openAIPromptExecutionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions);
|
||||
|
||||
return new() { [PromptExecutionSettings.DefaultServiceId] = openAIPromptExecutionSettings };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetRequestArgument(KernelArguments arguments)
|
||||
=> arguments.TryGetValue("Request", out var requestObj) && requestObj is string request ? request : null;
|
||||
}
|
||||
|
||||
#region Helper components
|
||||
|
||||
/// <summary>
|
||||
/// Helper function key provider.
|
||||
/// </summary>
|
||||
public interface IFunctionKeyProvider
|
||||
{
|
||||
string GetFunctionKey(KernelFunction kernelFunction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function provider to get best functions for specific request.
|
||||
/// </summary>
|
||||
public interface IFunctionProvider
|
||||
{
|
||||
Task<List<KernelFunction>> GetBestFunctionsAsync(
|
||||
string collectionName,
|
||||
string request,
|
||||
KernelPluginCollection plugins,
|
||||
int numberOfBestFunctions,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper plugin store to save information about imported plugins in vector database.
|
||||
/// </summary>
|
||||
public interface IPluginStore
|
||||
{
|
||||
Task SaveAsync(string collectionName, KernelPluginCollection plugins, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class FunctionKeyProvider : IFunctionKeyProvider
|
||||
{
|
||||
public string GetFunctionKey(KernelFunction kernelFunction)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(kernelFunction.PluginName) ?
|
||||
$"{kernelFunction.PluginName}-{kernelFunction.Name}" :
|
||||
kernelFunction.Name;
|
||||
}
|
||||
}
|
||||
|
||||
public class FunctionProvider(
|
||||
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
|
||||
VectorStore vectorStore,
|
||||
IFunctionKeyProvider functionKeyProvider) : IFunctionProvider
|
||||
{
|
||||
public async Task<List<KernelFunction>> GetBestFunctionsAsync(
|
||||
string collectionName,
|
||||
string request,
|
||||
KernelPluginCollection plugins,
|
||||
int numberOfBestFunctions,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Generate embedding for original request.
|
||||
var requestEmbedding = await embeddingGenerator.GenerateAsync(request, cancellationToken: cancellationToken);
|
||||
|
||||
var collection = vectorStore.GetCollection<string, FunctionRecord>(collectionName);
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
// Find best functions to call for original request.
|
||||
var recordKeys = (await collection.SearchAsync(requestEmbedding, top: numberOfBestFunctions, cancellationToken: cancellationToken)
|
||||
.ToListAsync(cancellationToken)).Select(l => l.Record.Id);
|
||||
|
||||
return plugins
|
||||
.SelectMany(plugin => plugin)
|
||||
.Where(function => recordKeys.Contains(functionKeyProvider.GetFunctionKey(function)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class PluginStore(
|
||||
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
|
||||
VectorStore vectorStore,
|
||||
IFunctionKeyProvider functionKeyProvider) : IPluginStore
|
||||
{
|
||||
public async Task SaveAsync(string collectionName, KernelPluginCollection plugins, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Collect data about imported functions in kernel.
|
||||
var functionRecords = new List<FunctionRecord>();
|
||||
var functionsData = GetFunctionsData(plugins);
|
||||
|
||||
// Generate embedding for each function.
|
||||
var embeddings = await embeddingGenerator
|
||||
.GenerateAsync(functionsData.Select(l => l.TextToVectorize).ToArray(), cancellationToken: cancellationToken);
|
||||
|
||||
// Create vector store record instances with function information and embedding.
|
||||
for (var i = 0; i < functionsData.Count; i++)
|
||||
{
|
||||
var (function, functionInfo) = functionsData[i];
|
||||
|
||||
functionRecords.Add(new FunctionRecord
|
||||
{
|
||||
Id = functionKeyProvider.GetFunctionKey(function),
|
||||
FunctionInfo = functionInfo,
|
||||
FunctionInfoEmbedding = embeddings[i].Vector
|
||||
});
|
||||
}
|
||||
|
||||
// Create collection and upsert all vector store records for search.
|
||||
// It's possible to do it only once and re-use the same functions for future requests.
|
||||
var collection = vectorStore.GetCollection<string, FunctionRecord>(collectionName);
|
||||
await collection.EnsureCollectionExistsAsync(cancellationToken);
|
||||
|
||||
await collection.UpsertAsync(functionRecords, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private static List<(KernelFunction Function, string TextToVectorize)> GetFunctionsData(KernelPluginCollection plugins)
|
||||
=> plugins
|
||||
.SelectMany(plugin => plugin)
|
||||
.Select(function => (function, $"Plugin name: {function.PluginName}. Function name: {function.Name}. Description: {function.Description}"))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample Plugins
|
||||
|
||||
private sealed class TimePlugin
|
||||
{
|
||||
[KernelFunction, Description("Provides the current date and time.")]
|
||||
public string GetCurrentTime() => DateTime.Now.ToString("R");
|
||||
}
|
||||
|
||||
private sealed class WeatherPlugin
|
||||
{
|
||||
[KernelFunction, Description("Provides weather information for various cities.")]
|
||||
public string GetWeather(string cityName) => cityName switch
|
||||
{
|
||||
"Boston" => "61 and rainy",
|
||||
"London" => "55 and cloudy",
|
||||
"Miami" => "80 and sunny",
|
||||
"Paris" => "60 and rainy",
|
||||
"Tokyo" => "50 and sunny",
|
||||
"Sydney" => "75 and sunny",
|
||||
"Tel Aviv" => "80 and sunny",
|
||||
_ => "No information",
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class EmailPlugin(ILogger logger)
|
||||
{
|
||||
[KernelFunction, Description("Sends email to recipient with subject and body.")]
|
||||
public void SendEmail(string from, string to, string subject, string body)
|
||||
{
|
||||
logger.LogInformation("Email has been sent successfully.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NewsPlugin
|
||||
{
|
||||
[KernelFunction, Description("Provides the latest news headlines.")]
|
||||
public List<string> GetLatestHeadlines() =>
|
||||
[
|
||||
"Tourism Industry Sees Record Growth",
|
||||
"Tech Company Releases New Product",
|
||||
"Sports Team Wins Championship",
|
||||
"New Study Reveals Health Benefits of Walking"
|
||||
];
|
||||
}
|
||||
|
||||
private sealed class CalendarPlugin
|
||||
{
|
||||
[KernelFunction, Description("Provides a list of upcoming events.")]
|
||||
public List<string> GetUpcomingEvents() =>
|
||||
[
|
||||
"Meeting with Bob on June 22",
|
||||
"Project deadline on June 30",
|
||||
"Dentist appointment on July 5",
|
||||
"Vacation starts on July 12"
|
||||
];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Vector Store Record
|
||||
|
||||
private sealed class FunctionRecord
|
||||
{
|
||||
[VectorStoreKey]
|
||||
public string Id { get; set; }
|
||||
|
||||
[VectorStoreData]
|
||||
public string FunctionInfo { get; set; }
|
||||
|
||||
[VectorStoreVector(1536)]
|
||||
public ReadOnlyMemory<float> FunctionInfoEmbedding { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user