docs/decisions/0072-context-based-function-selection.md
Currently, Semantic Kernel (SK) advertises all functions to the AI model, regardless of their source, whether they are from all registered plugins or provided directly when configuring function choice behavior. This approach works perfectly for most scenarios where there are not too many functions, and the AI model can easily choose the right one.
However, when there are many functions available, AI models may struggle to select the appropriate function, leading to confusion and suboptimal performance. This can result in the AI model calling functions that are not relevant to the current context or conversation, potentially causing the entire scenario to fail.
This ADR consider different options to provide context-based function selection and advertisement mechanism to such components as SK agents, chat completion services, and M.E.AI chat clients.
This option is demonstrated in the following sample: PluginSelectionWithFilters.UsingVectorSearchWithChatCompletionAsync
which uses the PluginStore class to vectorize kernel function and FunctionProvider to find functions relevant to the prompt:
// Register services
IKernelBuilder builder = Kernel.CreateBuilder();
builder.Services.AddInMemoryVectorStore();
builder.Services.AddSingleton<IFunctionProvider, FunctionProvider>();
builder.Services.AddSingleton<IPluginStore, PluginStore>();
// Register plugins
Kernel kernel = builder.Build();
kernel.ImportPluginFromType<TimePlugin>();
kernel.ImportPluginFromType<WeatherPlugin>();
// Vectorize all functions in the kernel
IPluginStore pluginStore = kernel.GetRequiredService<IPluginStore>();
await pluginStore.SaveAsync(collectionName: "functions", kernel.Plugins);
const string Prompt = "Provide latest headlines";
// Do RAG to find the relevant function for the prompt
IFunctionProvider functionProvider = kernel.GetRequiredService<IFunctionProvider>();
KernelFunction[] relevantFunctions = await functionProvider.GetRelevantFunctionsAsync(collectionName: "functions", Prompt, kernel.Plugins, numberOfFunctions: 1);
// Set the relevant functions to be advertised to the AI model
executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(relevantFunctions);
// Do the chat completion
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage(Prompt);
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var result = await chatCompletionService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel);
Console.WriteLine(result);
It's invoked per operation rather than per AI model request; one operation call may result in multiple AI model requests in cases where the AI model performs function calling.
Pros:
Cons:
This option is demonstrated in the following sample: PluginSelectionWithFilters.UsingVectorSearchWithKernelAsync.
It's identical to Option 1 for vectorization part and slightly deviates for the function selection part, which is implemented as a function invocation filter that intercepts calls to the InvokePromptAsync function,
identifies the relevant functions to the prompt, and sets them to be advertised to the AI model via execution settings:
// Register services
IKernelBuilder builder = Kernel.CreateBuilder();
builder.Services.AddInMemoryVectorStore();
builder.Services.AddSingleton<IFunctionProvider, FunctionProvider>();
builder.Services.AddSingleton<IPluginStore, PluginStore>();
// Register plugins
Kernel kernel = builder.Build();
kernel.ImportPluginFromType<TimePlugin>();
kernel.ImportPluginFromType<WeatherPlugin>();
// Vectorize all functions in the kernel
IPluginStore pluginStore = kernel.GetRequiredService<IPluginStore>();
await pluginStore.SaveAsync(collectionName: "functions", kernel.Plugins);
// Register function invocation filter
IFunctionProvider functionProvider = kernel.GetRequiredService<IFunctionProvider>();
kernel.FunctionInvocationFilters.Add(new PluginSelectionFilter(functionProvider: functionProvider, collectionName: "functions"));
// Do the chat completion
KernelArguments kernelArguments = new(executionSettings) { ["Request"] = "Provide latest headlines" };
await kernel.InvokePromptAsync("{{$Request}}", kernelArguments);
// Function invocation filter
class PluginSelectionFilter(IFunctionProvider functionProvider, string collectionName)
{
public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)
{
string request = context.Arguments["Request"];
if (context.Function.Name.Contains(nameof(KernelExtensions.InvokePromptAsync)) && !string.IsNullOrWhiteSpace(request))
{
var functions = await functionProvider.GetRelevantFunctionsAsync(collectionName, request, plugins, numberOfFunctions);
context.Arguments.ExecutionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(functions);
}
await next(context);
}
}
It's invoked per operation rather than per AI model request; one operation call may result in multiple AI model requests in cases where the AI model performs function calling.
Pros:
Cons:
InvokePromptAsync function, making it unusable for all scenarios except those where the kernel.InvokePromptAsync function is used.This option presumes having an implementation of the M.E.AI.IChatClient interface, such as the ContextFunctionSelectorChatClient class, which will vectorize all functions available in the
ChatOptions parameter of either GetResponseAsync or GetResponseStreamAsync methods. It will then search for functions relevant to the context represented by the list of chat messages passed to one of these methods:
public class ContextFunctionSelectorChatClient : DelegatingChatClient
{
protected ContextFunctionSelectorChatClient(IChatClient innerClient) : base(innerClient)
{
}
public override async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null)
{
ChatOptions? targetOptions = options;
if (options?.Tools?.Any() ?? false)
{
targetOptions = options.Clone();
AITool[] functionsToAdvertise = await this.GetRelevantFunctions(options, messages).ConfigureAwait(false);
targetOptions.Tools = functionsToAdvertise;
}
return await base.GetResponseAsync(messages, targetOptions, ct).ConfigureAwait(false);
}
private async Task<AITool[]> GetRelevantFunctions(ChatOptions options, IEnumerable<ChatMessage> messages)
{
// 1. Vectorize all the functions form the `options.Tool` collection, if not already vectorized.
// 2. Vectorize the context represented by the `messages` collection.
// 3. Search for and return the most relevant functions using the vectorized context.
}
public override IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null)
{
// similar to GetResponseAsync, but for streaming
}
}
// Usage with M.E.AI chat client
ChatClient chatClient = new("model", "api-key");
IChatClient client = chatClient.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation()
.UseContextFunctionSelector()
.Build();
// Usage with SK chat completion service
IChatCompletionService chatCompletionService = new OpenAIChatCompletionService("<model-id>", "<api-key>");
IChatClient client = chatCompletionService.AsChatClient()
.AsBuilder()
.UseContextFunctionSelector()
.Build();
The decorator is invoked per operation rather than per AI model request; one operation call may result in multiple AI model requests in cases where the AI model performs function calling.
Pros:
Cons:
This option assumes having a new filter type that will be used to select the functions to be advertised to the AI model based on the context of the conversation:
// Register plugins
Kernel kernel = new Kernel();
kernel.ImportPluginFromType<TimePlugin>();
kernel.ImportPluginFromType<WeatherPlugin>();
// Register function advertisement filter
kernel.FunctionAdvertisementFilters.Add(new ContextFunctionSelectorFilter());
// Do the chat completion
await kernel.InvokePromptAsync("Provide latest headlines");
// Function invocation filter
class ContextFunctionSelectorFilter()
{
public async Task OnFunctionsAdvertisementAsync(FunctionAdvertisementContext context, Func<FunctionAdvertisementContext, Task> next);
{
// 1. Vectorize all the functions form the `context.Functions` collection, if not already vectorized.
// 2. Vectorize the context represented by the `context.ChatHistory` collection.
// 3. Search for and assign the most relevant functions using the vectorized context to `context.Functions` property.
}
}
The filter can be invoked per operation and per AI model request as well; one operation call may result in multiple AI model requests in cases where the AI model performs function calling.
Pros:
Cons:
This options presume extending the existing AutoFunctionChoiceBehavior, RequiredFunctionChoiceBehavior and NoneFunctionChoiceBehavior classes with a new constructor that
takes a function selector as a parameter and uses it to select the functions based on the context to be advertised to the AI model.
// Register services
IKernelBuilder builder = Kernel.CreateBuilder();
builder.Services.AddInMemoryVectorStore();
builder.Services.AddSingleton<IFunctionProvider, FunctionProvider>();
builder.Services.AddSingleton<IPluginStore, PluginStore>();
// Register plugins
Kernel kernel = builder.Build();
kernel.ImportPluginFromType<TimePlugin>();
kernel.ImportPluginFromType<WeatherPlugin>();
// Set the relevant functions to be advertised to the AI model
executionSettings.FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(FunctionSelector);
// Do the chat completion
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("Provide latest headlines");
var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();
var result = await chatCompletionService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel);
Console.WriteLine(result);
async Task<IList<KernelFunction>> FunctionSelector(FunctionChoiceBehaviorConfigurationContext context)
{
// Vectorize all the functions form the `context.Functions` collection
IPluginStore pluginStore = context.Kernel.GetRequiredService<IPluginStore>();
await pluginStore.SaveAsync(collectionName: "functions", context.Kernel.Plugins);
// Search for the most relevant functions using the vectorized context
IFunctionProvider functionProvider = kernel.GetRequiredService<IFunctionProvider>();
IList<KernelFunction> relevantFunctions = await functionProvider.GetRelevantFunctionsAsync(collectionName: "functions", context.ChatHistory, kernel.Plugins, numberOfFunctions: 1);
return relevantFunctions;
}
The filter can be invoked per operation and per AI model request as well; one operation call may result in multiple AI model requests in cases where the AI model performs function calling.
Pros:
Cons:
FunctionChoiceBehavior: SK chat completion services and chat completion agents.This table summarizes the applicability of the options described above to the different components of the Semantic Kernel and M.E.AI:
| Option | Scope | OpenAI & AzureAI Agents | Bedrock Agent | Chat Completion Agent | SK Chat Completion Service | M.E.AI Chat Client |
|---|---|---|---|---|---|---|
| 1. External Vectorization & Search | Operation | Yes<sup>1,2</sup> | Yes<sup>1,3</sup> | Yes<sup>1,2or4</sup> | Yes<sup>1,2or4</sup> | Yes<sup>1</sup> |
| 1A. Function Invocation Filter | Operation | No<sup>5</sup> | No<sup>5</sup> | No<sup>5</sup> | No<sup>5</sup> | No |
| 2. M.E.AI ChatClient Decorator | Operation | No | No | Yes<sup>6</sup> | Yes<sup>6</sup> | Yes |
| 3. Function Advertisement Filter | Op & Req | Yes | No<sup>3</sup> | Yes | Yes | Yes<sup>7</sup> |
| 4. FunctionChoiceBehavior Callback | Op & Req | No<sup>8,9</sup> | No<sup>8</sup> | Yes | Yes | Yes<sup>7</sup> |
<sup>1</sup> Requires manual orchestration of function vectorization, function search, function advertisement, and agent/chat completion service invocation. This solution is available today but requires complex plumbing to integrate all the components together.
<sup>2</sup> To supply relevant functions for each invocation of the agent or chat completion service, all plugins registered in the kernel need to be removed first.
Then, a new plugin with relevant functions needs to be registered on the kernel using kernel.Plugins.AddFromFunctions("dynamicPlugin", [relevantFunctions]) for each invocation.
Alternatively, instead of removing the plugins, a new kernel can be created; however, a new instance of the agent needs to be created as well.
The fact that the relevant functions will no longer be part of their original plugins and will be repackaged into a new plugin may introduce some problems, such as function name collisions
and loss of the additional context provided by the original plugin.
<sup>3</sup> To supply relevant functions for each agent invocation, a new instance of agent needs to be created per invocation because the agent uses functions defined
in the AgentDefinition.Tools collection, which is used only at the time of agent initialization.
<sup>4</sup> To supply relevant functions for each invocation of the agent or chat completion service, the orchestration functionality needs to provide them via the functions parameter of a new instance of one
of the *FunctionChoiceBehavior class and assign that instance to the executionSettings.FunctionChoiceBehavior property: executionSettings.FunctionChoiceBehavior = new AutoFunctionChoiceBehavior(functions).
<sup>5</sup> Uses a function invocation filter to perform function selection and advertisement. The filter searches for the relevant functions and sets them to be advertised to the AI
model via execution settings only if triggered by the invocation of the kernel.InvokePromptAsync function. It does nothing if triggered by other function invocations, making this option unusable in
all cases except those where the kernel.InvokePromptAsync function is used.
<sup>6</sup> M.E.AI Chat Client needs to be adapted to the IChatCompletionService interface using the ChatClientChatCompletionService SK adapter.
<sup>7</sup> M.E.AI Chat Client needs to be decorated (the decorator is available in SK) so the decorator can access the function advertisement filter/function choice behavior to get the relevant functions.
<sup>8</sup> Neither OpenAI, AzureAI, nor Bedrock agents use function choice behavior for function advertisement. Extending any of the agents to use function choice behavior does not make any sense because they do not support any other function choice behavior except auto function choice behavior.
<sup>9</sup> Extending either OpenAI or AzureAI agents to obtain relevant functions from the provided function choice behavior will make the development experience confusing. Currently, functions can be sourced to agents from three places: agent definition, agent constructor, and kernel. Adding a fourth source will make it even more confusing.
Notes:
The agent's memory model is represented by the following classes:
public sealed class AIContextPart
{
public string? Instructions { get; set; }
public List<AIFunction> AIFunctions { get; set; } = new();
}
public abstract class AIContextBehavior
{
public virtual Task OnThreadCreatedAsync(string? threadId, CancellationToken ct) {...}
public virtual Task OnNewMessageAsync(string? threadId, ChatMessage newMessage, CancellationToken ct) {...}
public virtual Task OnThreadDeleteAsync(string? threadId, CancellationToken ct) {...}
public abstract Task<AIContextPart> OnModelInvokeAsync(ICollection<ChatMessage> newMessages, CancellationToken ct);
public virtual Task OnSuspendAsync(string? threadId, CancellationToken ct) {...}
public virtual Task OnResumeAsync(string? threadId, CancellationToken ct) {...}
}
public sealed class AIContextBehaviorsManager
{
public AIContextBehaviorsManager(IEnumerable<AIContextBehavior> aiContextBehaviors) {...}
public void Add(AIContextBehavior aiContextBehavior) {...}
public void AddFromServiceProvider(IServiceProvider serviceProvider) {...}
public async Task OnThreadCreatedAsync(string? threadId, CancellationToken ct) {...}
public async Task OnThreadDeleteAsync(string threadId, CancellationToken ct) {...}
public async Task OnNewMessageAsync(string? threadId, ChatMessage newMessage, CancellationToken ct) {...}
public async Task<AIContextPart> OnModelInvokeAsync(ICollection<ChatMessage> newMessages, CancellationToken ct) {...}
public async Task OnSuspendAsync(string? threadId, CancellationToken ct) {...}
public async Task OnResumeAsync(string? threadId, CancellationToken ct) {...}
}
An example demonstrating the model's usage:
// Create a kernel and register plugins
Kernel kernel = this.CreateKernelWithChatCompletion();
kernel.Plugins.AddFromType<FinancePlugin>();
// Create Mem0Behavior
Mem0Behavior mem0Behavior = new(...);
await mem0Behavior.ClearStoredMemoriesAsync();
// Create a chat completion agent
ChatCompletionAgent agent = new(kernel, ...);
// Create agent thread and add Mem0Behavior to it
ChatHistoryAgentThread agentThread = new();
agentThread.AIContextBehaviors.Add(mem0Behavior);
// Prompt the agent
string userMessage = "Please retrieve my company report";
ChatMessageContent message = await agent.InvokeAsync(userMessage, agentThread).FirstAsync();
There might be cases when there is a need to reuse an existing AI context behavior to narrow down the list of functions for non-agent scenarios, such as a chat completion service or chat client. In these cases, either the AI context behavior can be adapted to the model required by one of the options described above, or preferably the same components for vectorization and semantic search can be used to implement both the AI context behavior and the model required by one of the options described above.
During the ADR review meeting, it was decided to prioritize context-based function selection for agents by implementing an AIContextBehavior, which would perform RAG on the agent's functions. Later, upon request, the same functionality can be extended to chat completion services and M.E.AI chat clients using option 2: the M.E.AI ChatClient Decorator.