From 6c969418acba0cfa2a0a5bd3b9b95923e49be3e6 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Mon, 3 Nov 2025 19:19:14 +0100 Subject: [PATCH] setup azurestorage proj --- dotnet/Directory.Packages.props | 1 + dotnet/agent-framework-dotnet.slnx | 1 + .../AgentWebChat.AgentHost.csproj | 1 + .../AgentWebChat.AgentHost/Program.cs | 8 + .../Blob/AzureBlobAgentThreadStore.cs | 158 ++++++++++++++++++ .../Blob/AzureBlobAgentThreadStoreOptions.cs | 26 +++ .../HostedAgentBuilderExtensions.Blob.cs | 56 +++++++ ...soft.Agents.AI.Hosting.AzureStorage.csproj | 23 +++ 8 files changed, 274 insertions(+) create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStore.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStoreOptions.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/HostedAgentBuilderExtensions.Blob.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Microsoft.Agents.AI.Hosting.AzureStorage.csproj diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 08532904c4..a643a6cc89 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -21,6 +21,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 95812435d8..de9ab13e9b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -265,6 +265,7 @@ + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj index 802c864c1f..7347a642ae 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj @@ -8,6 +8,7 @@ + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 571b07b1d5..81430070eb 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -3,6 +3,7 @@ using A2A.AspNetCore; using AgentWebChat.AgentHost; using AgentWebChat.AgentHost.Utilities; +using Azure.Storage.Blobs; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Workflows; @@ -27,6 +28,13 @@ builder.AddAIAgent( chatClientServiceKey: "chat-model") .WithInMemoryThreadStore(); +builder.AddAIAgent( + "gambler", + instructions: "You are a gambler. Talk like a gambler.", + description: "An agent which gambles", + chatClientServiceKey: "chat-model") + .WithAzureBlobThreadStore(sp => new BlobContainerClient(connectionString: "UseDevelopmentStorage=true", "agent-threads")); + builder.AddAIAgent("knights-and-knaves", (sp, key) => { var chatClient = sp.GetRequiredKeyedService("chat-model"); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStore.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStore.cs new file mode 100644 index 0000000000..2e14042dac --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStore.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI.Hosting.AzureStorage.Blob; + +internal sealed class AzureBlobAgentThreadStore : AgentThreadStore +{ + private static readonly BlobOpenWriteOptions s_uploadJsonOptions = new() + { + HttpHeaders = new() + { + ContentType = "application/json" + } + }; + + private readonly BlobContainerClient _containerClient; + private readonly AzureBlobAgentThreadStoreOptions _options; + private bool _containerInitialized; + + /// + /// Initializes a new instance of the class using a . + /// + /// The blob container client to use for storage operations. + /// Optional configuration options. If , default options will be used. + /// is . + public AzureBlobAgentThreadStore(BlobContainerClient containerClient, AzureBlobAgentThreadStoreOptions? options = null) + { + this._containerClient = containerClient ?? throw new ArgumentNullException(nameof(containerClient)); + this._options = options ?? new AzureBlobAgentThreadStoreOptions(); + } + + /// + public override async ValueTask SaveThreadAsync( + AIAgent agent, + string conversationId, + AgentThread thread, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agent); + Throw.IfNull(conversationId); + Throw.IfNull(thread); + + await this.EnsureContainerExistsAsync(cancellationToken).ConfigureAwait(false); + + var blobName = this.GetBlobName(agent.Id, conversationId); + var blobClient = this._containerClient.GetBlobClient(blobName); + + JsonElement serializedThread = thread.Serialize(); +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await using Stream stream = await blobClient.OpenWriteAsync(overwrite: true, s_uploadJsonOptions, cancellationToken).ConfigureAwait(false); + await using Utf8JsonWriter writer = new(stream); +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + + serializedThread.WriteTo(writer); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public override async ValueTask GetThreadAsync( + AIAgent agent, + string conversationId, + CancellationToken cancellationToken = default) + { + Throw.IfNull(agent); + Throw.IfNull(conversationId); + + await this.EnsureContainerExistsAsync(cancellationToken).ConfigureAwait(false); + + var blobName = this.GetBlobName(agent.Id, conversationId); + var blobClient = this._containerClient.GetBlobClient(blobName); + + try + { + var stream = await blobClient.OpenReadAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + var jsonDoc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); + var serializedThread = jsonDoc.RootElement; + + return agent.DeserializeThread(serializedThread); + } + catch (RequestFailedException ex) when (ex.Status == 404) + { + // Blob doesn't exist, return a new thread + return agent.GetNewThread(); + } + } + + /// + /// Ensures that the blob container exists, creating it if necessary. + /// + private async Task EnsureContainerExistsAsync(CancellationToken cancellationToken) + { + if (this._containerInitialized) + { + return; + } + + if (!this._containerInitialized) + { + if (this._options.CreateContainerIfNotExists) + { + await this._containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + this._containerInitialized = true; + } + } + + /// + /// Generates the blob name for a given agent and conversation. + /// + private string GetBlobName(string agentId, string conversationId) + { + string sanitizedAgentId = this.SanitizeBlobNameSegment(agentId); + string sanitizedConversationId = this.SanitizeBlobNameSegment(conversationId); + string baseName = $"{sanitizedAgentId}/{sanitizedConversationId}.json"; + + return string.IsNullOrEmpty(this._options.BlobNamePrefix) + ? baseName + : $"{this._options.BlobNamePrefix.TrimEnd('/')}/{baseName}"; + } + + /// + /// Sanitizes a string to be safe for use in blob names. + /// + private string SanitizeBlobNameSegment(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return "default"; + } + + // Replace invalid characters with underscore + StringBuilder builder = new(input.Length); + foreach (char c in input) + { + if (char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.') + { + builder.Append(c); + } + else + { + builder.Append('_'); + } + } + + return builder.ToString(); + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStoreOptions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStoreOptions.cs new file mode 100644 index 0000000000..d7b5cf8dd4 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Blob/AzureBlobAgentThreadStoreOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Agents.AI.Hosting.AzureStorage.Blob; + +/// +/// Configuration options for . +/// +public sealed class AzureBlobAgentThreadStoreOptions +{ + /// + /// Gets or sets a value indicating whether to automatically create the container if it doesn't exist. + /// + /// + /// Defaults to . + /// + public bool CreateContainerIfNotExists { get; set; } = true; + + /// + /// Gets or sets the blob name prefix to use for organizing threads. + /// + /// + /// This can be used to namespace threads within a container. + /// For example, setting this to "prod/" will store all blobs under a "prod/" prefix. + /// + public string? BlobNamePrefix { get; set; } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/HostedAgentBuilderExtensions.Blob.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/HostedAgentBuilderExtensions.Blob.cs new file mode 100644 index 0000000000..463bc47420 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/HostedAgentBuilderExtensions.Blob.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Azure.Storage.Blobs; +using Microsoft.Agents.AI.Hosting.AzureStorage.Blob; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Agents.AI.Hosting; + +/// +/// Provides extension methods for configuring . +/// +public static partial class HostedAgentBuilderExtensions +{ + /// + /// Configures the host agent builder to use an Azure Blob thread store with dependency injection. + /// Resolves from the service provider. + /// + /// The host agent builder to configure with the Azure blob thread store. + /// Optional configuration options for the blob thread store. + /// The same instance, configured to use Azure blob thread store. + /// + /// This overload requires a to be registered in the service collection. + /// Use Azure.Extensions.AspNetCore.Configuration.Secrets or similar to register the client. + /// + public static IHostedAgentBuilder WithAzureBlobThreadStore(this IHostedAgentBuilder builder, AzureBlobAgentThreadStoreOptions? options = null) + => WithAzureBlobThreadStore(builder, sp => sp.GetRequiredKeyedService(builder.Name), options); + + /// + /// Configures the host agent builder to use an Azure Blob thread store for agent thread management. + /// + /// The host agent builder to configure with the Azure blob thread store. + /// The blob container client to use for storage operations. + /// Optional configuration options for the blob thread store. + /// The same instance, configured to use Azure blob thread store. + public static IHostedAgentBuilder WithAzureBlobThreadStore(this IHostedAgentBuilder builder, BlobContainerClient containerClient, AzureBlobAgentThreadStoreOptions? options = null) + => WithAzureBlobThreadStore(builder, sp => containerClient, options); + + /// + /// Configures the agent builder to use Azure Blob Storage as the thread store for agent state persistence. + /// + /// The agent builder to configure with Azure Blob thread store support. + /// A factory function that provides a configured BlobContainerClient instance for accessing the Azure Blob + /// container used to store thread data. + /// Optional settings for customizing the Azure Blob thread store behavior. If null, default options are used. + /// The same agent builder instance, configured to use Azure Blob Storage for thread persistence. + public static IHostedAgentBuilder WithAzureBlobThreadStore( + this IHostedAgentBuilder builder, + Func createBlobContainer, + AzureBlobAgentThreadStoreOptions? options = null) + => builder.WithThreadStore((sp, key) => + { + var blobContainer = createBlobContainer(sp); + return new AzureBlobAgentThreadStore(blobContainer, options); + }); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Microsoft.Agents.AI.Hosting.AzureStorage.csproj b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Microsoft.Agents.AI.Hosting.AzureStorage.csproj new file mode 100644 index 0000000000..d6a1548212 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureStorage/Microsoft.Agents.AI.Hosting.AzureStorage.csproj @@ -0,0 +1,23 @@ + + + + $(ProjectsTargetFrameworks) + net9.0 + preview + true + + + Microsoft Agent Framework AzureStorage + Provides AzureStorage integration with Microsoft Agent Framework. + + + + + + + + + + + +