-
Notifications
You must be signed in to change notification settings - Fork 2k
.NET: Add Cosmos DB implementations for ChatMessageStore and CheckpointStore. #1838
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
westey-m
merged 44 commits into
microsoft:main
from
TheovanKraay:csharp-cosmosdb-store-implementations
Nov 26, 2025
Merged
Changes from 5 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
b778a52
draft commit
TheovanKraay 2208d39
Added Cosmos agent thread and tests
TheovanKraay 67c65aa
revert unnecessary changes and fix tests
TheovanKraay d020e18
add multi-tenant support with hierarchical partition keys (and tests).
TheovanKraay 9de2fe8
enhance transactional batch
TheovanKraay b80c59e
address review comments
TheovanKraay 042db62
Address PR review comments from @westey-m
TheovanKraay 405d0ca
Merge upstream/main - resolve slnx conflicts
TheovanKraay 7201533
Merge upstream/main into csharp-cosmosdb-store-implementations
TheovanKraay 7672a76
use param validation helpers
TheovanKraay 394b749
Replace useManagedIdentity boolean with TokenCredential parameter
TheovanKraay 76f300d
Remove redundant suppressions and fix tests
TheovanKraay bbbc651
Rename project from Microsoft.Agents.AI.Abstractions.CosmosNoSql to M…
TheovanKraay 2015a76
Refactor constructors to use chaining pattern
TheovanKraay bea1b47
Reorder deserialization constructor parameters for consistency
TheovanKraay 721a0b0
Remove database/container IDs from serialized state
TheovanKraay c2aae5f
Remove auto-generation of MessageId
TheovanKraay c5ad371
Optimize AddMessagesAsync to avoid enumeration when possible
TheovanKraay ba719f1
Add MaxMessagesToRetrieve to limit context window
TheovanKraay ba431e5
Make Role nullable instead of defaulting
TheovanKraay aef9491
Merge branch 'main' into csharp-cosmosdb-store-implementations
TheovanKraay 03e7621
Fix net472 build without rebasing 19 commits
TheovanKraay 8d0fa99
Add Cosmos DB emulator to CI workflow
TheovanKraay 823b59a
Fix Cosmos DB emulator tests: use Skip.If instead of Assert.Fail and …
TheovanKraay ecc3498
Replace Skip.If() with conditional return to fix compilation
TheovanKraay 7a574c6
Use env var to skip Cosmos tests on non-Windows CI
TheovanKraay 8f0a3ba
Add Xunit.SkippableFact package to properly skip Cosmos tests on Linux
TheovanKraay 76ba62b
Change [Fact] to [SkippableFact] for proper test skipping behavior
TheovanKraay 4862ca4
Remove stale Microsoft.Agents.AI.Abstractions.CosmosNoSql directory
TheovanKraay b5675d6
Fix code formatting: add braces, this. qualifications, and final newl…
TheovanKraay e8c0e27
Fix file encoding to UTF-8 with BOM, fix import ordering, and remove …
TheovanKraay ec0a454
Convert backing fields to auto-properties and remove Azure.Identity u…
TheovanKraay 0b49065
Fix CosmosChatMessageStore.cs encoding back to UTF-8 with BOM
TheovanKraay e6f4b79
Fix test file formatting: indentation, encoding, imports, this. quali…
TheovanKraay 1d32377
Fix const field naming violations: Remove s_ prefix from const fields…
TheovanKraay b40a8ce
Add local .editorconfig for Cosmos DB tests to suppress IDE0005 false…
TheovanKraay 10aecaa
Fix IDE1006 naming violations: Rename TestDatabaseId to s_testDatabas…
TheovanKraay d3200f7
Address PR review comments
TheovanKraay 59b9cf8
Fix IDE0001 formatting error in AgentProviderExtensions.cs. Use type …
TheovanKraay 9c736b5
Merge upstream/main into csharp-cosmosdb-store-implementations
TheovanKraay 108d7ab
Update package versions for Aspire 13.0.0 compatibility
TheovanKraay 4c1a6b1
Merge upstream/main into csharp-cosmosdb-store-implementations
TheovanKraay d472ad4
Fix TargetFrameworks in Cosmos DB projects
TheovanKraay 5f06177
Remove redundant counter, add partition key validation, use factory p…
TheovanKraay 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
233 changes: 233 additions & 0 deletions
233
dotnet/src/Microsoft.Agents.AI.Abstractions/CosmosAgentThread.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,233 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Text.Json; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Azure.Cosmos; | ||
| using Microsoft.Extensions.AI; | ||
|
|
||
| namespace Microsoft.Agents.AI; | ||
|
|
||
| /// <summary> | ||
| /// Provides an abstract base class for agent threads that maintain conversation state in Azure Cosmos DB. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// <see cref="CosmosAgentThread"/> is designed for scenarios where conversation state should be persisted | ||
| /// in Azure Cosmos DB for durability, scalability, and cross-session availability. This approach provides | ||
| /// reliable persistence while maintaining efficient access to conversation data. | ||
| /// </para> | ||
| /// <para> | ||
| /// Cosmos threads persist conversation data across application restarts and can be shared across | ||
| /// multiple application instances. | ||
| /// </para> | ||
| /// </remarks> | ||
| [DebuggerDisplay("{DebuggerDisplay,nq}")] | ||
| public abstract class CosmosAgentThread : AgentThread | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="CosmosAgentThread"/> class. | ||
| /// </summary> | ||
| /// <param name="connectionString">The Cosmos DB connection string.</param> | ||
| /// <param name="databaseId">The identifier of the Cosmos DB database.</param> | ||
| /// <param name="containerId">The identifier of the Cosmos DB container.</param> | ||
| /// <param name="conversationId">The unique identifier for this conversation thread. If null, a new GUID will be generated.</param> | ||
| /// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception> | ||
| /// <remarks> | ||
| /// This constructor creates a new Cosmos DB message store with connection string authentication. | ||
| /// </remarks> | ||
| protected CosmosAgentThread(string connectionString, string databaseId, string containerId, string? conversationId = null) | ||
| { | ||
| this.MessageStore = new CosmosChatMessageStore(connectionString, databaseId, containerId, conversationId ?? Guid.NewGuid().ToString("N")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="CosmosAgentThread"/> class using managed identity. | ||
| /// </summary> | ||
| /// <param name="accountEndpoint">The Cosmos DB account endpoint URI.</param> | ||
| /// <param name="databaseId">The identifier of the Cosmos DB database.</param> | ||
| /// <param name="containerId">The identifier of the Cosmos DB container.</param> | ||
| /// <param name="conversationId">The unique identifier for this conversation thread. If null, a new GUID will be generated.</param> | ||
| /// <param name="useManagedIdentity">Must be true to use this constructor. This parameter distinguishes this constructor from the connection string version.</param> | ||
| /// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace, or when useManagedIdentity is false.</exception> | ||
| /// <remarks> | ||
| /// This constructor creates a new Cosmos DB message store with managed identity authentication. | ||
| /// </remarks> | ||
| protected CosmosAgentThread(string accountEndpoint, string databaseId, string containerId, string? conversationId, bool useManagedIdentity) | ||
| { | ||
| if (!useManagedIdentity) | ||
| { | ||
| throw new ArgumentException("This constructor requires useManagedIdentity to be true.", nameof(useManagedIdentity)); | ||
| } | ||
|
|
||
| this.MessageStore = new CosmosChatMessageStore(accountEndpoint, databaseId, containerId, conversationId ?? Guid.NewGuid().ToString("N"), useManagedIdentity: true); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="CosmosAgentThread"/> class using an existing CosmosClient. | ||
| /// </summary> | ||
| /// <param name="cosmosClient">The <see cref="CosmosClient"/> instance to use for Cosmos DB operations.</param> | ||
| /// <param name="databaseId">The identifier of the Cosmos DB database.</param> | ||
| /// <param name="containerId">The identifier of the Cosmos DB container.</param> | ||
| /// <param name="conversationId">The unique identifier for this conversation thread. If null, a new GUID will be generated.</param> | ||
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="cosmosClient"/> is null.</exception> | ||
| /// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception> | ||
| /// <remarks> | ||
| /// This constructor allows reuse of an existing CosmosClient instance across multiple threads. | ||
| /// </remarks> | ||
| protected CosmosAgentThread(CosmosClient cosmosClient, string databaseId, string containerId, string? conversationId = null) | ||
| { | ||
| this.MessageStore = new CosmosChatMessageStore(cosmosClient, databaseId, containerId, conversationId ?? Guid.NewGuid().ToString("N")); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="CosmosAgentThread"/> class with a pre-configured message store. | ||
| /// </summary> | ||
| /// <param name="messageStore"> | ||
| /// A <see cref="CosmosChatMessageStore"/> instance to use for storing chat messages. | ||
| /// If <see langword="null"/>, an exception will be thrown. | ||
| /// </param> | ||
| /// <exception cref="ArgumentNullException"><paramref name="messageStore"/> is <see langword="null"/>.</exception> | ||
| /// <remarks> | ||
| /// This constructor allows sharing of message stores between threads or providing pre-configured | ||
| /// message stores with specific settings. | ||
| /// </remarks> | ||
| protected CosmosAgentThread(CosmosChatMessageStore messageStore) | ||
| { | ||
| this.MessageStore = messageStore ?? throw new ArgumentNullException(nameof(messageStore)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="CosmosAgentThread"/> class from previously serialized state. | ||
| /// </summary> | ||
| /// <param name="serializedThreadState">A <see cref="JsonElement"/> representing the serialized state of the thread.</param> | ||
| /// <param name="messageStoreFactory"> | ||
| /// Factory function to create the <see cref="CosmosChatMessageStore"/> from its serialized state. | ||
| /// This is required because Cosmos DB connection information cannot be reconstructed from serialized state. | ||
| /// </param> | ||
| /// <param name="jsonSerializerOptions">Optional settings for customizing the JSON deserialization process.</param> | ||
| /// <exception cref="ArgumentException">The <paramref name="serializedThreadState"/> is not a JSON object.</exception> | ||
| /// <exception cref="ArgumentNullException"><paramref name="messageStoreFactory"/> is <see langword="null"/>.</exception> | ||
| /// <exception cref="JsonException">The <paramref name="serializedThreadState"/> is invalid or cannot be deserialized to the expected type.</exception> | ||
| /// <remarks> | ||
| /// This constructor enables restoration of Cosmos threads from previously saved state. Since Cosmos DB | ||
| /// connection information cannot be serialized for security reasons, a factory function must be provided | ||
| /// to reconstruct the message store with appropriate connection details. | ||
| /// </remarks> | ||
| protected CosmosAgentThread( | ||
| JsonElement serializedThreadState, | ||
| Func<JsonElement, JsonSerializerOptions?, CosmosChatMessageStore> messageStoreFactory, | ||
| JsonSerializerOptions? jsonSerializerOptions = null) | ||
| { | ||
| #if NET6_0_OR_GREATER | ||
| ArgumentNullException.ThrowIfNull(messageStoreFactory); | ||
| #else | ||
| if (messageStoreFactory is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(messageStoreFactory)); | ||
| } | ||
| #endif | ||
|
|
||
| if (serializedThreadState.ValueKind != JsonValueKind.Object) | ||
| { | ||
| throw new ArgumentException("The serialized thread state must be a JSON object.", nameof(serializedThreadState)); | ||
| } | ||
|
|
||
| var state = serializedThreadState.Deserialize( | ||
| AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(CosmosAgentThreadState))) as CosmosAgentThreadState; | ||
|
|
||
| this.MessageStore = messageStoreFactory.Invoke(state?.StoreState ?? default, jsonSerializerOptions); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the <see cref="CosmosChatMessageStore"/> used by this thread. | ||
| /// </summary> | ||
| public CosmosChatMessageStore MessageStore { get; } | ||
|
|
||
| /// <summary> | ||
| /// Gets the conversation ID for this thread from the underlying message store. | ||
| /// </summary> | ||
| public string ConversationId => this.MessageStore.ConversationId; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the maximum number of messages to return in a single query batch. | ||
| /// This is delegated to the underlying message store. | ||
| /// </summary> | ||
| public int MaxItemCount | ||
| { | ||
| get => this.MessageStore.MaxItemCount; | ||
| set => this.MessageStore.MaxItemCount = value; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the Time-To-Live (TTL) in seconds for messages. | ||
| /// This is delegated to the underlying message store. | ||
| /// </summary> | ||
| public int? MessageTtlSeconds | ||
| { | ||
| get => this.MessageStore.MessageTtlSeconds; | ||
| set => this.MessageStore.MessageTtlSeconds = value; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options. | ||
| /// </summary> | ||
| /// <param name="jsonSerializerOptions">The JSON serialization options to use.</param> | ||
| /// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns> | ||
| /// <remarks> | ||
| /// Note that connection strings and credentials are not included in the serialized state for security reasons. | ||
| /// When deserializing, you will need to provide connection information through the messageStoreFactory parameter. | ||
| /// </remarks> | ||
| public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) | ||
| { | ||
| var storeState = this.MessageStore.Serialize(jsonSerializerOptions); | ||
|
|
||
| var state = new CosmosAgentThreadState | ||
| { | ||
| StoreState = storeState, | ||
| }; | ||
|
|
||
| return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(CosmosAgentThreadState))); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public override object? GetService(Type serviceType, object? serviceKey = null) => | ||
| base.GetService(serviceType, serviceKey) ?? this.MessageStore?.GetService(serviceType, serviceKey); | ||
|
|
||
| /// <inheritdoc /> | ||
| protected internal override Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default) | ||
| => this.MessageStore.AddMessagesAsync(newMessages, cancellationToken); | ||
|
|
||
| /// <summary> | ||
| /// Gets the total number of messages in this conversation. | ||
| /// This is a Cosmos-specific optimization that provides efficient message counting. | ||
| /// </summary> | ||
| /// <param name="cancellationToken">The cancellation token.</param> | ||
| /// <returns>The total number of messages in the conversation.</returns> | ||
| public async Task<int> GetMessageCountAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| return await this.MessageStore.GetMessageCountAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Clears all messages in this conversation. | ||
| /// This is a Cosmos-specific utility method for conversation cleanup. | ||
| /// </summary> | ||
| /// <param name="cancellationToken">The cancellation token.</param> | ||
| /// <returns>The number of messages that were deleted.</returns> | ||
| public async Task<int> ClearMessagesAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| return await this.MessageStore.ClearMessagesAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| [DebuggerBrowsable(DebuggerBrowsableState.Never)] | ||
| private string DebuggerDisplay => $"ConversationId = {this.ConversationId}"; | ||
|
|
||
| internal sealed class CosmosAgentThreadState | ||
| { | ||
| public JsonElement? StoreState { get; set; } | ||
| } | ||
| } | ||
Oops, something went wrong.
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.