mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: API to manage AgentThreads in hosting scenarios (#1520)
* skeleton * wip * rename + fix tests * implement workflow tests * fix comments * Update dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fixes * proto * fix worfklow build logic * build it / no reflection / no generics / extensions on aiagent * rollback + new overload on workflow builder * address PR comments * fix build * take from main * correct based on latest API * apply suggestion * address PR comments x1 * address PR comments 2 * renames * *With* * refactor api a bit * refactor + merge main + apply suggestions --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
31701dbb92
commit
73eb00b37b
@@ -104,7 +104,10 @@ else
|
||||
throw new ArgumentException("Either A2AServer:ApiKey or A2AServer:ConnectionString & agentId must be provided");
|
||||
}
|
||||
|
||||
var a2aTaskManager = app.MapA2A(hostA2AAgent, path: "/", agentCard: hostA2AAgentCard);
|
||||
app.MapWellKnownAgentCard(a2aTaskManager, "/");
|
||||
var a2aTaskManager = app.MapA2A(
|
||||
hostA2AAgent,
|
||||
path: "/",
|
||||
agentCard: hostA2AAgentCard,
|
||||
taskManager => app.MapWellKnownAgentCard(taskManager, "/"));
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -24,7 +24,8 @@ builder.AddAIAgent(
|
||||
"pirate",
|
||||
instructions: "You are a pirate. Speak like a pirate",
|
||||
description: "An agent that speaks like a pirate.",
|
||||
chatClientServiceKey: "chat-model");
|
||||
chatClientServiceKey: "chat-model")
|
||||
.WithInMemoryThreadStore();
|
||||
|
||||
builder.AddAIAgent("knights-and-knaves", (sp, key) =>
|
||||
{
|
||||
|
||||
+79
-14
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using A2A;
|
||||
using A2A.AspNetCore;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Hosting;
|
||||
using Microsoft.Agents.AI.Hosting.A2A;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -23,10 +25,21 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path)
|
||||
=> endpoints.MapA2A(agentName, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path);
|
||||
return endpoints.MapA2A(agent, path, configureTaskManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -42,10 +55,27 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agentName, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agentName">The name of the agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, string agentName, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
var agent = endpoints.ServiceProvider.GetRequiredKeyedService<AIAgent>(agentName);
|
||||
return endpoints.MapA2A(agent, path, agentCard);
|
||||
return endpoints.MapA2A(agent, path, agentCard, configureTaskManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,11 +85,26 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path)
|
||||
=> endpoints.MapA2A(agent, path, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var taskManager = agent.MapA2A(loggerFactory: loggerFactory);
|
||||
return endpoints.MapA2A(taskManager, path);
|
||||
var agentThreadStore = endpoints.ServiceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
|
||||
var taskManager = agent.MapA2A(loggerFactory: loggerFactory, agentThreadStore: agentThreadStore);
|
||||
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
|
||||
|
||||
configureTaskManager(taskManager);
|
||||
return endpointConventionBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,11 +120,33 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard)
|
||||
=> endpoints.MapA2A(agent, path, agentCard, _ => { });
|
||||
|
||||
/// <summary>
|
||||
/// Attaches A2A (Agent2Agent) communication capabilities via Message processing to the specified web application.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the A2A endpoints to.</param>
|
||||
/// <param name="agent">The agent to use for A2A protocol integration.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <param name="agentCard">Agent card info to return on query.</param>
|
||||
/// <param name="configureTaskManager">The callback to configure <see cref="ITaskManager"/>.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
/// <remarks>
|
||||
/// This method can be used to access A2A agents that support the
|
||||
/// <see href="https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md#2-curated-registries-catalog-based-discovery">Curated Registries (Catalog-Based Discovery)</see>
|
||||
/// discovery mechanism.
|
||||
/// </remarks>
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, AIAgent agent, string path, AgentCard agentCard, Action<ITaskManager> configureTaskManager)
|
||||
{
|
||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, loggerFactory: loggerFactory);
|
||||
return endpoints.MapA2A(taskManager, path);
|
||||
var agentThreadStore = endpoints.ServiceProvider.GetKeyedService<AgentThreadStore>(agent.Name);
|
||||
var taskManager = agent.MapA2A(agentCard: agentCard, agentThreadStore: agentThreadStore, loggerFactory: loggerFactory);
|
||||
var endpointConventionBuilder = endpoints.MapA2A(taskManager, path);
|
||||
|
||||
configureTaskManager(taskManager);
|
||||
|
||||
return endpointConventionBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,14 +157,12 @@ public static class MicrosoftAgentAIHostingA2AEndpointRouteBuilderExtensions
|
||||
/// <param name="taskManager">Pre-configured A2A TaskManager to use for A2A endpoints handling.</param>
|
||||
/// <param name="path">The route group to use for A2A endpoints.</param>
|
||||
/// <returns>Configured <see cref="ITaskManager"/> for A2A integration.</returns>
|
||||
public static ITaskManager MapA2A(this IEndpointRouteBuilder endpoints, TaskManager taskManager, string path)
|
||||
public static IEndpointConventionBuilder MapA2A(this IEndpointRouteBuilder endpoints, ITaskManager taskManager, string path)
|
||||
{
|
||||
// note: current SDK version registers multiple `.well-known/agent.json` handlers here.
|
||||
// it makes app return HTTP 500, but will be fixed once new A2A SDK is released.
|
||||
// see https://github.com/microsoft/agent-framework/issues/476 for details
|
||||
A2ARouteBuilderExtensions.MapA2A(endpoints, taskManager, path);
|
||||
endpoints.MapHttpA2A(taskManager, path);
|
||||
|
||||
return taskManager;
|
||||
return endpoints.MapHttpA2A(taskManager, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,29 +20,37 @@ public static class AIAgentExtensions
|
||||
/// <param name="agent">Agent to attach A2A messaging processing capabilities to.</param>
|
||||
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
|
||||
/// <param name="agentThreadStore">The store to store thread contents and metadata.</param>
|
||||
/// <returns>The configured <see cref="TaskManager"/>.</returns>
|
||||
public static TaskManager MapA2A(
|
||||
public static ITaskManager MapA2A(
|
||||
this AIAgent agent,
|
||||
TaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
ITaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
AgentThreadStore? agentThreadStore = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(agent);
|
||||
ArgumentNullException.ThrowIfNull(agent.Name);
|
||||
|
||||
taskManager ??= new();
|
||||
var hostAgent = new AIHostAgent(
|
||||
innerAgent: agent,
|
||||
threadStore: agentThreadStore ?? new NoopAgentThreadStore());
|
||||
|
||||
taskManager ??= new TaskManager();
|
||||
taskManager.OnMessageReceived += OnMessageReceivedAsync;
|
||||
|
||||
return taskManager;
|
||||
|
||||
async Task<A2AResponse> OnMessageReceivedAsync(MessageSendParams messageSendParams, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await agent.RunAsync(
|
||||
messageSendParams.ToChatMessages(),
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
|
||||
var parts = response.Messages.ToParts();
|
||||
var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await hostAgent.RunAsync(
|
||||
messageSendParams.ToChatMessages(),
|
||||
thread: thread,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false);
|
||||
var parts = response.Messages.ToParts();
|
||||
return new AgentMessage
|
||||
{
|
||||
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
|
||||
@@ -60,14 +68,16 @@ public static class AIAgentExtensions
|
||||
/// <param name="agentCard">The agent card to return on query.</param>
|
||||
/// <param name="taskManager">Instance of <see cref="TaskManager"/> to configure for A2A messaging. New instance will be created if not passed.</param>
|
||||
/// <param name="loggerFactory">The logger factory to use for creating <see cref="ILogger"/> instances.</param>
|
||||
/// <param name="agentThreadStore">The store to store thread contents and metadata.</param>
|
||||
/// <returns>The configured <see cref="TaskManager"/>.</returns>
|
||||
public static TaskManager MapA2A(
|
||||
public static ITaskManager MapA2A(
|
||||
this AIAgent agent,
|
||||
AgentCard agentCard,
|
||||
TaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
ITaskManager? taskManager = null,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
AgentThreadStore? agentThreadStore = null)
|
||||
{
|
||||
taskManager = agent.MapA2A(taskManager, loggerFactory);
|
||||
taskManager = agent.MapA2A(taskManager, loggerFactory, agentThreadStore);
|
||||
|
||||
taskManager.OnAgentCardQuery += (context, query) =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a hosting wrapper around an <see cref="AIAgent"/> that adds thread persistence capabilities
|
||||
/// for server-hosted scenarios where conversations need to be restored across requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="AIHostAgent"/> wraps an existing agent implementation and adds the ability to
|
||||
/// persist and restore conversation threads using an <see cref="AgentThreadStore"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This wrapper enables thread persistence without requiring type-specific knowledge of the thread type,
|
||||
/// as all thread operations work through the base <see cref="AgentThread"/> abstraction.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class AIHostAgent : DelegatingAIAgent
|
||||
{
|
||||
private readonly AgentThreadStore _threadStore;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AIHostAgent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="innerAgent">The underlying agent implementation to wrap.</param>
|
||||
/// <param name="threadStore">The thread store to use for persisting conversation state.</param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <paramref name="innerAgent"/> or <paramref name="threadStore"/> is <see langword="null"/>.
|
||||
/// </exception>
|
||||
public AIHostAgent(AIAgent innerAgent, AgentThreadStore threadStore)
|
||||
: base(innerAgent)
|
||||
{
|
||||
this._threadStore = Throw.IfNull(threadStore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an existing agent thread for the specified conversation, or creates a new one if none exists.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The unique identifier of the conversation for which to retrieve or create the agent thread. Cannot be null,
|
||||
/// empty, or consist only of white-space characters.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The task result contains the agent thread associated with the
|
||||
/// specified conversation. If no thread exists, a new thread is created and returned.</returns>
|
||||
public ValueTask<AgentThread> GetOrCreateThreadAsync(string conversationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(conversationId);
|
||||
|
||||
return this._threadStore.GetThreadAsync(this.InnerAgent, conversationId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists a conversation thread to the thread store.
|
||||
/// </summary>
|
||||
/// <param name="conversationId">The unique identifier for the conversation.</param>
|
||||
/// <param name="thread">The thread to persist.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
|
||||
/// <returns>A task that represents the asynchronous save operation.</returns>
|
||||
/// <exception cref="ArgumentException"><paramref name="conversationId"/> is null or whitespace.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="thread"/> is <see langword="null"/>.</exception>
|
||||
public ValueTask SaveThreadAsync(string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNullOrWhitespace(conversationId);
|
||||
_ = Throw.IfNull(thread);
|
||||
|
||||
return this._threadStore.SaveThreadAsync(this.InnerAgent, conversationId, thread, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring <see cref="AIAgent"/>.
|
||||
/// </summary>
|
||||
public static class HostedAgentBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the host agent builder to use an in-memory thread store for agent thread management.
|
||||
/// </summary>
|
||||
/// <param name="builder">The host agent builder to configure with the in-memory thread store.</param>
|
||||
/// <returns>The same <paramref name="builder"/> instance, configured to use an in-memory thread store.</returns>
|
||||
public static IHostedAgentBuilder WithInMemoryThreadStore(this IHostedAgentBuilder builder)
|
||||
{
|
||||
builder.ServiceCollection.AddKeyedSingleton<AgentThreadStore>(builder.Name, new InMemoryAgentThreadStore());
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified agent thread store with the host agent builder, enabling thread-specific storage for
|
||||
/// agent operations.
|
||||
/// </summary>
|
||||
/// <param name="builder">The host agent builder to configure with the thread store. Cannot be null.</param>
|
||||
/// <param name="store">The agent thread store instance to register. Cannot be null.</param>
|
||||
/// <returns>The same host agent builder instance, allowing for method chaining.</returns>
|
||||
public static IHostedAgentBuilder WithThreadStore(this IHostedAgentBuilder builder, AgentThreadStore store)
|
||||
{
|
||||
builder.ServiceCollection.AddKeyedSingleton(builder.Name, store);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the host agent builder to use a custom thread store implementation for agent threads.
|
||||
/// </summary>
|
||||
/// <param name="builder">The host agent builder to configure.</param>
|
||||
/// <param name="createAgentThreadStore">A factory function that creates an agent thread store instance using the provided service provider and agent
|
||||
/// name.</param>
|
||||
/// <returns>The same host agent builder instance, enabling further configuration.</returns>
|
||||
public static IHostedAgentBuilder WithThreadStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, AgentThreadStore> createAgentThreadStore)
|
||||
{
|
||||
builder.ServiceCollection.AddKeyedSingleton(builder.Name, (sp, key) =>
|
||||
{
|
||||
Throw.IfNull(key);
|
||||
var keyString = key as string;
|
||||
Throw.IfNullOrEmpty(keyString);
|
||||
var store = createAgentThreadStore(sp, keyString);
|
||||
if (store is null)
|
||||
{
|
||||
throw new InvalidOperationException($"The agent thread store factory did not return a valid {nameof(AgentThreadStore)} instance for key '{keyString}'.");
|
||||
}
|
||||
|
||||
return store;
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the contract for storing and retrieving agent conversation threads.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implementations of this interface enable persistent storage of conversation threads,
|
||||
/// allowing conversations to be resumed across HTTP requests, application restarts,
|
||||
/// or different service instances in hosted scenarios.
|
||||
/// </remarks>
|
||||
public abstract class AgentThreadStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a serialized agent thread to persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that owns this thread.</param>
|
||||
/// <param name="conversationId">The unique identifier for the conversation/thread.</param>
|
||||
/// <param name="thread">The thread 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 SaveThreadAsync(
|
||||
AIAgent agent,
|
||||
string conversationId,
|
||||
AgentThread thread,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a serialized agent thread from persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="agent">The agent that owns this thread.</param>
|
||||
/// <param name="conversationId">The unique identifier for the conversation/thread 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 serialized thread state, or <see langword="null"/> if not found.
|
||||
/// </returns>
|
||||
public abstract ValueTask<AgentThread> GetThreadAsync(
|
||||
AIAgent agent,
|
||||
string conversationId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an in-memory implementation of <see cref="AgentThreadStore"/> for development and testing scenarios.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This implementation stores threads 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 thread persistence across restarts is not required</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Warning:</strong> All stored threads 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 InMemoryAgentThreadStore : AgentThreadStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, JsonElement> _threads = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask SaveThreadAsync(AIAgent agent, string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = GetKey(conversationId, agent.Id);
|
||||
this._threads[key] = thread.Serialize();
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<AgentThread> GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = GetKey(conversationId, agent.Id);
|
||||
JsonElement? threadContent = this._threads.TryGetValue(key, out var existingThread) ? existingThread : null;
|
||||
|
||||
return threadContent switch
|
||||
{
|
||||
null => new ValueTask<AgentThread>(agent.GetNewThread()),
|
||||
_ => new ValueTask<AgentThread>(agent.DeserializeThread(threadContent.Value)),
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// This store implementation does not have any store under the hood and operates with empty threads.
|
||||
/// It is the "noop" store, and could be used if you are keeping the thread contents on the client side for example.
|
||||
/// </summary>
|
||||
public sealed class NoopAgentThreadStore : AgentThreadStore
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask SaveThreadAsync(AIAgent agent, string conversationId, AgentThread thread, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ValueTask<AgentThread> GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return new ValueTask<AgentThread>(agent.GetNewThread());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user