Back to Semantic Kernel

These are optional elements. Feel free to remove any of them.

docs/decisions/0072-context-based-function-selection.md

latest21.5 KB
Original Source

Context and Problem Statement

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.

Decision Drivers

  • It should be possible to advertise functions dynamically based on the context of the conversation.
  • It should seamlessly integrate with SK and M.E.AI AI connectors and SK agents.
  • It should have access to context and functions without the need for complex plumbing.

Out of Scope

  • A particular implementation of the function selection algorithm whether it's RAG or any other.

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:

csharp
// 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:

  • Can be used with all AI components, including SK chat completion services, SK agents, and M.E.AI chat clients.

Cons:

  • Complex integration of all the parts (vectorization of functions, function search, advertisement of functions) of the solution together.
  • Doesn't support function choice behavior configured in prompt templates.

Option 1A: Function Invocation Filter

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:

csharp
// 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:

  • Relies on usage of the InvokePromptAsync function, making it unusable for all scenarios except those where the kernel.InvokePromptAsync function is used.
  • Doesn't support function choice behavior configured in prompt templates.

Option 2: M.E.AI ChatClient Decorator

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:

csharp
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:

  • Works seamlessly with SK chat completion services and M.E.AI chat clients.
  • Easy wiring aligned with the initialization pattern adopted by M.E.AI.
  • No need for a new abstraction.
  • Easy to add new function selectors and chain them together.

Cons:

  • Works with chat completion agents only and does not work with SK agents that don't use the chat completion service.
  • Doesn't support function choice behavior configured in prompt templates.

Option 3: Function Advertisement Filter

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:

csharp
// 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:

  • Familiar concept for SK users.
  • Works with chat completion services.
  • Works with both chat completion and non-chat completion SK agents, provided they can provide context to the filter.

Cons:

  • New abstraction is required.
  • Public API surface of Kernel needs to be extended.
  • All AI components: SK agents, chat completion services, and M.E.AI chat clients adapters need to be updated to invoke the filter.

Option 4: FunctionChoiceBehavior Callback

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.

csharp
// 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:

  • Doesn't support function choice behavior configured in prompt templates.
  • Can only be used by components that use FunctionChoiceBehavior: SK chat completion services and chat completion agents.

Options Applicability

This table summarizes the applicability of the options described above to the different components of the Semantic Kernel and M.E.AI:

OptionScopeOpenAI & AzureAI AgentsBedrock AgentChat Completion AgentSK Chat Completion ServiceM.E.AI Chat Client
1. External Vectorization & SearchOperationYes<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 FilterOperationNo<sup>5</sup>No<sup>5</sup>No<sup>5</sup>No<sup>5</sup>No
2. M.E.AI ChatClient DecoratorOperationNoNoYes<sup>6</sup>Yes<sup>6</sup>Yes
3. Function Advertisement FilterOp & ReqYesNo<sup>3</sup>YesYesYes<sup>7</sup>
4. FunctionChoiceBehavior CallbackOp & ReqNo<sup>8,9</sup>No<sup>8</sup>YesYesYes<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:

  • For agents that maintain threads on the server side, getting the full context is impossible without first loading the entire thread from the server. This is not efficient and might not be supported by agents. However, the messages passed during agent invocation might be enough and can be used as context for function selection.

Integration with Agent Memory

The agent's memory model is represented by the following classes:

csharp
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:

csharp
// 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.

Decision Outcome

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.