-
Notifications
You must be signed in to change notification settings - Fork 1.9k
.NET: Add factory delegate overload to MapAGUI for per-request agent resolution #6659
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
Open
Ashutosh0x
wants to merge
2
commits into
microsoft:main
Choose a base branch
from
Ashutosh0x:feature/dynamic-agent-resolution-phase1
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+172
−43
Open
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
@@ -169,6 +169,115 @@ public static IEndpointConventionBuilder MapAGUI( | |
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Maps an AG-UI agent endpoint using a factory delegate for per-request agent resolution. | ||
| /// This enables dynamic, multi-tenant agent hosting where the agent is selected based on | ||
| /// route parameters, request headers, claims, or other per-request information. | ||
| /// </summary> | ||
| /// <param name="endpoints">The endpoint route builder.</param> | ||
| /// <param name="pattern">The URL pattern for the endpoint (e.g., "/agents/{agentId}").</param> | ||
| /// <param name="agentFactory"> | ||
| /// A factory delegate that resolves an <see cref="AIAgent"/> for each request. | ||
| /// The delegate receives the current <see cref="HttpContext"/> and a <see cref="CancellationToken"/>. | ||
| /// Return <c>null</c> to produce a 404 Not Found response. | ||
| /// </param> | ||
| /// <returns>An <see cref="IEndpointConventionBuilder"/> for the mapped endpoint.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Unlike the static <see cref="MapAGUI(IEndpointRouteBuilder, string, AIAgent)"/> overload, | ||
| /// this method does not capture the agent at startup. Instead, the agent and its | ||
| /// <see cref="AgentSessionStore"/> are resolved per-request from the factory delegate and | ||
| /// <see cref="HttpContext.RequestServices"/> respectively. This fixes the singleton-capture | ||
| /// issue where scoped or transient session stores were inadvertently captured at startup. | ||
| /// </para> | ||
| /// <para> | ||
| /// <strong>Trust model.</strong> See remarks on | ||
| /// <see cref="MapAGUI(IEndpointRouteBuilder, string, AIAgent)"/> for session isolation guidance. | ||
| /// </para> | ||
| /// </remarks> | ||
| public static IEndpointConventionBuilder MapAGUI( | ||
| this IEndpointRouteBuilder endpoints, | ||
| [StringSyntax("route")] string pattern, | ||
| Func<HttpContext, CancellationToken, ValueTask<AIAgent?>> agentFactory) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(endpoints); | ||
| ArgumentNullException.ThrowIfNull(agentFactory); | ||
|
|
||
| return endpoints.MapPost(pattern, async ([FromBody] RunAgentInput? input, HttpContext context, CancellationToken cancellationToken) => | ||
| { | ||
| if (input is null) | ||
| { | ||
| return Results.BadRequest(); | ||
| } | ||
|
|
||
| // Resolve agent per-request via factory delegate | ||
| var aiAgent = await agentFactory(context, cancellationToken).ConfigureAwait(false); | ||
|
Comment on lines
+162
to
+170
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed! Extracted the shared execution pipeline into a private
This ensures the message conversion, run options, streaming pipeline, and session save logic stay in sync when AG-UI behavior changes. |
||
| if (aiAgent is null) | ||
| { | ||
| return Results.NotFound(); | ||
| } | ||
|
|
||
| // Resolve session store per-request from the request's DI scope (not app-level) | ||
| var agentSessionStore = context.RequestServices.GetKeyedService<AgentSessionStore>(aiAgent.Name); | ||
|
|
||
| // Ensure that we have an IsolationKeyScopedAgentSessionStore registered. | ||
| var isolationKeyProvider = context.RequestServices.GetService<SessionIsolationKeyProvider>(); | ||
| if (agentSessionStore?.GetService<IsolationKeyScopedAgentSessionStore>() is null) | ||
| { | ||
| agentSessionStore ??= new NoopAgentSessionStore(); | ||
| agentSessionStore = new IsolationKeyScopedAgentSessionStore(agentSessionStore, isolationKeyProvider, new() { Strict = isolationKeyProvider != null }); | ||
| } | ||
|
|
||
| var hostAgent = new AIHostAgent(aiAgent, agentSessionStore); | ||
|
|
||
| var jsonOptions = context.RequestServices.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>(); | ||
| var jsonSerializerOptions = jsonOptions.Value.SerializerOptions; | ||
|
|
||
| var messages = input.Messages.AsChatMessages(jsonSerializerOptions); | ||
| var clientTools = input.Tools?.AsAITools().ToList(); | ||
|
|
||
| // Create run options with AG-UI context in AdditionalProperties | ||
| var runOptions = new ChatClientAgentRunOptions | ||
| { | ||
| ChatOptions = new ChatOptions | ||
| { | ||
| Tools = clientTools, | ||
| AdditionalProperties = new AdditionalPropertiesDictionary | ||
| { | ||
| ["ag_ui_state"] = input.State, | ||
| ["ag_ui_context"] = input.Context?.Select(c => new KeyValuePair<string, string>(c.Description, c.Value)).ToArray(), | ||
| ["ag_ui_forwarded_properties"] = input.ForwardedProperties, | ||
| ["ag_ui_thread_id"] = input.ThreadId, | ||
| ["ag_ui_run_id"] = input.RunId | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| var threadId = string.IsNullOrWhiteSpace(input.ThreadId) ? Guid.NewGuid().ToString("N") : input.ThreadId; | ||
| var session = await hostAgent.GetOrCreateSessionAsync(threadId, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| // Run the agent and convert to AG-UI events | ||
| var events = hostAgent.RunStreamingAsync( | ||
| messages, | ||
| session: session, | ||
| options: runOptions, | ||
| cancellationToken: cancellationToken) | ||
| .AsChatResponseUpdatesAsync() | ||
| .FilterServerToolsFromMixedToolInvocationsAsync(clientTools, cancellationToken) | ||
| .AsAGUIEventStreamAsync( | ||
| threadId, | ||
| input.RunId, | ||
| jsonSerializerOptions, | ||
| cancellationToken); | ||
|
|
||
| // Wrap the event stream to save the session after streaming completes | ||
| var eventsWithSessionSave = SaveSessionAfterStreamingAsync(events, hostAgent, threadId, session, cancellationToken); | ||
|
|
||
| var sseLogger = context.RequestServices.GetRequiredService<ILogger<AGUIServerSentEventsResult>>(); | ||
| return new AGUIServerSentEventsResult(eventsWithSessionSave, sseLogger); | ||
| }); | ||
| } | ||
|
|
||
| private static async IAsyncEnumerable<BaseEvent> SaveSessionAfterStreamingAsync( | ||
| IAsyncEnumerable<BaseEvent> events, | ||
| AIHostAgent hostAgent, | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Pushed 0131d01 addressing both review items:
MapAGUI_WithFactoryDelegate_MapsEndpoint_AtSpecifiedPatternMapAGUI_WithNullFactory_ThrowsArgumentNullExceptionMapAGUI_WithFactoryDelegate_AndNullEndpoints_ThrowsArgumentNullExceptionThese follow the same Moq + xUnit pattern used by the existing overload tests.