Edit

Share via


Access data in AI functions

When you create AI functions, you might need to access contextual data beyond the parameters provided by the AI model. The Microsoft.Extensions.AI library provides several mechanisms to pass data to function delegates.

AIFunction class

The AIFunction type represents a function that can be described to an AI service and invoked. You can create AIFunction objects by calling one of the AIFunctionFactory.Create overloads. But AIFunction is also a base class, and you can derive from it and implement your own AI function type. DelegatingAIFunction provides an easy way to wrap an existing AIFunction and layer in additional functionality, including capturing additional data to be used.

Pass data

You can associate data with the function at the time it's created, either via closure or via AdditionalProperties. If you're creating your own function, you can populate AdditionalProperties however you want. If you use AIFunctionFactory to create the function, you can populate data using AIFunctionFactoryOptions.AdditionalProperties.

You can also capture any references to data as part of the delegate provided to AIFunctionFactory. That is, you can bake in whatever you want to reference as part of the AIFunction itself.

Access data in function delegates

You might call your AIFunction directly, or you might call it indirectly by using FunctionInvokingChatClient. The following sections describe how to access argument data using either approach.

Manual function invocation

If you manually invoke an AIFunction by calling AIFunction.InvokeAsync(AIFunctionArguments, CancellationToken), you pass in AIFunctionArguments. The AIFunctionArguments type includes:

If you want to access either the AIFunctionArguments or the IServiceProvider from within your AIFunctionFactory.Create delegate, create a parameter typed as IServiceProvider or AIFunctionArguments. That parameter will be bound to the relevant data from the AIFunctionArguments passed to AIFunction.InvokeAsync().

The following code shows an example:

Delegate getWeatherDelegate = (AIFunctionArguments args) =>
{
    // Access named parameters from the arguments dictionary.
    string? location = args.TryGetValue("location", out object? loc) ? loc.ToString() : "Unknown";
    string? units = args.TryGetValue("units", out object? u) ? u.ToString() : "celsius";

    return $"Weather in {location}: 35°{units}";
};

// Create the AIFunction.
AIFunction getWeather = AIFunctionFactory.Create(getWeatherDelegate);

// Call the function manually.
var result = await getWeather.InvokeAsync(new AIFunctionArguments
{
    { "location", "Seattle" },
    { "units", "F" }
});
Console.WriteLine($"Function result: {result}");

CancellationToken is also special-cased: if the AIFunctionFactory.Create delegate or lambda has a CancellationToken parameter, it will be bound to the CancellationToken that was passed to AIFunction.InvokeAsync().

Invocation through FunctionInvokingChatClient

FunctionInvokingChatClient publishes state about the current invocation to FunctionInvokingChatClient.CurrentContext, including not only the arguments, but all of the input ChatMessage objects, the ChatOptions, and details on which function is being invoked (out of how many). You can add any data you want into ChatOptions.AdditionalProperties and extract that inside of your AIFunction from FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties.

The following code shows an example:

FunctionInvokingChatClient client = new FunctionInvokingChatClient(
    new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey))
    .GetChatClient(model).AsIChatClient());

AIFunction getWeather = AIFunctionFactory.Create(() =>
    {
        // Access named parameters from the arguments dictionary.
        AdditionalPropertiesDictionary props =
            FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties;

        string location = props["location"].ToString();
        string units = props["units"].ToString();

        return $"Weather in {location}: 35°{units}";
    });

var chatOptions = new ChatOptions
{
    Tools = [getWeather],
    AdditionalProperties = new AdditionalPropertiesDictionary {
        ["location"] = "Seattle",
        ["units"] = "F"
    },
};

List<ChatMessage> chatHistory = [
    new(ChatRole.System, "You're a helpful weather assistant.")
];
chatHistory.Add(new ChatMessage(ChatRole.User, "What's the weather like?"));

ChatResponse response = await client.GetResponseAsync(chatHistory, chatOptions);
Console.WriteLine($"Response: {response.Text}");

Dependency injection

If you use FunctionInvokingChatClient to invoke functions automatically, that client configures an AIFunctionArguments object that it passes into the AIFunction. Because AIFunctionArguments includes the IServiceProvider that the FunctionInvokingChatClient was itself provided with, if you construct your client using standard DI means, that IServiceProvider is passed all the way into your AIFunction. At that point, you can query it for anything you want from DI.

Advanced techniques

If you want more fine-grained control over how parameters are bound, you can use AIFunctionFactoryOptions.ConfigureParameterBinding, which puts you in control over how each parameter is populated. For example, the MCP C# SDK uses this technique to automatically bind parameters from DI.

If you use the AIFunctionFactory.Create(MethodInfo, Func<AIFunctionArguments,Object>, AIFunctionFactoryOptions) overload, you can also run your own arbitrary logic when you create the target object that the instance method will be called on, each time. And you can do whatever you want to configure that instance.