-
Notifications
You must be signed in to change notification settings - Fork 503
.NET: introduce API to manage the thread in Hosting scenarios #1392
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
Closed
DeagleGross
wants to merge
6
commits into
dmkorolev/workflow-extensions
from
dmkorolev/hosting-threads
Closed
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
71692a7
proto
DeagleGross 91f4e6e
Merge branch 'dmkorolev/workflow-extensions' into dmkorolev/hosting-t…
DeagleGross f4601b8
build it / no reflection / no generics / extensions on aiagent
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 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
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,134 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Text.Json; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.AI; | ||
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 : AIAgent | ||
{ | ||
private readonly AIAgent _innerAgent; | ||
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) | ||
{ | ||
this._innerAgent = Throw.IfNull(innerAgent); | ||
this._threadStore = Throw.IfNull(threadStore); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override string Id => this._innerAgent.Id; | ||
|
||
/// <inheritdoc/> | ||
public override string? Name => this._innerAgent.Name; | ||
|
||
/// <inheritdoc/> | ||
public override string? Description => this._innerAgent.Description; | ||
|
||
/// <inheritdoc/> | ||
public override object? GetService(Type serviceType, object? serviceKey = null) | ||
=> base.GetService(serviceType, serviceKey) ?? this._innerAgent.GetService(serviceType, serviceKey); | ||
|
||
/// <inheritdoc/> | ||
public override AgentThread GetNewThread() => this._innerAgent.GetNewThread(); | ||
|
||
/// <inheritdoc/> | ||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) | ||
=> this._innerAgent.DeserializeThread(serializedThread, jsonSerializerOptions); | ||
|
||
/// <summary> | ||
/// Restores a conversation thread from the thread store using the specified conversation ID. | ||
/// </summary> | ||
/// <param name="conversationId">The unique identifier of the conversation to restore.</param> | ||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param> | ||
/// <returns> | ||
/// A task that represents the asynchronous operation. The task result contains the restored thread, | ||
/// or <see langword="null"/> if no thread with the given ID exists in the store. | ||
/// </returns> | ||
/// <exception cref="ArgumentException"><paramref name="conversationId"/> is null or whitespace.</exception> | ||
public async ValueTask<AgentThread?> RestoreThreadAsync( | ||
string conversationId, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
_ = Throw.IfNullOrWhitespace(conversationId); | ||
|
||
var serializedThread = await this._threadStore.GetOrCreateThreadAsync( | ||
conversationId, | ||
this.Id, | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
if (serializedThread is null) | ||
{ | ||
return null; | ||
} | ||
|
||
return this._innerAgent.DeserializeThread(serializedThread.Value); | ||
} | ||
|
||
/// <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 async ValueTask SaveThreadAsync( | ||
string conversationId, | ||
AgentThread thread, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
_ = Throw.IfNullOrWhitespace(conversationId); | ||
_ = Throw.IfNull(thread); | ||
|
||
await this._threadStore.SaveThreadAsync( | ||
conversationId, | ||
this.Id, | ||
thread, | ||
cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override Task<AgentRunResponse> RunAsync( | ||
IEnumerable<ChatMessage> messages, | ||
AgentThread? thread = null, | ||
AgentRunOptions? options = null, | ||
CancellationToken cancellationToken = default) | ||
=> this._innerAgent.RunAsync(messages, thread, options, cancellationToken); | ||
|
||
/// <inheritdoc/> | ||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync( | ||
IEnumerable<ChatMessage> messages, | ||
AgentThread? thread = null, | ||
AgentRunOptions? options = null, | ||
CancellationToken cancellationToken = default) | ||
=> this._innerAgent.RunStreamingAsync(messages, thread, options, cancellationToken); | ||
} |
62 changes: 62 additions & 0 deletions
62
dotnet/src/Microsoft.Agents.AI.Hosting/HostAgentBuilderExtensions.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 HostAgentBuilderExtensions | ||
{ | ||
/// <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 IHostAgentBuilder WithInMemoryThreadStore(this IHostAgentBuilder 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 IHostAgentBuilder WithThreadStore(this IHostAgentBuilder 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 IHostAgentBuilder WithThreadStore(this IHostAgentBuilder 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; | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
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,51 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.Text.Json; | ||
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 | ||
{ | ||
/// <summary> | ||
/// Saves a serialized agent thread to persistent storage. | ||
/// </summary> | ||
/// <param name="conversationId">The unique identifier for the conversation/thread.</param> | ||
/// <param name="agentId">The identifier of the agent that owns this 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> | ||
/// <remarks> | ||
/// The <paramref name="conversationId"/> is used as the primary key for thread lookup. | ||
/// The <paramref name="agentId"/> provides additional scoping to prevent cross-agent thread access. | ||
/// </remarks> | ||
ValueTask SaveThreadAsync( | ||
string conversationId, | ||
string agentId, | ||
AgentThread thread, | ||
CancellationToken cancellationToken = default); | ||
|
||
/// <summary> | ||
/// Retrieves a serialized agent thread from persistent storage. | ||
/// </summary> | ||
/// <param name="conversationId">The unique identifier for the conversation/thread to retrieve.</param> | ||
/// <param name="agentId">The identifier of the agent that owns this thread.</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<JsonElement?> GetOrCreateThreadAsync( | ||
string conversationId, | ||
string agentId, | ||
CancellationToken cancellationToken = default); | ||
} |
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.
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.
The null-forgiving operator
!
is used onthread
butthread
can be null fromRestoreThreadAsync
. This could cause a NullReferenceException. Check ifthread
is null before saving or handle the null case appropriately.Copilot uses AI. Check for mistakes.