mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Adding SessionStorage and SessionManagement, improving samples to align Consumption vs Hosting
This commit is contained in:
+5
-1
@@ -11,6 +11,10 @@ Env.TraversePath().Load();
|
||||
|
||||
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
|
||||
|
||||
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
|
||||
?? throw new InvalidOperationException("AGENT_NAME is not set.");
|
||||
|
||||
var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
|
||||
|
||||
// Create the agent via the AI project client using the Responses API.
|
||||
@@ -23,7 +27,7 @@ AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential(
|
||||
providing explanations, brainstorming ideas, and offering guidance.
|
||||
Be concise, clear, and helpful in your responses.
|
||||
""",
|
||||
name: "simple-agent",
|
||||
name: agentName,
|
||||
description: "A simple general-purpose AI assistant");
|
||||
|
||||
// Host the agent as a Foundry Hosted Agent using the Responses API.
|
||||
|
||||
-1
@@ -2,7 +2,6 @@
|
||||
"profiles": {
|
||||
"Hosted-ChatClientAgent": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"simple-agent-foundry": {
|
||||
"HostedFoundryAgent": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
|
||||
+15
-10
@@ -1,33 +1,38 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ClientModel.Primitives;
|
||||
using Azure.AI.Extensions.OpenAI;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using DotNetEnv;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Foundry;
|
||||
|
||||
// Load .env file if present (for local development)
|
||||
Env.TraversePath().Load();
|
||||
|
||||
string agentEndpoint = Environment.GetEnvironmentVariable("AGENT_ENDPOINT") ?? "http://localhost:8088";
|
||||
Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT")
|
||||
?? "http://localhost:8088");
|
||||
|
||||
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
|
||||
?? throw new InvalidOperationException("AGENT_NAME is not set.");
|
||||
|
||||
// ── Create an agent-framework agent backed by the remote agent endpoint ──────
|
||||
|
||||
var endpointUri = new Uri(agentEndpoint);
|
||||
var options = new AIProjectClientOptions();
|
||||
|
||||
// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy
|
||||
// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right
|
||||
// before the request hits the wire.
|
||||
Uri clientEndpoint = endpointUri;
|
||||
if (endpointUri.Scheme == "http")
|
||||
if (agentEndpoint.Scheme == "http")
|
||||
{
|
||||
clientEndpoint = new UriBuilder(endpointUri) { Scheme = "https" }.Uri;
|
||||
// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy
|
||||
// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right
|
||||
// before the request hits the wire.
|
||||
|
||||
agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
|
||||
options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
|
||||
}
|
||||
|
||||
var aiProjectClient = new AIProjectClient(clientEndpoint, new AzureCliCredential(), options);
|
||||
var agent = aiProjectClient.AsAIAgent();
|
||||
var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options);
|
||||
FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName));
|
||||
|
||||
AgentSession session = await agent.CreateSessionAsync();
|
||||
|
||||
|
||||
@@ -229,7 +229,8 @@ public static partial class AzureAIProjectChatClientExtensions
|
||||
{
|
||||
Throw.IfNull(aiProjectClient);
|
||||
Throw.IfNull(agentOptions);
|
||||
agentOptions.ChatOptions ??= new();
|
||||
Throw.IfNull(agentOptions.ChatOptions);
|
||||
Throw.IfNullOrWhitespace(agentOptions.ChatOptions.ModelId);
|
||||
|
||||
IChatClient chatClient = aiProjectClient
|
||||
.GetProjectOpenAIClient()
|
||||
|
||||
@@ -47,8 +47,20 @@ public class AgentFrameworkResponseHandler : ResponseHandler
|
||||
{
|
||||
// 1. Resolve agent
|
||||
var agent = this.ResolveAgent(request);
|
||||
var sessionStore = this.ResolveSessionStore(request);
|
||||
|
||||
// 2. Create the SDK event stream builder
|
||||
// 2. Load or create a new session from the interaction
|
||||
var sessionConversationId = request.GetConversationId() ?? Guid.NewGuid().ToString();
|
||||
|
||||
var chatClientAgent = agent.GetService<ChatClientAgent>();
|
||||
|
||||
AgentSession? session = !string.IsNullOrEmpty(sessionConversationId)
|
||||
? await sessionStore.GetSessionAsync(agent, sessionConversationId, cancellationToken).ConfigureAwait(false)
|
||||
: chatClientAgent is not null
|
||||
? await chatClientAgent.CreateSessionAsync(sessionConversationId, cancellationToken).ConfigureAwait(false)
|
||||
: await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 3. Create the SDK event stream builder
|
||||
var stream = new ResponseEventStream(context, request);
|
||||
|
||||
// 3. Emit lifecycle events
|
||||
@@ -87,7 +99,7 @@ public class AgentFrameworkResponseHandler : ResponseHandler
|
||||
// and inside catch blocks. We use a flag to defer the yield to outside the try/catch.
|
||||
bool emittedTerminal = false;
|
||||
var enumerator = OutputConverter.ConvertUpdatesToEventsAsync(
|
||||
agent.RunStreamingAsync(messages, options: options, cancellationToken: cancellationToken),
|
||||
agent.RunStreamingAsync(messages, session, options: options, cancellationToken: cancellationToken),
|
||||
stream,
|
||||
cancellationToken).GetAsyncEnumerator(cancellationToken);
|
||||
try
|
||||
@@ -151,6 +163,12 @@ public class AgentFrameworkResponseHandler : ResponseHandler
|
||||
finally
|
||||
{
|
||||
await enumerator.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
// Persist session after streaming completes (successful or not)
|
||||
if (session is not null && !string.IsNullOrEmpty(sessionConversationId))
|
||||
{
|
||||
await sessionStore.SaveSessionAsync(agent, sessionConversationId, session, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +209,43 @@ public class AgentFrameworkResponseHandler : ResponseHandler
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an <see cref="AIAgent"/> from the request.
|
||||
/// Tries <c>agent.name</c> first, then falls back to <c>metadata["entity_id"]</c>.
|
||||
/// If neither is present, attempts to resolve a default (non-keyed) <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
private AgentSessionStore ResolveSessionStore(CreateResponse request)
|
||||
{
|
||||
var agentName = GetAgentName(request);
|
||||
|
||||
if (!string.IsNullOrEmpty(agentName))
|
||||
{
|
||||
var sessionStore = this._serviceProvider.GetKeyedService<AgentSessionStore>(agentName);
|
||||
if (sessionStore is not null)
|
||||
{
|
||||
return sessionStore;
|
||||
}
|
||||
|
||||
if (this._logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
this._logger.LogWarning("SessionStore for agent '{AgentName}' not found in keyed services. Attempting default resolution.", agentName);
|
||||
}
|
||||
}
|
||||
|
||||
// Try non-keyed default
|
||||
var defaultSessionStore = this._serviceProvider.GetService<AgentSessionStore>();
|
||||
if (defaultSessionStore is not null)
|
||||
{
|
||||
return defaultSessionStore;
|
||||
}
|
||||
|
||||
var errorMessage = string.IsNullOrEmpty(agentName)
|
||||
? "No agent name specified in the request (via agent.name or metadata[\"entity_id\"]) and no default AgentSessionStore is registered."
|
||||
: $"Agent '{agentName}' not found. Ensure it is registered via AddAIAgent(\"{agentName}\", ...) or as a default AgentSessionStore.";
|
||||
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
private static string? GetAgentName(CreateResponse request)
|
||||
{
|
||||
// Try agent.name from AgentReference
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Foundry.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for storing and retrieving agent conversation sessions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations of this interface enable persistent storage of conversation sessions,
|
||||
/// allowing conversations to be resumed across HTTP requests, application restarts,
|
||||
/// or different service instances in hosted scenarios.
|
||||
/// </remarks>
|
||||
public abstract class AgentSessionStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a serialized agent session to persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that owns this session.</param>
|
||||
/// <param name="conversationId">The unique identifier for the conversation/session.</param>
|
||||
/// <param name="session">The session to save.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task that represents the asynchronous save operation.</returns>
|
||||
public abstract ValueTask SaveSessionAsync(
|
||||
AIAgent agent,
|
||||
string conversationId,
|
||||
AgentSession session,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a serialized agent session from persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that owns this session.</param>
|
||||
/// <param name="conversationId">The unique identifier for the conversation/session to retrieve.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous retrieval operation.
|
||||
/// The task result contains the session, or a new session if not found.
|
||||
/// </returns>
|
||||
public abstract ValueTask<AgentSession> GetSessionAsync(
|
||||
AIAgent agent,
|
||||
string conversationId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Foundry.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-memory implementation of <see cref="AgentSessionStore"/> for development and testing scenarios.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This implementation stores sessions in memory using a concurrent dictionary and is suitable for:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Single-instance development scenarios</description></item>
|
||||
/// <item><description>Testing and prototyping</description></item>
|
||||
/// <item><description>Scenarios where session persistence across restarts is not required</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Warning:</strong> All stored sessions will be lost when the application restarts.
|
||||
/// For production use with multiple instances or persistence across restarts, use a durable storage implementation
|
||||
/// such as Redis, SQL Server, or Azure Cosmos DB.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class InMemoryAgentSessionStore : AgentSessionStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, JsonElement> _sessions = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask SaveSessionAsync(AIAgent agent, string conversationId, AgentSession session, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = GetKey(conversationId, agent.Id);
|
||||
this._sessions[key] = await agent.SerializeSessionAsync(session, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async ValueTask<AgentSession> GetSessionAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = GetKey(conversationId, agent.Id);
|
||||
JsonElement? sessionContent = this._sessions.TryGetValue(key, out var existingSession) ? existingSession : null;
|
||||
|
||||
return sessionContent switch
|
||||
{
|
||||
null => await agent.CreateSessionAsync(cancellationToken).ConfigureAwait(false),
|
||||
_ => await agent.DeserializeSessionAsync(sessionContent.Value, cancellationToken: cancellationToken).ConfigureAwait(false),
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}";
|
||||
}
|
||||
@@ -45,6 +45,7 @@ public static class FoundryHostingExtensions
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
services.AddResponsesServer();
|
||||
services.TryAddSingleton<AgentSessionStore, InMemoryAgentSessionStore>();
|
||||
services.TryAddSingleton<ResponseHandler, AgentFrameworkResponseHandler>();
|
||||
return services;
|
||||
}
|
||||
@@ -71,14 +72,27 @@ public static class FoundryHostingExtensions
|
||||
/// </remarks>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="agent">The agent instance to register.</param>
|
||||
/// <param name="agentSessionStore">The agent session store to use for managing agent sessions server-side. If null, an in-memory session store will be used.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent)
|
||||
public static IServiceCollection AddFoundryResponses(this IServiceCollection services, AIAgent agent, AgentSessionStore? agentSessionStore = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
|
||||
services.AddResponsesServer();
|
||||
services.TryAddSingleton(agent);
|
||||
agentSessionStore ??= new InMemoryAgentSessionStore();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(agent.Name))
|
||||
{
|
||||
services.TryAddKeyedSingleton(agent.Name, agent);
|
||||
services.TryAddKeyedSingleton(agent.Name, agentSessionStore);
|
||||
}
|
||||
else
|
||||
{
|
||||
services.TryAddSingleton(agent);
|
||||
services.TryAddSingleton(agentSessionStore);
|
||||
}
|
||||
|
||||
services.TryAddSingleton<ResponseHandler, AgentFrameworkResponseHandler>();
|
||||
return services;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user