Using an agent
Your app uses an agent through its host, AgentHost<TAgent>, which services.AddTypeshift() registers for every agent (Registration). You get the host like any other service, from the scope of the request or job that runs the agent, so the agent's tools see that scope's services. Running agents covers what a run takes and returns.
Inject the host in a web app
In a web app, add the host to the constructor of the class that runs the agent, such as a controller or a service of your own, or to the parameters of a minimal API endpoint:
internal sealed class RefundDesk(AgentHost<SupportAgent> support, ICurrentCustomer customer)
{
public async Task<string> AnswerAsync(string question, CancellationToken ct)
{
var answer = await support.RunAsync(question, settingsId: customer.Id, ct: ct);
return answer.Text;
}
}ICurrentCustomer stands for a scoped service of your app, and the customer's id is the settings id, so that customer's stored settings apply (Stored settings). Register RefundDesk as scoped, with services.AddScoped<RefundDesk>(), so ASP.NET Core creates it in each request's scope, together with its host and that request's customer. Controllers and endpoint parameters need no registration of their own.
Run from a console app
Outside a request, such as in a console app, nothing creates a scope for you. Create one from your app's service provider and resolve the host from it:
await using var scope = provider.CreateAsyncScope();
var support = scope.ServiceProvider.GetRequiredService<AgentHost<SupportAgent>>();
var answer = await support.RunAsync("Where is my refund?");
Console.WriteLine($"{answer.Messages[^1].AuthorName}: {answer.Text}");Resolve the host from a scope, never from the root provider
A host resolved from the root provider, such as app.Services or what BuildServiceProvider() returns, runs with the root's services. With scope validation on, as in ASP.NET Core's development environment, the run fails as soon as it resolves a scoped service, such as your chat client provider. Without it, every run shares one instance of each scoped service, so one customer's ICurrentCustomer reaches the tools of another.
Run from a background job
A BackgroundService is a singleton, so a host it took in its constructor would come from the root provider. Take IServiceScopeFactory instead, and create a scope for each job:
internal sealed class TicketDigest(IServiceScopeFactory scopes, TimeProvider time) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromDays(1), time);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DigestAsync(stoppingToken);
}
}
public async Task<string> DigestAsync(CancellationToken ct)
{
// A scope for each job, so the run gets scoped services of its own, as a request would
await using var scope = scopes.CreateAsyncScope();
var support = scope.ServiceProvider.GetRequiredService<AgentHost<SupportAgent>>();
var answer = await support.RunAsync("Summarize the tickets opened today.", ct: ct);
return answer.Text;
}
}Register the job with services.AddHostedService<TicketDigest>(), and the clock its timer waits on with services.AddSingleton(TimeProvider.System). A test can register a fake TimeProvider instead.
Related
- Getting started: a first toolset, agent and run.
- Running agents: settings ids, run settings, typed output, streaming and the raw path.
- Registration: every service
AddTypeshift()registers and its lifetime. - Agent host: every member of the host and its signature.
- Serving a chat endpoint: the endpoints a chat UI talks to.