Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable AIFunctions to be passed an IServiceProvider, Alternate 3 #6146

Closed
wants to merge 1 commit into from

Conversation

stephentoub
Copy link
Member

@stephentoub stephentoub commented Mar 18, 2025

Alternate to #6141. Change AIFunction.InvokeAsync to directly take IServiceProvider as an argument, and special-case IServiceProvider for schema generation. Basically IServiceProvider is a first-class citizen in the AIFunction world.

Microsoft Reviewers: Open in CodeFlow

/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>The result of the function's execution.</returns>
public Task<object?> InvokeAsync(
IEnumerable<KeyValuePair<string, object?>>? arguments = null,
IServiceProvider? services = null,
Copy link
Member

@halter73 halter73 Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of all the alternative proposals, I think I like this one the best.

This makes the IServiceProvider way more discoverable compared to putting it in the IEnumerable<KeyValuePair<string, object?>>? param or the FunctionInvokingChatClient.CurrentContext async local.

I'd be more okay with putting the IServiceProvider in the arguments parameter if it was typed as a AIFunctionArguments rather than IEnumerable<KeyValuePair<string, object?>>?, since then the IServiceProvider would be a little more discoverable, but still I think the IServiceProvider should be a first-class concept similar to the CancellationToken. It's very different than all the other non-CancellationToken arguments that are deserialized from the tool invocation.

I also like that it doesn't introduce a FromServicesAttribute in the Microsoft.Extensions.AI namespace. I get the other proposals don't require that, but I think it's a good decision. I think wanting to use attributes to identify service parameters is a common enough scenario to move into Microst.Extensions.DependencyInjection.Abstractions alongside the FromKeyedServicesAttribute.

However, it might be interesting to have an overload of AIFunction.Create that took an IServiceProvider and used IServiceProviderIsService to determine which parameters should come from the service provider. Than I think it would be reasonable to respect the existing FromKeyedServicesAttribute without adding a new FromServicesAttribute which would no longer be necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, it might be interesting to have an overload of AIFunction.Create that took an IServiceProvider and used IServiceProviderIsService to determine which parameters should come from the service provider.

It still seems really strange to me that we'd base decisions for the returned AIFunction on an IServiceProvider instance that is possibly not the same IServiceProvider instance that's used later for invocation.

I think wanting to use attributes to identify service parameters is a common enough scenario to move into Microst.Extensions.DependencyInjection.Abstractions alongside the FromKeyedServicesAttribute.

We'd need to introduce a new attribute, presumably, in order to fix the namespace. And ASP.NET would then need to support both (the interface it uses wouldn't be implementable on the M.E.DI.Abstractions one).

Seems like for now the best answer is to just special-case IServerProvider in the signature of AIFunctionFactory.Create methods, and we could later support the convenience attribute mechanism.

That is separate from how the IServiceProvider finds its way into the AIFunction invocation. Choices are basically:

  1. Introduce an AIFunctionArguments type, leave InvokeAsync signature as it is, AIFunction implementations can type test for AIFunctionArguments, callers of InvokeAsync that have an IServiceProvider instantiate an AIFunctionArguments.
  2. Introduce an AIFunctionArguments type, change InvokeAsync's arguments parameter to be strongly typed as AIFunctionArguments.
  3. Change InvokeAsync's signature to take the arguments, the service provider, and the cancellation token, all as peers.

(1) is the only one we could do if we were doing this in a month. As we're doing it now, we can choose to take the break for (2) or (3).

(1) is hacky and relies on callers knowing they should instantiate the special type and implementers knowing they should check for it.

(2) makes it clear exactly what type should be used, and doesn't promote IServiceProvider to the same importance conceptually as the nominal arguments or cancellation token, while still allowing for future expansion should there be other state we want to allow to flow in. But it also forces all inputs to be wrapped in this special collection, which has some overhead and inconvenience (though most code won't directly invoke AIFunctions).

(3) doesn't require such wrapping and makes it obvious that an IServiceProvider can be passed in, but effectively makes DI primary in the API and doesn't afford us the ability to pass in more state in the future (we'd end up falling back to (1) if we need to be able to do that).

I'm leaning towards (2). @halter73 is leaning towards (3). @eiriktsarpalis? @SteveSandersonMS?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still seems really strange to me that we'd base decisions for the returned AIFunction on an IServiceProvider instance that is possibly not the same IServiceProvider instance that's used later for invocation.It still seems really strange to me that we'd base decisions for the returned AIFunction on an IServiceProvider instance that is possibly not the same IServiceProvider instance that's used later for invocation.

I don't think it's that strange. It's something we do a lot in ASP.NET Core. Here's an example from minimal APIs. We do the same thing for MVC and SignalR to avoid regenerating the method invocation code every request.

In theory, someone could replace HttpContext.RequestServices to not match IHost.Services, but that hasn't been a problem. In practice, RequestServices usually represents a scope created from IHost.Services.

It is really convenient to get services automatically injected without needing to attribute them or rely on the service locator pattern, so it'd be sad to give that up to avoid the small risk of someone getting confused because they used multiple service providers with different sets of services. However, I suppose we could always add support for this later, since this would involve new overloads to AIFunction.Create.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference in my mind is how integrated the usage is into the DI-rooted programming model in the ASP..NET cases. The IServiceProvider ends up largely being ambient implicit context. That is not the case for the largely independent AIFunctionFactory, where you'd be very explicitly providing it with a specific IServiceProvider instance rather than it being picked up automatically from the environment.

we could always add support for this later, since this would involve new overloads to AIFunction.Create

Yes, though I don't think it would need new overloads. We'd just change the behavior if an IServiceProvider was set into a new property on the creation options.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards (2). @halter73 is leaning towards (3). @eiriktsarpalis? @SteveSandersonMS?

Barring other's input, I'm going to go with (2). I think we'll regret not having the mechanism to pass in additional state.

@stephentoub
Copy link
Member Author

Replaced by #6158

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-ai Microsoft.Extensions.AI libraries
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants