-
Notifications
You must be signed in to change notification settings - Fork 587
.NET: API to manage AgentThreads in hosting scenarios #1520
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
Merged
+368
−29
Merged
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
f2ce394
skeleton
DeagleGross 645c75d
wip
DeagleGross 768e459
rename + fix tests
DeagleGross 1be5db3
implement workflow tests
DeagleGross 390a27b
fix comments
DeagleGross afbc5a7
Merge branch 'main' into dmkorolev/workflow-extensions
DeagleGross e40c341
Update dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderW…
DeagleGross 650fc07
fixes
DeagleGross 864643a
Merge branch 'dmkorolev/workflow-extensions' of https://github.com/mi…
DeagleGross 282e2cb
Merge branch 'main' into dmkorolev/workflow-extensions
DeagleGross 71692a7
proto
DeagleGross e512e45
fix worfklow build logic
DeagleGross 91f4e6e
Merge branch 'dmkorolev/workflow-extensions' into dmkorolev/hosting-t…
DeagleGross f4601b8
build it / no reflection / no generics / extensions on aiagent
DeagleGross 9d36d4c
rollback + new overload on workflow builder
DeagleGross da86a48
address PR comments
DeagleGross 35cc557
Merge branch 'main' into dmkorolev/workflow-extensions
DeagleGross 950c90b
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross 1454e28
Merge branch 'dmkorolev/workflow-extensions' into dmkorolev/hosting-t…
DeagleGross 3aef3f7
fix build
DeagleGross 928cb27
merge main
DeagleGross 644a44f
take from main
DeagleGross 7e02022
correct based on latest API
DeagleGross 1b502d7
apply suggestion
DeagleGross 3f20a5c
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross 372740c
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross 82798f2
address PR comments x1
DeagleGross 1da7511
Merge branch 'dmkorolev/hosting-threads' of https://github.com/micros…
DeagleGross 24dcecc
address PR comments 2
DeagleGross 70bec38
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross c673d7c
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross ae2eb5f
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross 482b198
Merge branch 'main' into dmkorolev/hosting-threads
DeagleGross 07a4833
renames
DeagleGross 3ef2f04
Merge branch 'dmkorolev/hosting-threads' of https://github.com/micros…
DeagleGross 1cb2284
*With*
DeagleGross b0e55ef
merge main
DeagleGross 93bd904
refactor api a bit
DeagleGross 7a0882e
refactor + merge main + apply suggestions
DeagleGross 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
Some comments aren't visible on the classic Files Changed page.
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
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
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
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 |
|---|---|---|
| @@ -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="IAgentThreadStore"/>. | ||
| /// </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 IAgentThreadStore _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, IAgentThreadStore 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); | ||
| } | ||
| } |
62 changes: 62 additions & 0 deletions
62
dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentBuilderExtensions.cs
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 |
|---|---|---|
| @@ -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 AddInMemoryThreadStore(this IHostedAgentBuilder builder) | ||
| { | ||
| builder.HostApplicationBuilder.Services.AddKeyedSingleton<IAgentThreadStore>(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 AddThreadStore(this IHostedAgentBuilder builder, IAgentThreadStore store) | ||
| { | ||
| builder.HostApplicationBuilder.Services.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 AddThreadStore(this IHostedAgentBuilder builder, Func<IServiceProvider, string, IAgentThreadStore> createAgentThreadStore) | ||
| { | ||
| builder.HostApplicationBuilder.Services.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(IAgentThreadStore)} instance for key '{keyString}'."); | ||
| } | ||
|
|
||
| return store; | ||
| }); | ||
| return builder; | ||
| } | ||
| } |
46 changes: 46 additions & 0 deletions
46
dotnet/src/Microsoft.Agents.AI.Hosting/IAgentThreadStore.cs
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 |
|---|---|---|
| @@ -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 interface IAgentThreadStore | ||
DeagleGross marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| /// <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> | ||
| 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> | ||
| ValueTask<AgentThread> GetThreadAsync( | ||
| AIAgent agent, | ||
| string conversationId, | ||
| CancellationToken cancellationToken = default); | ||
| } | ||
54 changes: 54 additions & 0 deletions
54
dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentThreadStore.cs
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 |
|---|---|---|
| @@ -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="IAgentThreadStore"/> 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 : IAgentThreadStore | ||
| { | ||
| private readonly ConcurrentDictionary<string, JsonElement> _threads = new(); | ||
|
|
||
| /// <inheritdoc/> | ||
| public 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 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()), | ||
DeagleGross marked this conversation as resolved.
Show resolved
Hide resolved
DeagleGross marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _ => new ValueTask<AgentThread>(agent.DeserializeThread(threadContent.Value)), | ||
| }; | ||
| } | ||
|
|
||
| private static string GetKey(string conversationId, string agentId) => $"{agentId}:{conversationId}"; | ||
| } | ||
25 changes: 25 additions & 0 deletions
25
dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentThreadStore.cs
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 |
|---|---|---|
| @@ -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 : IAgentThreadStore | ||
| { | ||
| /// <inheritdoc/> | ||
| public ValueTask SaveThreadAsync(AIAgent agent, string conversationId, AgentThread thread, CancellationToken cancellationToken = default) | ||
| { | ||
| return new ValueTask(); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public ValueTask<AgentThread> GetThreadAsync(AIAgent agent, string conversationId, CancellationToken cancellationToken = default) | ||
| { | ||
| return new ValueTask<AgentThread>(agent.GetNewThread()); | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.