From 94f00666eb6c1e1d2a2f1002528bd39c0d95bddb Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Thu, 29 Jan 2026 21:32:27 +0000
Subject: [PATCH 1/8] Add Azure AI Foundry Memory Context Provider with unit
tests
---
dotnet/agent-framework-dotnet.slnx | 2 +
.../AIProjectClientExtensions.cs | 113 ++++++
.../AIProjectClientMemoryOperations.cs | 50 +++
.../Core/Models/DeleteScopeRequest.cs | 17 +
.../Core/Models/MemoryInputMessage.cs | 23 ++
.../Core/Models/MemoryItem.cs | 29 ++
.../Core/Models/MemorySearchResult.cs | 23 ++
.../Core/Models/SearchMemoriesOptions.cs | 17 +
.../Core/Models/SearchMemoriesRequest.cs | 29 ++
.../Core/Models/SearchMemoriesResponse.cs | 23 ++
.../Core/Models/UpdateMemoriesRequest.cs | 35 ++
.../Core/Models/UpdateMemoriesResponse.cs | 23 ++
.../FoundryMemoryJsonUtilities.cs | 48 +++
.../FoundryMemoryProvider.cs | 314 +++++++++++++++
.../FoundryMemoryProviderOptions.cs | 45 +++
.../FoundryMemoryProviderScope.cs | 41 ++
.../IFoundryMemoryOperations.cs | 42 ++
.../Microsoft.Agents.AI.FoundryMemory.csproj | 37 ++
.../FoundryMemoryProviderTests.cs | 369 ++++++++++++++++++
...t.Agents.AI.FoundryMemory.UnitTests.csproj | 16 +
20 files changed, 1296 insertions(+)
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
create mode 100644 dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 280632bc4b..027f453493 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -411,6 +411,7 @@
+
@@ -454,6 +455,7 @@
+
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
new file mode 100644
index 0000000000..4ee323bc5a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Extension methods for to provide MemoryStores operations
+/// using the SDK's HTTP pipeline until the SDK releases convenience methods.
+///
+internal static class AIProjectClientExtensions
+{
+ ///
+ /// Searches for relevant memories from a memory store based on conversation context.
+ ///
+ /// The AI Project client.
+ /// The name of the memory store to search.
+ /// The namespace that logically groups and isolates memories, such as a user ID.
+ /// The conversation messages to use for the search query.
+ /// Maximum number of memories to return.
+ /// Cancellation token.
+ /// Enumerable of memory content strings.
+ public static async Task> SearchMemoriesAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int maxMemories,
+ CancellationToken cancellationToken)
+ {
+ var request = new SearchMemoriesRequest
+ {
+ Scope = scope,
+ Items = messages.ToArray(),
+ Options = new SearchMemoriesOptions { MaxMemories = maxMemories }
+ };
+
+ var json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.SearchMemoriesRequest);
+ var content = BinaryContent.Create(BinaryData.FromString(json));
+
+ var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
+ ClientResult result = await client.MemoryStores.SearchMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+
+ var response = JsonSerializer.Deserialize(
+ result.GetRawResponse().Content.ToString(),
+ FoundryMemoryJsonContext.Default.SearchMemoriesResponse);
+
+ return response?.Memories?.Select(m => m.MemoryItem?.Content ?? string.Empty)
+ .Where(c => !string.IsNullOrWhiteSpace(c)) ?? [];
+ }
+
+ ///
+ /// Updates memory store with conversation memories.
+ ///
+ /// The AI Project client.
+ /// The name of the memory store to update.
+ /// The namespace that logically groups and isolates memories, such as a user ID.
+ /// The conversation messages to extract memories from.
+ /// Delay in seconds before processing the update.
+ /// Cancellation token.
+ public static async Task UpdateMemoriesAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int updateDelay,
+ CancellationToken cancellationToken)
+ {
+ var request = new UpdateMemoriesRequest
+ {
+ Scope = scope,
+ Items = messages.ToArray(),
+ UpdateDelay = updateDelay
+ };
+
+ var json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.UpdateMemoriesRequest);
+ var content = BinaryContent.Create(BinaryData.FromString(json));
+
+ var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
+ await client.MemoryStores.UpdateMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+ }
+
+ ///
+ /// Deletes all memories associated with a specific scope from a memory store.
+ ///
+ /// The AI Project client.
+ /// The name of the memory store.
+ /// The namespace that logically groups and isolates memories to delete, such as a user ID.
+ /// Cancellation token.
+ public static async Task DeleteScopeAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string scope,
+ CancellationToken cancellationToken)
+ {
+ var request = new DeleteScopeRequest { Scope = scope };
+
+ var json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.DeleteScopeRequest);
+ var content = BinaryContent.Create(BinaryData.FromString(json));
+
+ var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
+ await client.MemoryStores.DeleteScopeAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs
new file mode 100644
index 0000000000..3a3cecaac9
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Implementation of using .
+///
+internal sealed class AIProjectClientMemoryOperations : IFoundryMemoryOperations
+{
+ private readonly AIProjectClient _client;
+
+ public AIProjectClientMemoryOperations(AIProjectClient client)
+ {
+ this._client = client;
+ }
+
+ public Task> SearchMemoriesAsync(
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int maxMemories,
+ CancellationToken cancellationToken)
+ {
+ return this._client.SearchMemoriesAsync(memoryStoreName, scope, messages, maxMemories, cancellationToken);
+ }
+
+ public Task UpdateMemoriesAsync(
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int updateDelay,
+ CancellationToken cancellationToken)
+ {
+ return this._client.UpdateMemoriesAsync(memoryStoreName, scope, messages, updateDelay, cancellationToken);
+ }
+
+ public Task DeleteScopeAsync(
+ string memoryStoreName,
+ string scope,
+ CancellationToken cancellationToken)
+ {
+ return this._client.DeleteScopeAsync(memoryStoreName, scope, cancellationToken);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs
new file mode 100644
index 0000000000..48e1363efe
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/DeleteScopeRequest.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for the delete scope API.
+///
+internal sealed class DeleteScopeRequest
+{
+ ///
+ /// Gets or sets the scope to delete.
+ ///
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs
new file mode 100644
index 0000000000..e34a3d8f74
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryInputMessage.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Represents an input message for the memory API.
+///
+internal sealed class MemoryInputMessage
+{
+ ///
+ /// Gets or sets the role of the message (e.g., "user", "assistant", "system").
+ ///
+ [JsonPropertyName("role")]
+ public string Role { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the content of the message.
+ ///
+ [JsonPropertyName("content")]
+ public string Content { get; set; } = string.Empty;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs
new file mode 100644
index 0000000000..04e2c413f6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryItem.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Represents a memory item.
+///
+internal sealed class MemoryItem
+{
+ ///
+ /// Gets or sets the unique identifier of the memory.
+ ///
+ [JsonPropertyName("memory_id")]
+ public string? MemoryId { get; set; }
+
+ ///
+ /// Gets or sets the content of the memory.
+ ///
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ ///
+ /// Gets or sets the type of the memory.
+ ///
+ [JsonPropertyName("memory_type")]
+ public string? MemoryType { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs
new file mode 100644
index 0000000000..0e841060ba
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemorySearchResult.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Represents a memory search result.
+///
+internal sealed class MemorySearchResult
+{
+ ///
+ /// Gets or sets the memory item.
+ ///
+ [JsonPropertyName("memory_item")]
+ public MemoryItem? MemoryItem { get; set; }
+
+ ///
+ /// Gets or sets the relevance score.
+ ///
+ [JsonPropertyName("score")]
+ public double Score { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs
new file mode 100644
index 0000000000..cf12335083
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesOptions.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Options for searching memories.
+///
+internal sealed class SearchMemoriesOptions
+{
+ ///
+ /// Gets or sets the maximum number of memories to return.
+ ///
+ [JsonPropertyName("max_memories")]
+ public int MaxMemories { get; set; } = 5;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs
new file mode 100644
index 0000000000..74ee62d9bc
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesRequest.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for the search memories API.
+///
+internal sealed class SearchMemoriesRequest
+{
+ ///
+ /// Gets or sets the namespace that logically groups and isolates memories, such as a user ID.
+ ///
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the conversation messages to use for the search query.
+ ///
+ [JsonPropertyName("items")]
+ public MemoryInputMessage[] Items { get; set; } = [];
+
+ ///
+ /// Gets or sets the search options.
+ ///
+ [JsonPropertyName("options")]
+ public SearchMemoriesOptions? Options { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs
new file mode 100644
index 0000000000..f3811c1461
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/SearchMemoriesResponse.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Response from the search memories API.
+///
+internal sealed class SearchMemoriesResponse
+{
+ ///
+ /// Gets or sets the unique identifier for the search operation.
+ ///
+ [JsonPropertyName("search_id")]
+ public string? SearchId { get; set; }
+
+ ///
+ /// Gets or sets the list of retrieved memories.
+ ///
+ [JsonPropertyName("memories")]
+ public MemorySearchResult[]? Memories { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs
new file mode 100644
index 0000000000..a356059e19
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesRequest.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for the update memories API.
+///
+internal sealed class UpdateMemoriesRequest
+{
+ ///
+ /// Gets or sets the namespace that logically groups and isolates memories, such as a user ID.
+ ///
+ [JsonPropertyName("scope")]
+ public string Scope { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the conversation messages to extract memories from.
+ ///
+ [JsonPropertyName("items")]
+ public MemoryInputMessage[] Items { get; set; } = [];
+
+ ///
+ /// Gets or sets the delay in seconds before processing the update.
+ ///
+ [JsonPropertyName("update_delay")]
+ public int UpdateDelay { get; set; }
+
+ ///
+ /// Gets or sets the ID of a previous update operation to chain with.
+ ///
+ [JsonPropertyName("previous_update_id")]
+ public string? PreviousUpdateId { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
new file mode 100644
index 0000000000..f69474d89d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Response from the update memories API.
+///
+internal sealed class UpdateMemoriesResponse
+{
+ ///
+ /// Gets or sets the unique identifier of the update operation.
+ ///
+ [JsonPropertyName("update_id")]
+ public string? UpdateId { get; set; }
+
+ ///
+ /// Gets or sets the status of the update operation.
+ ///
+ [JsonPropertyName("status")]
+ public string? Status { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
new file mode 100644
index 0000000000..8250b12a3f
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Provides JSON serialization utilities for the Foundry Memory provider.
+///
+internal static class FoundryMemoryJsonUtilities
+{
+ ///
+ /// Gets the default JSON serializer options for Foundry Memory operations.
+ ///
+ public static JsonSerializerOptions DefaultOptions { get; } = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = false,
+ TypeInfoResolver = FoundryMemoryJsonContext.Default
+ };
+}
+
+///
+/// Source-generated JSON serialization context for Foundry Memory types.
+///
+[JsonSourceGenerationOptions(
+ JsonSerializerDefaults.General,
+ UseStringEnumConverter = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ WriteIndented = false)]
+[JsonSerializable(typeof(FoundryMemoryProviderScope))]
+[JsonSerializable(typeof(FoundryMemoryProvider.FoundryMemoryState))]
+[JsonSerializable(typeof(SearchMemoriesRequest))]
+[JsonSerializable(typeof(SearchMemoriesResponse))]
+[JsonSerializable(typeof(SearchMemoriesOptions))]
+[JsonSerializable(typeof(UpdateMemoriesRequest))]
+[JsonSerializable(typeof(UpdateMemoriesResponse))]
+[JsonSerializable(typeof(DeleteScopeRequest))]
+[JsonSerializable(typeof(MemoryInputMessage))]
+[JsonSerializable(typeof(MemoryInputMessage[]))]
+[JsonSerializable(typeof(MemorySearchResult))]
+[JsonSerializable(typeof(MemorySearchResult[]))]
+[JsonSerializable(typeof(MemoryItem))]
+internal partial class FoundryMemoryJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
new file mode 100644
index 0000000000..ef0afda93d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -0,0 +1,314 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Provides an Azure AI Foundry Memory backed that persists conversation messages as memories
+/// and retrieves related memories to augment the agent invocation context.
+///
+///
+/// The provider stores user, assistant and system messages as Foundry memories and retrieves relevant memories
+/// for new invocations using the memory search endpoint. Retrieved memories are injected as user messages
+/// to the model, prefixed by a configurable context prompt.
+///
+public sealed class FoundryMemoryProvider : AIContextProvider
+{
+ private const string DefaultContextPrompt = "## Memories\nConsider the following memories when answering user questions:";
+
+ private readonly string _contextPrompt;
+ private readonly string _memoryStoreName;
+ private readonly int _maxMemories;
+ private readonly int _updateDelay;
+ private readonly bool _enableSensitiveTelemetryData;
+
+ private readonly IFoundryMemoryOperations _operations;
+ private readonly ILogger? _logger;
+
+ private readonly FoundryMemoryProviderScope _scope;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Azure AI Project client configured for your Foundry project.
+ /// The scope configuration for memory storage and retrieval.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
+ FoundryMemoryProviderScope scope,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ : this(new AIProjectClientMemoryOperations(Throw.IfNull(client)), scope, options, loggerFactory)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class, with existing state from a serialized JSON element.
+ ///
+ /// The Azure AI Project client configured for your Foundry project.
+ /// A representing the serialized state of the provider.
+ /// Optional settings for customizing the JSON deserialization process.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
+ JsonElement serializedState,
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ : this(new AIProjectClientMemoryOperations(Throw.IfNull(client)), serializedState, jsonSerializerOptions, options, loggerFactory)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a custom operations implementation.
+ ///
+ /// This constructor enables testability by allowing injection of mock operations.
+ internal FoundryMemoryProvider(
+ IFoundryMemoryOperations operations,
+ FoundryMemoryProviderScope scope,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Throw.IfNull(operations);
+ Throw.IfNull(scope);
+
+ if (string.IsNullOrWhiteSpace(scope.Scope))
+ {
+ throw new ArgumentException("The Scope property must be provided.", nameof(scope));
+ }
+
+ var effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+
+ if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
+ {
+ throw new ArgumentException("The MemoryStoreName option must be provided.", nameof(options));
+ }
+
+ this._logger = loggerFactory?.CreateLogger();
+ this._operations = operations;
+
+ this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
+ this._memoryStoreName = effectiveOptions.MemoryStoreName;
+ this._maxMemories = effectiveOptions.MaxMemories;
+ this._updateDelay = effectiveOptions.UpdateDelay;
+ this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
+ this._scope = new FoundryMemoryProviderScope(scope);
+ }
+
+ ///
+ /// Initializes a new instance of the class, with existing state from a serialized JSON element.
+ ///
+ /// This constructor enables testability by allowing injection of mock operations.
+ internal FoundryMemoryProvider(
+ IFoundryMemoryOperations operations,
+ JsonElement serializedState,
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ FoundryMemoryProviderOptions? options = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ Throw.IfNull(operations);
+
+ var effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+
+ if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
+ {
+ throw new ArgumentException("The MemoryStoreName option must be provided.", nameof(options));
+ }
+
+ this._logger = loggerFactory?.CreateLogger();
+ this._operations = operations;
+
+ this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
+ this._memoryStoreName = effectiveOptions.MemoryStoreName;
+ this._maxMemories = effectiveOptions.MaxMemories;
+ this._updateDelay = effectiveOptions.UpdateDelay;
+ this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
+
+ var jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ var state = serializedState.Deserialize(jso.GetTypeInfo(typeof(FoundryMemoryState))) as FoundryMemoryState;
+
+ if (state?.Scope == null || string.IsNullOrWhiteSpace(state.Scope.Scope))
+ {
+ throw new InvalidOperationException("The FoundryMemoryProvider state did not contain the required scope property.");
+ }
+
+ this._scope = state.Scope;
+ }
+
+ ///
+ public override async ValueTask InvokingAsync(InvokingContext context, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(context);
+
+#pragma warning disable CA1308 // Lowercase required by service
+ var messageItems = context.RequestMessages
+ .Where(m => !string.IsNullOrWhiteSpace(m.Text))
+ .Select(m => new MemoryInputMessage
+ {
+ Role = m.Role.Value.ToLowerInvariant(),
+ Content = m.Text!
+ })
+ .ToArray();
+#pragma warning restore CA1308
+
+ if (messageItems.Length == 0)
+ {
+ return new AIContext();
+ }
+
+ try
+ {
+ var memories = (await this._operations.SearchMemoriesAsync(
+ this._memoryStoreName,
+ this._scope.Scope!,
+ messageItems,
+ this._maxMemories,
+ cancellationToken).ConfigureAwait(false)).ToList();
+
+ var outputMessageText = memories.Count == 0
+ ? null
+ : $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
+
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Retrieved {Count} memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ memories.Count,
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+
+ if (outputMessageText is not null && this._logger.IsEnabled(LogLevel.Trace))
+ {
+ this._logger.LogTrace(
+ "FoundryMemoryProvider: Search Results\nOutput:{MessageText}\nMemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this.SanitizeLogData(outputMessageText),
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+
+ return new AIContext
+ {
+ Messages = [new ChatMessage(ChatRole.User, outputMessageText)]
+ };
+ }
+ catch (ArgumentException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Error) is true)
+ {
+ this._logger.LogError(
+ ex,
+ "FoundryMemoryProvider: Failed to search for memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ return new AIContext();
+ }
+ }
+
+ ///
+ public override async ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
+ {
+ if (context.InvokeException is not null)
+ {
+ return; // Do not update memory on failed invocations.
+ }
+
+ try
+ {
+#pragma warning disable CA1308 // Lowercase required by service
+ var messageItems = context.RequestMessages
+ .Concat(context.ResponseMessages ?? [])
+ .Where(m => IsAllowedRole(m.Role) && !string.IsNullOrWhiteSpace(m.Text))
+ .Select(m => new MemoryInputMessage
+ {
+ Role = m.Role.Value.ToLowerInvariant(),
+ Content = m.Text!
+ })
+ .ToArray();
+#pragma warning restore CA1308
+
+ if (messageItems.Length == 0)
+ {
+ return;
+ }
+
+ await this._operations.UpdateMemoriesAsync(
+ this._memoryStoreName,
+ this._scope.Scope!,
+ messageItems,
+ this._updateDelay,
+ cancellationToken).ConfigureAwait(false);
+
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Sent {Count} messages to update memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ messageItems.Length,
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ catch (Exception ex)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Error) is true)
+ {
+ this._logger.LogError(
+ ex,
+ "FoundryMemoryProvider: Failed to send messages to update memories due to error. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ }
+
+ ///
+ /// Clears all stored memories for the configured scope.
+ ///
+ /// Cancellation token.
+ public Task ClearStoredMemoriesAsync(CancellationToken cancellationToken = default) =>
+ this._operations.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken);
+
+ ///
+ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
+ {
+ var state = new FoundryMemoryState(this._scope);
+
+ var jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(FoundryMemoryState)));
+ }
+
+ private static bool IsAllowedRole(ChatRole role) =>
+ role == ChatRole.User || role == ChatRole.Assistant || role == ChatRole.System;
+
+ private string? SanitizeLogData(string? data) => this._enableSensitiveTelemetryData ? data : "";
+
+ internal sealed class FoundryMemoryState
+ {
+ [JsonConstructor]
+ public FoundryMemoryState(FoundryMemoryProviderScope scope)
+ {
+ this.Scope = scope;
+ }
+
+ public FoundryMemoryProviderScope Scope { get; set; }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
new file mode 100644
index 0000000000..b6b157431d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderOptions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Options for configuring the .
+///
+public sealed class FoundryMemoryProviderOptions
+{
+ ///
+ /// Gets or sets the name of the pre-existing memory store in Azure AI Foundry.
+ ///
+ ///
+ /// The memory store must be created in your Azure AI Foundry project before using this provider.
+ ///
+ public string? MemoryStoreName { get; set; }
+
+ ///
+ /// When providing memories to the model, this string is prefixed to the retrieved memories to supply context.
+ ///
+ /// Defaults to "## Memories\nConsider the following memories when answering user questions:".
+ public string? ContextPrompt { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of memories to retrieve during search.
+ ///
+ /// Defaults to 5.
+ public int MaxMemories { get; set; } = 5;
+
+ ///
+ /// Gets or sets the delay in seconds before memory updates are processed.
+ ///
+ ///
+ /// Setting to 0 triggers updates immediately without waiting for inactivity.
+ /// Higher values allow the service to batch multiple updates together.
+ ///
+ /// Defaults to 0 (immediate).
+ public int UpdateDelay { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether sensitive data such as user ids and user messages may appear in logs.
+ ///
+ /// Defaults to .
+ public bool EnableSensitiveTelemetryData { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
new file mode 100644
index 0000000000..542e5bf995
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProviderScope.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Allows scoping of memories for the .
+///
+///
+/// Azure AI Foundry memories are scoped by a single string identifier that you control.
+/// Common patterns include using a user ID, team ID, or other unique identifier
+/// to partition memories across different contexts.
+///
+public sealed class FoundryMemoryProviderScope
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FoundryMemoryProviderScope() { }
+
+ ///
+ /// Initializes a new instance of the class by cloning an existing scope.
+ ///
+ /// The scope to clone.
+ public FoundryMemoryProviderScope(FoundryMemoryProviderScope sourceScope)
+ {
+ Throw.IfNull(sourceScope);
+ this.Scope = sourceScope.Scope;
+ }
+
+ ///
+ /// Gets or sets the scope identifier used to partition memories.
+ ///
+ ///
+ /// This value controls how memory is partitioned in the memory store.
+ /// Each unique scope maintains its own isolated collection of memory items.
+ /// For example, use a user ID to ensure each user has their own individual memory.
+ ///
+ public string? Scope { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs
new file mode 100644
index 0000000000..af7fd6822c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+namespace Microsoft.Agents.AI.FoundryMemory;
+
+///
+/// Interface for Foundry Memory operations, enabling testability.
+///
+internal interface IFoundryMemoryOperations
+{
+ ///
+ /// Searches for relevant memories from a memory store based on conversation context.
+ ///
+ Task> SearchMemoriesAsync(
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int maxMemories,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Updates memory store with conversation memories.
+ ///
+ Task UpdateMemoriesAsync(
+ string memoryStoreName,
+ string scope,
+ IEnumerable messages,
+ int updateDelay,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Deletes all memories associated with a specific scope from a memory store.
+ ///
+ Task DeleteScopeAsync(
+ string memoryStoreName,
+ string scope,
+ CancellationToken cancellationToken);
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
new file mode 100644
index 0000000000..0aa2dbb3e6
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
@@ -0,0 +1,37 @@
+
+
+
+ preview
+
+
+
+ true
+ true
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ Microsoft Agent Framework - Azure AI Foundry Memory integration
+ Provides Azure AI Foundry Memory integration for Microsoft Agent Framework.
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
new file mode 100644
index 0000000000..a9ac8c585c
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
@@ -0,0 +1,369 @@
+// 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.Agents.AI.FoundryMemory.Core.Models;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Logging;
+using Moq;
+
+namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
+
+///
+/// Tests for .
+///
+public sealed class FoundryMemoryProviderTests
+{
+ private readonly Mock _operationsMock;
+ private readonly Mock> _loggerMock;
+ private readonly Mock _loggerFactoryMock;
+
+ public FoundryMemoryProviderTests()
+ {
+ this._operationsMock = new();
+ this._loggerMock = new();
+ this._loggerFactoryMock = new();
+ this._loggerFactoryMock
+ .Setup(f => f.CreateLogger(It.IsAny()))
+ .Returns(this._loggerMock.Object);
+ this._loggerFactoryMock
+ .Setup(f => f.CreateLogger(typeof(FoundryMemoryProvider).FullName!))
+ .Returns(this._loggerMock.Object);
+
+ this._loggerMock
+ .Setup(f => f.IsEnabled(It.IsAny()))
+ .Returns(true);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenOperationsIsNull()
+ {
+ // Act & Assert
+ var ex = Assert.Throws(() => new FoundryMemoryProvider(
+ (IFoundryMemoryOperations)null!,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("operations", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenScopeIsNull()
+ {
+ // Act & Assert
+ var ex = Assert.Throws(() => new FoundryMemoryProvider(
+ this._operationsMock.Object,
+<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
+ (FoundryMemoryProviderScope)null!,
+=======
+ null!,
+>>>>>>> After
+
+ null!,
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("scope", ex.ParamName);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenScopeValueIsEmpty()
+ {
+ // Act & Assert
+ var ex = Assert.Throws(() => new FoundryMemoryProvider(
+ this._operationsMock.Object,
+ new FoundryMemoryProviderScope(),
+ new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The Scope property must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void Constructor_Throws_WhenMemoryStoreNameIsMissing()
+ {
+ // Act & Assert
+ var ex = Assert.Throws(() => new FoundryMemoryProvider(
+ this._operationsMock.Object,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ new FoundryMemoryProviderOptions()));
+ Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
+ }
+
+ [Fact]
+ public void DeserializingConstructor_Throws_WithEmptyJsonElement()
+ {
+ // Arrange
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(new object(), FoundryMemoryJsonUtilities.DefaultOptions);
+
+ // Act & Assert
+ var ex = Assert.Throws(() => new FoundryMemoryProvider(
+ this._operationsMock.Object,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
+ }
+
+ [Fact]
+ public async Task InvokingAsync_PerformsSearch_AndReturnsContextMessageAsync()
+ {
+ // Arrange
+ this._operationsMock
+ .Setup(o => o.SearchMemoriesAsync(
+ "my-store",
+ "user-123",
+ It.IsAny>(),
+ 5,
+ It.IsAny()))
+ .ReturnsAsync(["User prefers dark roast coffee", "User is from Seattle"]);
+
+ FoundryMemoryProviderScope scope = new() { Scope = "user-123" };
+ FoundryMemoryProviderOptions options = new()
+ {
+ MemoryStoreName = "my-store",
+ EnableSensitiveTelemetryData = true
+ };
+
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, scope, options);
+ AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "What are my coffee preferences?")]);
+
+ // Act
+ AIContext aiContext = await sut.InvokingAsync(invokingContext);
+
+ // Assert
+ this._operationsMock.Verify(
+ o => o.SearchMemoriesAsync("my-store", "user-123", It.IsAny>(), 5, It.IsAny()),
+ Times.Once);
+
+ Assert.NotNull(aiContext.Messages);
+ ChatMessage contextMessage = Assert.Single(aiContext.Messages);
+ Assert.Equal(ChatRole.User, contextMessage.Role);
+ Assert.Contains("User prefers dark roast coffee", contextMessage.Text);
+ Assert.Contains("User is from Seattle", contextMessage.Text);
+ }
+
+ [Fact]
+ public async Task InvokingAsync_ReturnsEmptyContext_WhenNoMemoriesFoundAsync()
+ {
+ // Arrange
+ this._operationsMock
+ .Setup(o => o.SearchMemoriesAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(Array.Empty());
+
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+ AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "Hello")]);
+
+ // Act
+ AIContext aiContext = await sut.InvokingAsync(invokingContext);
+
+ // Assert
+ Assert.NotNull(aiContext.Messages);
+ ChatMessage contextMessage = Assert.Single(aiContext.Messages);
+ Assert.True(string.IsNullOrEmpty(contextMessage.Text)); // Text is null or empty when no memories found
+ }
+
+ [Fact]
+ public async Task InvokingAsync_ShouldNotThrow_WhenSearchFailsAsync()
+ {
+ // Arrange
+ this._operationsMock
+ .Setup(o => o.SearchMemoriesAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ThrowsAsync(new InvalidOperationException("Search failed"));
+
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" }, this._loggerFactoryMock.Object);
+ AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "Q?")]);
+
+ // Act
+ AIContext aiContext = await sut.InvokingAsync(invokingContext, CancellationToken.None);
+
+ // Assert
+ Assert.Null(aiContext.Messages);
+ Assert.Null(aiContext.Tools);
+ this._loggerMock.Verify(
+ l => l.Log(
+ LogLevel.Error,
+ It.IsAny(),
+ It.Is((v, t) => v.ToString()!.Contains("FoundryMemoryProvider: Failed to search for memories due to error")),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task InvokedAsync_PersistsAllowedMessagesAsync()
+ {
+ // Arrange
+ IEnumerable? capturedMessages = null;
+ this._operationsMock
+ .Setup(o => o.UpdateMemoriesAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .Callback, int, CancellationToken>((_, _, msgs, _, _) => capturedMessages = msgs)
+ .Returns(Task.CompletedTask);
+
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ List requestMessages =
+ [
+ new(ChatRole.User, "User text"),
+ new(ChatRole.System, "System text"),
+ new(ChatRole.Tool, "Tool text should be ignored")
+ ];
+ List responseMessages = [new(ChatRole.Assistant, "Assistant text")];
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
+
+ // Assert
+ this._operationsMock.Verify(
+ o => o.UpdateMemoriesAsync("my-store", "user-123", It.IsAny>(), 0, It.IsAny()),
+ Times.Once);
+
+ Assert.NotNull(capturedMessages);
+ List messagesList = [.. capturedMessages];
+ Assert.Equal(3, messagesList.Count); // user, system, assistant (tool excluded)
+ Assert.Contains(messagesList, m => m.Role == "user" && m.Content == "User text");
+ Assert.Contains(messagesList, m => m.Role == "system" && m.Content == "System text");
+ Assert.Contains(messagesList, m => m.Role == "assistant" && m.Content == "Assistant text");
+ Assert.DoesNotContain(messagesList, m => m.Content == "Tool text should be ignored");
+ }
+
+ [Fact]
+ public async Task InvokedAsync_PersistsNothingForFailedRequestAsync()
+ {
+ // Arrange
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ List requestMessages =
+ [
+ new(ChatRole.User, "User text"),
+ new(ChatRole.System, "System text")
+ ];
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null)
+ {
+ ResponseMessages = null,
+ InvokeException = new InvalidOperationException("Request Failed")
+ });
+
+ // Assert
+ this._operationsMock.Verify(
+ o => o.UpdateMemoriesAsync(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()),
+ Times.Never);
+ }
+
+ [Fact]
+ public async Task InvokedAsync_ShouldNotThrow_WhenStorageFailsAsync()
+ {
+ // Arrange
+ this._operationsMock
+ .Setup(o => o.UpdateMemoriesAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ThrowsAsync(new InvalidOperationException("Storage failed"));
+
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" }, this._loggerFactoryMock.Object);
+
+ List requestMessages = [new(ChatRole.User, "User text")];
+ List responseMessages = [new(ChatRole.Assistant, "Assistant text")];
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
+
+ // Assert
+ this._loggerMock.Verify(
+ l => l.Log(
+ LogLevel.Error,
+ It.IsAny(),
+ It.Is((v, t) => v.ToString()!.Contains("FoundryMemoryProvider: Failed to send messages to update memories due to error")),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task ClearStoredMemoriesAsync_SendsDeleteRequestAsync()
+ {
+ // Arrange
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act
+ await sut.ClearStoredMemoriesAsync();
+
+ // Assert
+ this._operationsMock.Verify(
+ o => o.DeleteScopeAsync("my-store", "user-123", It.IsAny()),
+ Times.Once);
+ }
+
+ [Fact]
+ public void Serialize_RoundTripsScope()
+ {
+ // Arrange
+ FoundryMemoryProviderScope scope = new() { Scope = "user-456" };
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, scope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act
+ JsonElement stateElement = sut.Serialize();
+ using JsonDocument doc = JsonDocument.Parse(stateElement.GetRawText());
+
+ // Assert (JSON uses camelCase naming policy)
+ Assert.True(doc.RootElement.TryGetProperty("scope", out JsonElement scopeElement));
+ Assert.Equal("user-456", scopeElement.GetProperty("scope").GetString());
+ }
+
+ [Theory]
+ [InlineData(true, "user-123")]
+ [InlineData(false, "")]
+ public async Task InvokingAsync_LogsScopeBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, string expectedScopeInLog)
+ {
+ // Arrange
+ this._operationsMock
+ .Setup(o => o.SearchMemoriesAsync(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>(),
+ It.IsAny(),
+ It.IsAny()))
+ .ReturnsAsync(["test memory"]);
+
+ FoundryMemoryProviderOptions options = new()
+ {
+ MemoryStoreName = "my-store",
+ EnableSensitiveTelemetryData = enableSensitiveTelemetryData
+ };
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, options, this._loggerFactoryMock.Object);
+
+ AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "test")]);
+
+ // Act
+ await sut.InvokingAsync(invokingContext, CancellationToken.None);
+
+ // Assert
+ this._loggerMock.Verify(
+ l => l.Log(
+ LogLevel.Information,
+ It.IsAny(),
+ It.Is((v, t) =>
+ v.ToString()!.Contains("Retrieved 1 memories") &&
+ v.ToString()!.Contains($"Scope: '{expectedScopeInLog}'")),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
new file mode 100644
index 0000000000..9973e6f247
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
From dfde827c1fd67e8fce22044e836dc4a8f35b28a3 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Fri, 30 Jan 2026 11:26:26 +0000
Subject: [PATCH 2/8] Add FoundryMemory integration tests and sample
application
---
dotnet/agent-framework-dotnet.slnx | 2 +
...ithMemory_Step04_MemoryUsingFoundry.csproj | 23 +++
.../Program.cs | 61 ++++++
.../README.md | 64 ++++++
.../GettingStarted/AgentWithMemory/README.md | 1 +
.../FoundryMemoryProvider.cs | 1 -
.../FoundryMemoryConfiguration.cs | 12 ++
.../FoundryMemoryProviderTests.cs | 190 ++++++++++++++++++
...s.AI.FoundryMemory.IntegrationTests.csproj | 20 ++
.../FoundryMemoryProviderTests.cs | 8 +-
10 files changed, 374 insertions(+), 8 deletions(-)
create mode 100644 dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
create mode 100644 dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
create mode 100644 dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 027f453493..50cadefae1 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -137,6 +137,7 @@
+
@@ -433,6 +434,7 @@
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
new file mode 100644
index 0000000000..7a576946ae
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
new file mode 100644
index 0000000000..537891af5b
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
+// The sample stores conversation messages in an Azure AI Foundry memory store and retrieves relevant
+// memories for subsequent invocations, even across new sessions.
+
+using System.Text.Json;
+using Azure.AI.OpenAI;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.FoundryMemory;
+using Microsoft.Extensions.AI;
+using OpenAI.Chat;
+
+string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_MEMORY_STORE_NAME") ?? throw new InvalidOperationException("FOUNDRY_MEMORY_STORE_NAME is not set.");
+
+// Create an AIProjectClient for Foundry Memory with Azure Identity authentication.
+AzureCliCredential credential = new();
+AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
+
+AIAgent agent = new AzureOpenAIClient(new Uri(openAiEndpoint), credential)
+ .GetChatClient(deploymentName)
+ .AsAIAgent(new ChatClientAgentOptions()
+ {
+ ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
+ AIContextProviderFactory = (ctx, ct) => new ValueTask(ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
+ // If each session should have its own scope, you can create a new id per session here:
+ // ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = Guid.NewGuid().ToString() }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
+ // In this case we are storing memories scoped by user so that memories are retained across sessions.
+ ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = "sample-user-123" }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
+ // For cases where we are restoring from serialized state:
+ : new FoundryMemoryProvider(projectClient, ctx.SerializedState, ctx.JsonSerializerOptions, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName }))
+ });
+
+AgentSession session = await agent.GetNewSessionAsync();
+
+// Clear any existing memories for this scope to demonstrate fresh behavior.
+FoundryMemoryProvider memoryProvider = session.GetService()!;
+await memoryProvider.ClearStoredMemoriesAsync();
+
+Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
+Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
+
+Console.WriteLine("\nWaiting briefly for Foundry Memory to index the new memories...\n");
+await Task.Delay(TimeSpan.FromSeconds(3));
+
+Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
+
+Console.WriteLine("\n>> Serialize and deserialize the session to demonstrate persisted state\n");
+JsonElement serializedSession = session.Serialize();
+AgentSession restoredSession = await agent.DeserializeSessionAsync(serializedSession);
+Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
+
+Console.WriteLine("\n>> Start a new session that shares the same Foundry Memory scope\n");
+AgentSession newSession = await agent.GetNewSessionAsync();
+Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
new file mode 100644
index 0000000000..d170497a46
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
@@ -0,0 +1,64 @@
+# Agent with Memory Using Azure AI Foundry
+
+This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories across sessions.
+
+## Features Demonstrated
+
+- Creating a `FoundryMemoryProvider` with Azure Identity authentication
+- Multi-turn conversations with automatic memory extraction
+- Memory retrieval to inform agent responses
+- Session serialization and deserialization
+- Memory persistence across completely new sessions
+
+## Prerequisites
+
+1. Azure subscription with Azure AI Foundry project
+2. Memory store created in your Foundry project (see setup below)
+3. Azure OpenAI resource with a chat model deployment (e.g., gpt-4o-mini)
+4. .NET 10.0 SDK
+5. Azure CLI logged in (`az login`)
+
+## Setup Memory Store
+
+1. Navigate to your [Azure AI Foundry project](https://ai.azure.com/)
+2. Go to **Agents** > **Memory stores**
+3. Create a new memory store with:
+ - A chat model deployment for memory extraction
+ - An embedding model deployment for semantic search
+ - Enable user profile memory and/or chat summary memory
+
+## Environment Variables
+
+```bash
+# Azure OpenAI endpoint and deployment
+export AZURE_OPENAI_ENDPOINT="https://your-openai.openai.azure.com/"
+export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
+
+# Azure AI Foundry project endpoint and memory store name
+export AZURE_AI_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
+export FOUNDRY_MEMORY_STORE_NAME="my_memory_store"
+```
+
+## Run the Sample
+
+```bash
+dotnet run
+```
+
+## Expected Output
+
+The agent will:
+1. Learn your name (Taylor), travel destination (Patagonia), timing (November), companions (sister), and interests (scenic viewpoints)
+2. Wait for Foundry Memory to index the memories
+3. Recall those details when asked about the trip
+4. Demonstrate memory persistence across session serialization/deserialization
+5. Show that a brand new session can still access the same memories
+
+## Key Differences from Mem0
+
+| Aspect | Mem0 | Azure AI Foundry Memory |
+|--------|------|------------------------|
+| Authentication | API Key | Azure Identity (DefaultAzureCredential) |
+| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
+| Memory Types | Single memory store | User Profile + Chat Summary |
+| Hosting | Mem0 cloud or self-hosted | Azure AI Foundry managed service |
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/README.md
index 903fcf1b78..6e36ba0511 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/README.md
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/README.md
@@ -7,3 +7,4 @@ These samples show how to create an agent with the Agent Framework that uses Mem
|[Chat History memory](./AgentWithMemory_Step01_ChatHistoryMemory/)|This sample demonstrates how to enable an agent to remember messages from previous conversations.|
|[Memory with MemoryStore](./AgentWithMemory_Step02_MemoryUsingMem0/)|This sample demonstrates how to create and run an agent that uses the Mem0 service to extract and retrieve individual memories.|
|[Custom Memory Implementation](./AgentWithMemory_Step03_CustomMemory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.|
+|[Memory with Azure AI Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Azure AI Foundry's managed memory service to extract and retrieve individual memories.|
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
index ef0afda93d..d41770e283 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
diff --git a/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs b/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
new file mode 100644
index 0000000000..b752754e59
--- /dev/null
+++ b/dotnet/src/Shared/IntegrationTests/FoundryMemoryConfiguration.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Shared.IntegrationTests;
+
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
+#pragma warning disable CA1812 // Internal class that is apparently never instantiated.
+
+internal sealed class FoundryMemoryConfiguration
+{
+ public string Endpoint { get; set; }
+ public string MemoryStoreName { get; set; }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
new file mode 100644
index 0000000000..fce3f58bb4
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using Shared.IntegrationTests;
+
+namespace Microsoft.Agents.AI.FoundryMemory.IntegrationTests;
+
+///
+/// Integration tests for against a configured Azure AI Foundry Memory service.
+///
+public sealed class FoundryMemoryProviderTests : IDisposable
+{
+ private const string SkipReason = "Requires an Azure AI Foundry Memory service configured"; // Set to null to enable.
+
+ private readonly AIProjectClient? _client;
+ private readonly string? _memoryStoreName;
+ private bool _disposed;
+
+ public FoundryMemoryProviderTests()
+ {
+ IConfigurationRoot configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets(optional: true)
+ .Build();
+
+ var foundrySettings = configuration.GetSection("FoundryMemory").Get();
+
+ if (foundrySettings is not null &&
+ !string.IsNullOrWhiteSpace(foundrySettings.Endpoint) &&
+ !string.IsNullOrWhiteSpace(foundrySettings.MemoryStoreName))
+ {
+ this._client = new AIProjectClient(new Uri(foundrySettings.Endpoint), new AzureCliCredential());
+ this._memoryStoreName = foundrySettings.MemoryStoreName;
+ }
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanAddAndRetrieveUserMemoriesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is my name?");
+ var input = new ChatMessage(ChatRole.User, "Hello, my name is Caoimhe.");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-user-1" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.ClearStoredMemoriesAsync();
+ var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input], aiContextProviderMessages: null));
+ var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
+ await sut.ClearStoredMemoriesAsync();
+ var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task CanAddAndRetrieveAssistantMemoriesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is your name?");
+ var assistantIntro = new ChatMessage(ChatRole.Assistant, "Hello, I'm a friendly assistant and my name is Caoimhe.");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-agent-1" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.ClearStoredMemoriesAsync();
+ var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
+ var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
+ await sut.ClearStoredMemoriesAsync();
+ var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterClearing.Messages?[0].Text ?? string.Empty);
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task DoesNotLeakMemoriesAcrossScopesAsync()
+ {
+ // Arrange
+ var question = new ChatMessage(ChatRole.User, "What is your name?");
+ var assistantIntro = new ChatMessage(ChatRole.Assistant, "I'm an AI tutor and my name is Caoimhe.");
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut1 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-a" }, options);
+ var sut2 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-b" }, options);
+
+ await sut1.ClearStoredMemoriesAsync();
+ await sut2.ClearStoredMemoriesAsync();
+
+ var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+ Assert.DoesNotContain("Caoimhe", ctxBefore1.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxBefore2.Messages?[0].Text ?? string.Empty);
+
+ // Act
+ await sut1.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
+ var ctxAfterAdding1 = await GetContextWithRetryAsync(sut1, question);
+ var ctxAfterAdding2 = await GetContextWithRetryAsync(sut2, question);
+
+ // Assert
+ Assert.Contains("Caoimhe", ctxAfterAdding1.Messages?[0].Text ?? string.Empty);
+ Assert.DoesNotContain("Caoimhe", ctxAfterAdding2.Messages?[0].Text ?? string.Empty);
+
+ // Cleanup
+ await sut1.ClearStoredMemoriesAsync();
+ await sut2.ClearStoredMemoriesAsync();
+ }
+
+ [Fact(Skip = SkipReason)]
+ public async Task ClearStoredMemoriesRemovesAllMemoriesAsync()
+ {
+ // Arrange
+ var input1 = new ChatMessage(ChatRole.User, "My favorite color is blue.");
+ var input2 = new ChatMessage(ChatRole.User, "My favorite food is pizza.");
+ var question = new ChatMessage(ChatRole.User, "What do you know about my preferences?");
+ var storageScope = new FoundryMemoryProviderScope { Scope = "it-clear-test" };
+ var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
+ var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
+
+ await sut.ClearStoredMemoriesAsync();
+
+ // Act - Add multiple memories
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input1], aiContextProviderMessages: null));
+ await sut.InvokedAsync(new AIContextProvider.InvokedContext([input2], aiContextProviderMessages: null));
+ var ctxBeforeClear = await GetContextWithRetryAsync(sut, question, searchTerms: ["blue", "pizza"]);
+
+ await sut.ClearStoredMemoriesAsync();
+ var ctxAfterClear = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
+
+ // Assert
+ var textBefore = ctxBeforeClear.Messages?[0].Text ?? string.Empty;
+ var textAfter = ctxAfterClear.Messages?[0].Text ?? string.Empty;
+
+ Assert.True(textBefore.Contains("blue") || textBefore.Contains("pizza"), "Should contain at least one preference before clear");
+ Assert.DoesNotContain("blue", textAfter);
+ Assert.DoesNotContain("pizza", textAfter);
+ }
+
+ private static async Task GetContextWithRetryAsync(
+ FoundryMemoryProvider provider,
+ ChatMessage question,
+ string[]? searchTerms = null,
+ int attempts = 5,
+ int delayMs = 2000)
+ {
+ searchTerms ??= ["Caoimhe"];
+ AIContext? ctx = null;
+
+ for (int i = 0; i < attempts; i++)
+ {
+ ctx = await provider.InvokingAsync(new AIContextProvider.InvokingContext([question]), CancellationToken.None);
+ var text = ctx.Messages?[0].Text ?? string.Empty;
+
+ if (Array.Exists(searchTerms, term => text.Contains(term, StringComparison.OrdinalIgnoreCase)))
+ {
+ break;
+ }
+
+ await Task.Delay(delayMs);
+ }
+
+ return ctx!;
+ }
+
+ public void Dispose()
+ {
+ if (!this._disposed)
+ {
+ this._disposed = true;
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
new file mode 100644
index 0000000000..d4cabd7687
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
index a9ac8c585c..7a314eb867 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
@@ -55,12 +55,6 @@ public void Constructor_Throws_WhenScopeIsNull()
// Act & Assert
var ex = Assert.Throws(() => new FoundryMemoryProvider(
this._operationsMock.Object,
-<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
- (FoundryMemoryProviderScope)null!,
-=======
- null!,
->>>>>>> After
-
null!,
new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
Assert.Equal("scope", ex.ParamName);
From 372f885624b984da282f5718144e08ec39a58f65 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Fri, 30 Jan 2026 12:38:57 +0000
Subject: [PATCH 3/8] Fix ClearStoredMemoriesAsync to handle 404 gracefully and
rename to EnsureStoredMemoriesDeletedAsync
---
...ithMemory_Step04_MemoryUsingFoundry.csproj | 1 -
.../Program.cs | 18 ++--
.../README.md | 9 +-
.../FoundryMemoryProvider.cs | 24 +++++-
.../FoundryMemoryProviderTests.cs | 20 ++---
.../FoundryMemoryProviderTests.cs | 84 ++++++++++++++++++-
6 files changed, 125 insertions(+), 31 deletions(-)
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
index 7a576946ae..12d585e418 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
@@ -9,7 +9,6 @@
-
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
index 537891af5b..7a7382a5d4 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -5,30 +5,28 @@
// memories for subsequent invocations, even across new sessions.
using System.Text.Json;
-using Azure.AI.OpenAI;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.FoundryMemory;
using Microsoft.Extensions.AI;
-using OpenAI.Chat;
-string openAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
-string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
-
-string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
string memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_MEMORY_STORE_NAME") ?? throw new InvalidOperationException("FOUNDRY_MEMORY_STORE_NAME is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
-// Create an AIProjectClient for Foundry Memory with Azure Identity authentication.
+// Create an AIProjectClient for Foundry with Azure Identity authentication.
AzureCliCredential credential = new();
AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
-AIAgent agent = new AzureOpenAIClient(new Uri(openAiEndpoint), credential)
+// Get the ChatClient from the AIProjectClient's OpenAI property using the deployment name.
+AIAgent agent = projectClient.OpenAI
.GetChatClient(deploymentName)
+ .AsIChatClient()
.AsAIAgent(new ChatClientAgentOptions()
{
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
- AIContextProviderFactory = (ctx, ct) => new ValueTask(ctx.SerializedState.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined
+ AIContextProviderFactory = (ctx, ct) => new ValueTask(ctx.SerializedState.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
// If each session should have its own scope, you can create a new id per session here:
// ? new FoundryMemoryProvider(projectClient, new FoundryMemoryProviderScope() { Scope = Guid.NewGuid().ToString() }, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName })
// In this case we are storing memories scoped by user so that memories are retained across sessions.
@@ -41,7 +39,7 @@
// Clear any existing memories for this scope to demonstrate fresh behavior.
FoundryMemoryProvider memoryProvider = session.GetService()!;
-await memoryProvider.ClearStoredMemoriesAsync();
+await memoryProvider.EnsureStoredMemoriesDeletedAsync();
Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
index d170497a46..d2cbd843c2 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
@@ -30,13 +30,12 @@ This sample demonstrates how to create and run an agent that uses Azure AI Found
## Environment Variables
```bash
-# Azure OpenAI endpoint and deployment
-export AZURE_OPENAI_ENDPOINT="https://your-openai.openai.azure.com/"
-export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
-
# Azure AI Foundry project endpoint and memory store name
-export AZURE_AI_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
+export AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
export FOUNDRY_MEMORY_STORE_NAME="my_memory_store"
+
+# Azure OpenAI deployment name (model deployed in your Foundry project)
+export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
```
## Run the Sample
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
index d41770e283..d7919f3ca9 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -280,11 +281,28 @@ await this._operations.UpdateMemoriesAsync(
}
///
- /// Clears all stored memories for the configured scope.
+ /// Ensures all stored memories for the configured scope are deleted.
+ /// This method handles cases where the scope doesn't exist (no memories stored yet).
///
/// Cancellation token.
- public Task ClearStoredMemoriesAsync(CancellationToken cancellationToken = default) =>
- this._operations.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken);
+ public async Task EnsureStoredMemoriesDeletedAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ await this._operations.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken).ConfigureAwait(false);
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Scope doesn't exist (no memories stored yet), nothing to delete
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: No memories to delete for scope. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ this._memoryStoreName,
+ this.SanitizeLogData(this._scope.Scope));
+ }
+ }
+ }
///
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
index fce3f58bb4..2a5e154cbc 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs
@@ -52,14 +52,14 @@ public async Task CanAddAndRetrieveUserMemoriesAsync()
var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext([input], aiContextProviderMessages: null));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
// Assert
@@ -77,14 +77,14 @@ public async Task CanAddAndRetrieveAssistantMemoriesAsync()
var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
var ctxBefore = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
Assert.DoesNotContain("Caoimhe", ctxBefore.Messages?[0].Text ?? string.Empty);
// Act
await sut.InvokedAsync(new AIContextProvider.InvokedContext([assistantIntro], aiContextProviderMessages: null));
var ctxAfterAdding = await GetContextWithRetryAsync(sut, question);
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
var ctxAfterClearing = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
// Assert
@@ -102,8 +102,8 @@ public async Task DoesNotLeakMemoriesAcrossScopesAsync()
var sut1 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-a" }, options);
var sut2 = new FoundryMemoryProvider(this._client!, new FoundryMemoryProviderScope { Scope = "it-scope-b" }, options);
- await sut1.ClearStoredMemoriesAsync();
- await sut2.ClearStoredMemoriesAsync();
+ await sut1.EnsureStoredMemoriesDeletedAsync();
+ await sut2.EnsureStoredMemoriesDeletedAsync();
var ctxBefore1 = await sut1.InvokingAsync(new AIContextProvider.InvokingContext([question]));
var ctxBefore2 = await sut2.InvokingAsync(new AIContextProvider.InvokingContext([question]));
@@ -120,8 +120,8 @@ public async Task DoesNotLeakMemoriesAcrossScopesAsync()
Assert.DoesNotContain("Caoimhe", ctxAfterAdding2.Messages?[0].Text ?? string.Empty);
// Cleanup
- await sut1.ClearStoredMemoriesAsync();
- await sut2.ClearStoredMemoriesAsync();
+ await sut1.EnsureStoredMemoriesDeletedAsync();
+ await sut2.EnsureStoredMemoriesDeletedAsync();
}
[Fact(Skip = SkipReason)]
@@ -135,14 +135,14 @@ public async Task ClearStoredMemoriesRemovesAllMemoriesAsync()
var options = new FoundryMemoryProviderOptions { MemoryStoreName = this._memoryStoreName! };
var sut = new FoundryMemoryProvider(this._client!, storageScope, options);
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
// Act - Add multiple memories
await sut.InvokedAsync(new AIContextProvider.InvokedContext([input1], aiContextProviderMessages: null));
await sut.InvokedAsync(new AIContextProvider.InvokedContext([input2], aiContextProviderMessages: null));
var ctxBeforeClear = await GetContextWithRetryAsync(sut, question, searchTerms: ["blue", "pizza"]);
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
var ctxAfterClear = await sut.InvokingAsync(new AIContextProvider.InvokingContext([question]));
// Assert
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
index 7a314eb867..310da61ba5 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
using System.Collections.Generic;
+using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -291,13 +294,13 @@ public async Task InvokedAsync_ShouldNotThrow_WhenStorageFailsAsync()
}
[Fact]
- public async Task ClearStoredMemoriesAsync_SendsDeleteRequestAsync()
+ public async Task EnsureStoredMemoriesDeletedAsync_SendsDeleteRequestAsync()
{
// Arrange
FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
// Act
- await sut.ClearStoredMemoriesAsync();
+ await sut.EnsureStoredMemoriesDeletedAsync();
// Assert
this._operationsMock.Verify(
@@ -305,6 +308,20 @@ public async Task ClearStoredMemoriesAsync_SendsDeleteRequestAsync()
Times.Once);
}
+ [Fact]
+ public async Task EnsureStoredMemoriesDeletedAsync_Handles404GracefullyAsync()
+ {
+ // Arrange
+ this._operationsMock
+ .Setup(o => o.DeleteScopeAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ThrowsAsync(new ClientResultException(new MockPipelineResponse(404)));
+
+ FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+
+ // Act & Assert - should not throw
+ await sut.EnsureStoredMemoriesDeletedAsync();
+ }
+
[Fact]
public void Serialize_RoundTripsScope()
{
@@ -360,4 +377,67 @@ public async Task InvokingAsync_LogsScopeBasedOnEnableSensitiveTelemetryDataAsyn
It.IsAny>()),
Times.Once);
}
+
+ private sealed class MockPipelineResponse : PipelineResponse
+ {
+ private readonly int _status;
+ private readonly MockPipelineResponseHeaders _headers;
+
+ public MockPipelineResponse(int status)
+ {
+ this._status = status;
+ this.Content = BinaryData.Empty;
+ this._headers = new MockPipelineResponseHeaders();
+ }
+
+ public override int Status => this._status;
+
+ public override string ReasonPhrase => this._status == 404 ? "Not Found" : "OK";
+
+ public override Stream? ContentStream
+ {
+ get => null;
+ set { }
+ }
+
+ public override BinaryData Content { get; }
+
+ protected override PipelineResponseHeaders HeadersCore => this._headers;
+
+ public override BinaryData BufferContent(CancellationToken cancellationToken = default) => this.Content;
+
+ public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) =>
+ new(this.Content);
+
+ public override void Dispose()
+ {
+ }
+
+ private sealed class MockPipelineResponseHeaders : PipelineResponseHeaders
+ {
+ private readonly Dictionary _headers = new(StringComparer.OrdinalIgnoreCase);
+
+ public override bool TryGetValue(string name, out string? value)
+ {
+ return this._headers.TryGetValue(name, out value);
+ }
+
+ public override bool TryGetValues(string name, out IEnumerable? values)
+ {
+ if (this._headers.TryGetValue(name, out string? value))
+ {
+ values = [value];
+ return true;
+ }
+
+ values = null;
+ return false;
+ }
+
+ public override IEnumerator> GetEnumerator()
+ {
+ return this._headers.GetEnumerator();
+ }
+ }
+ }
}
From b3c8acfb16e1115908cdb5c0ef9cfec168649cef Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Fri, 30 Jan 2026 15:09:49 +0000
Subject: [PATCH 4/8] Refactor FoundryMemory: simplify architecture and add
memory store creation
- Remove IFoundryMemoryOperations interface (was only for test mocking)
- Remove AIProjectClientMemoryOperations wrapper class
- Provider now directly uses AIProjectClient with internal extension methods
- Extension methods return actual response models instead of extracted values
- Remove WaitForUpdateCompletionAsync from provider (sample uses delay)
- Simplify EnsureMemoryStoreCreatedAsync to return Task instead of Task
- Add memory store creation with chat_model and embedding_model
- Add UpdateMemoriesResponse with SupersededBy and Error fields
- Simplify unit tests to focus on constructor validation and serialization
- Update sample to use simple delay for memory processing wait
---
...ithMemory_Step04_MemoryUsingFoundry.csproj | 5 +-
.../Program.cs | 79 +++-
.../README.md | 38 +-
.../AIProjectClientExtensions.cs | 120 +++--
.../AIProjectClientMemoryOperations.cs | 50 --
.../Core/Models/CreateMemoryStoreRequest.cs | 29 ++
.../Core/Models/MemoryStoreDefinition.cs | 29 ++
.../Core/Models/MemoryStoreResponse.cs | 29 ++
.../Core/Models/UpdateMemoriesError.cs | 23 +
.../Core/Models/UpdateMemoriesResponse.cs | 13 +
.../FoundryMemoryJsonUtilities.cs | 4 +
.../FoundryMemoryProvider.cs | 128 +++---
.../IFoundryMemoryOperations.cs | 42 --
.../FoundryMemoryProviderTests.cs | 430 ++++--------------
.../TestableAIProjectClient.cs | 215 +++++++++
15 files changed, 661 insertions(+), 573 deletions(-)
delete mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs
create mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs
delete mode 100644 dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
index 12d585e418..0b6c06a5a8 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/AgentWithMemory_Step04_MemoryUsingFoundry.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -11,11 +11,10 @@
-
-
+
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
index 7a7382a5d4..b4f4270bee 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -3,28 +3,39 @@
// This sample shows how to use the FoundryMemoryProvider to persist and recall memories for an agent.
// The sample stores conversation messages in an Azure AI Foundry memory store and retrieves relevant
// memories for subsequent invocations, even across new sessions.
+//
+// Note: Memory extraction in Azure AI Foundry is asynchronous and takes time. This sample demonstrates
+// a simple polling approach to wait for memory updates to complete before querying.
+using System.ClientModel.Primitives;
using System.Text.Json;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.FoundryMemory;
-using Microsoft.Extensions.AI;
-string foundryEndpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
-string memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_MEMORY_STORE_NAME") ?? throw new InvalidOperationException("FOUNDRY_MEMORY_STORE_NAME is not set.");
-string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+string foundryEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string memoryStoreName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_MEMORY_STORE_NAME") ?? "sample-memory-store-name";
+string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_MODEL") ?? "gpt-4o-mini";
+string embeddingModelName = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_EMBEDDING_MODEL") ?? "text-embedding-ada-002";
// Create an AIProjectClient for Foundry with Azure Identity authentication.
AzureCliCredential credential = new();
-AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential);
+
+// Add a debug handler to log all HTTP requests
+DebugHttpClientHandler debugHandler = new() { CheckCertificateRevocationList = true };
+HttpClient httpClient = new(debugHandler);
+AIProjectClientOptions clientOptions = new()
+{
+ Transport = new HttpClientPipelineTransport(httpClient)
+};
+AIProjectClient projectClient = new(new Uri(foundryEndpoint), credential, clientOptions);
// Get the ChatClient from the AIProjectClient's OpenAI property using the deployment name.
-AIAgent agent = projectClient.OpenAI
- .GetChatClient(deploymentName)
- .AsIChatClient()
- .AsAIAgent(new ChatClientAgentOptions()
+AIAgent agent = await projectClient.CreateAIAgentAsync(deploymentName,
+ options: new ChatClientAgentOptions()
{
+ Name = "TravelAssistantWithFoundryMemory",
ChatOptions = new() { Instructions = "You are a friendly travel assistant. Use known memories about the user when responding, and do not invent details." },
AIContextProviderFactory = (ctx, ct) => new ValueTask(ctx.SerializedState.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined
// If each session should have its own scope, you can create a new id per session here:
@@ -37,15 +48,30 @@
AgentSession session = await agent.GetNewSessionAsync();
-// Clear any existing memories for this scope to demonstrate fresh behavior.
FoundryMemoryProvider memoryProvider = session.GetService()!;
+
+Console.WriteLine("\n>> Setting up Foundry Memory Store\n");
+
+// Ensure the memory store exists (creates it with the specified models if needed).
+await memoryProvider.EnsureMemoryStoreCreatedAsync(deploymentName, embeddingModelName, "Sample memory store for travel assistant");
+
+// Clear any existing memories for this scope to demonstrate fresh behavior.
await memoryProvider.EnsureStoredMemoriesDeletedAsync();
Console.WriteLine(await agent.RunAsync("Hi there! My name is Taylor and I'm planning a hiking trip to Patagonia in November.", session));
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
-Console.WriteLine("\nWaiting briefly for Foundry Memory to index the new memories...\n");
-await Task.Delay(TimeSpan.FromSeconds(3));
+// Memory extraction in Azure AI Foundry is asynchronous and takes time to process.
+// In production scenarios, you would:
+// 1. Track the UpdateId returned from each memory update operation
+// 2. Poll the GetUpdateResultAsync API to check completion status
+// 3. Wait for status to become "completed" before querying memories
+//
+// For simplicity, this sample uses a fixed delay. For production code, implement proper polling
+// using the Azure.AI.Projects SDK's MemoryStores.GetUpdateResultAsync method.
+Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
+await Task.Delay(TimeSpan.FromSeconds(10));
+Console.WriteLine("Continuing after delay.\n");
Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
@@ -57,3 +83,32 @@
Console.WriteLine("\n>> Start a new session that shares the same Foundry Memory scope\n");
AgentSession newSession = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
+
+// Debug HTTP handler to log all requests (commented out by default)
+internal sealed class DebugHttpClientHandler : HttpClientHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // Uncomment to debug HTTP traffic:
+ // Console.WriteLine("\n=== HTTP REQUEST ===");
+ // Console.WriteLine($"Method: {request.Method}");
+ // Console.WriteLine($"URI: {request.RequestUri}");
+ //
+ // if (request.Content != null)
+ // {
+ // string body = await request.Content.ReadAsStringAsync(cancellationToken);
+ // Console.WriteLine("Body: " + body);
+ // }
+ //
+ // Console.WriteLine("====================\n");
+
+ // Uncomment to debug HTTP traffic:
+ // Console.WriteLine("\n=== HTTP RESPONSE ===");
+ // Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");
+ // string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+ // Console.WriteLine("Body: " + responseBody);
+ // Console.WriteLine("=====================\n");
+
+ return await base.SendAsync(request, cancellationToken);
+ }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
index d2cbd843c2..dfea386d82 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/README.md
@@ -5,6 +5,7 @@ This sample demonstrates how to create and run an agent that uses Azure AI Found
## Features Demonstrated
- Creating a `FoundryMemoryProvider` with Azure Identity authentication
+- Automatic memory store creation if it doesn't exist
- Multi-turn conversations with automatic memory extraction
- Memory retrieval to inform agent responses
- Session serialization and deserialization
@@ -13,29 +14,20 @@ This sample demonstrates how to create and run an agent that uses Azure AI Found
## Prerequisites
1. Azure subscription with Azure AI Foundry project
-2. Memory store created in your Foundry project (see setup below)
-3. Azure OpenAI resource with a chat model deployment (e.g., gpt-4o-mini)
-4. .NET 10.0 SDK
-5. Azure CLI logged in (`az login`)
-
-## Setup Memory Store
-
-1. Navigate to your [Azure AI Foundry project](https://ai.azure.com/)
-2. Go to **Agents** > **Memory stores**
-3. Create a new memory store with:
- - A chat model deployment for memory extraction
- - An embedding model deployment for semantic search
- - Enable user profile memory and/or chat summary memory
+2. Azure OpenAI resource with a chat model deployment (e.g., gpt-4o-mini) and an embedding model deployment (e.g., text-embedding-ada-002)
+3. .NET 10.0 SDK
+4. Azure CLI logged in (`az login`)
## Environment Variables
```bash
# Azure AI Foundry project endpoint and memory store name
-export AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
-export FOUNDRY_MEMORY_STORE_NAME="my_memory_store"
+export FOUNDRY_PROJECT_ENDPOINT="https://your-account.services.ai.azure.com/api/projects/your-project"
+export FOUNDRY_PROJECT_MEMORY_STORE_NAME="my_memory_store"
-# Azure OpenAI deployment name (model deployed in your Foundry project)
-export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
+# Model deployment names (models deployed in your Foundry project)
+export FOUNDRY_PROJECT_MODEL="gpt-4o-mini"
+export FOUNDRY_PROJECT_EMBEDDING_MODEL="text-embedding-ada-002"
```
## Run the Sample
@@ -47,11 +39,12 @@ dotnet run
## Expected Output
The agent will:
-1. Learn your name (Taylor), travel destination (Patagonia), timing (November), companions (sister), and interests (scenic viewpoints)
-2. Wait for Foundry Memory to index the memories
-3. Recall those details when asked about the trip
-4. Demonstrate memory persistence across session serialization/deserialization
-5. Show that a brand new session can still access the same memories
+1. Create the memory store if it doesn't exist (using the specified chat and embedding models)
+2. Learn your name (Taylor), travel destination (Patagonia), timing (November), companions (sister), and interests (scenic viewpoints)
+3. Wait for Foundry Memory to index the memories
+4. Recall those details when asked about the trip
+5. Demonstrate memory persistence across session serialization/deserialization
+6. Show that a brand new session can still access the same memories
## Key Differences from Mem0
@@ -61,3 +54,4 @@ The agent will:
| Scope | ApplicationId, UserId, AgentId, ThreadId | Single `Scope` string |
| Memory Types | Single memory store | User Profile + Chat Summary |
| Hosting | Mem0 cloud or self-hosted | Azure AI Foundry managed service |
+| Store Creation | N/A (automatic) | Explicit via `EnsureMemoryStoreCreatedAsync` |
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
index 4ee323bc5a..6107a01cc7 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientExtensions.cs
@@ -14,22 +14,58 @@
namespace Microsoft.Agents.AI.FoundryMemory;
///
-/// Extension methods for to provide MemoryStores operations
+/// Internal extension methods for to provide MemoryStores operations
/// using the SDK's HTTP pipeline until the SDK releases convenience methods.
///
internal static class AIProjectClientExtensions
{
+ ///
+ /// Creates a memory store if it doesn't already exist.
+ ///
+ internal static async Task CreateMemoryStoreIfNotExistsAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string? description,
+ string chatModel,
+ string embeddingModel,
+ CancellationToken cancellationToken)
+ {
+ // First try to get the store to see if it exists
+ try
+ {
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ await client.MemoryStores.GetMemoryStoreAsync(memoryStoreName, requestOptions).ConfigureAwait(false);
+ return false; // Store already exists
+ }
+ catch (ClientResultException ex) when (ex.Status == 404)
+ {
+ // Store doesn't exist, create it
+ }
+
+ CreateMemoryStoreRequest request = new()
+ {
+ Name = memoryStoreName,
+ Description = description,
+ Definition = new MemoryStoreDefinitionRequest
+ {
+ Kind = "default",
+ ChatModel = chatModel,
+ EmbeddingModel = embeddingModel
+ }
+ };
+
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.CreateMemoryStoreRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
+
+ RequestOptions createOptions = new() { CancellationToken = cancellationToken };
+ await client.MemoryStores.CreateMemoryStoreAsync(content, createOptions).ConfigureAwait(false);
+ return true;
+ }
+
///
/// Searches for relevant memories from a memory store based on conversation context.
///
- /// The AI Project client.
- /// The name of the memory store to search.
- /// The namespace that logically groups and isolates memories, such as a user ID.
- /// The conversation messages to use for the search query.
- /// Maximum number of memories to return.
- /// Cancellation token.
- /// Enumerable of memory content strings.
- public static async Task> SearchMemoriesAsync(
+ internal static async Task SearchMemoriesAsync(
this AIProjectClient client,
string memoryStoreName,
string scope,
@@ -37,37 +73,28 @@ public static async Task> SearchMemoriesAsync(
int maxMemories,
CancellationToken cancellationToken)
{
- var request = new SearchMemoriesRequest
+ SearchMemoriesRequest request = new()
{
Scope = scope,
Items = messages.ToArray(),
Options = new SearchMemoriesOptions { MaxMemories = maxMemories }
};
- var json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.SearchMemoriesRequest);
- var content = BinaryContent.Create(BinaryData.FromString(json));
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.SearchMemoriesRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
- var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
ClientResult result = await client.MemoryStores.SearchMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
- var response = JsonSerializer.Deserialize(
+ return JsonSerializer.Deserialize(
result.GetRawResponse().Content.ToString(),
FoundryMemoryJsonContext.Default.SearchMemoriesResponse);
-
- return response?.Memories?.Select(m => m.MemoryItem?.Content ?? string.Empty)
- .Where(c => !string.IsNullOrWhiteSpace(c)) ?? [];
}
///
/// Updates memory store with conversation memories.
///
- /// The AI Project client.
- /// The name of the memory store to update.
- /// The namespace that logically groups and isolates memories, such as a user ID.
- /// The conversation messages to extract memories from.
- /// Delay in seconds before processing the update.
- /// Cancellation token.
- public static async Task UpdateMemoriesAsync(
+ internal static async Task UpdateMemoriesAsync(
this AIProjectClient client,
string memoryStoreName,
string scope,
@@ -75,39 +102,56 @@ public static async Task UpdateMemoriesAsync(
int updateDelay,
CancellationToken cancellationToken)
{
- var request = new UpdateMemoriesRequest
+ UpdateMemoriesRequest request = new()
{
Scope = scope,
Items = messages.ToArray(),
UpdateDelay = updateDelay
};
- var json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.UpdateMemoriesRequest);
- var content = BinaryContent.Create(BinaryData.FromString(json));
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.UpdateMemoriesRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
+
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ ClientResult result = await client.MemoryStores.UpdateMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+
+ return JsonSerializer.Deserialize(
+ result.GetRawResponse().Content.ToString(),
+ FoundryMemoryJsonContext.Default.UpdateMemoriesResponse);
+ }
+
+ ///
+ /// Gets the status of a memory update operation.
+ ///
+ internal static async Task GetUpdateStatusAsync(
+ this AIProjectClient client,
+ string memoryStoreName,
+ string updateId,
+ CancellationToken cancellationToken)
+ {
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
+ ClientResult result = await client.MemoryStores.GetUpdateResultAsync(memoryStoreName, updateId, requestOptions).ConfigureAwait(false);
- var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
- await client.MemoryStores.UpdateMemoriesAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
+ return JsonSerializer.Deserialize(
+ result.GetRawResponse().Content.ToString(),
+ FoundryMemoryJsonContext.Default.UpdateMemoriesResponse);
}
///
/// Deletes all memories associated with a specific scope from a memory store.
///
- /// The AI Project client.
- /// The name of the memory store.
- /// The namespace that logically groups and isolates memories to delete, such as a user ID.
- /// Cancellation token.
- public static async Task DeleteScopeAsync(
+ internal static async Task DeleteScopeAsync(
this AIProjectClient client,
string memoryStoreName,
string scope,
CancellationToken cancellationToken)
{
- var request = new DeleteScopeRequest { Scope = scope };
+ DeleteScopeRequest request = new() { Scope = scope };
- var json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.DeleteScopeRequest);
- var content = BinaryContent.Create(BinaryData.FromString(json));
+ string json = JsonSerializer.Serialize(request, FoundryMemoryJsonContext.Default.DeleteScopeRequest);
+ BinaryContent content = BinaryContent.Create(BinaryData.FromString(json));
- var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
+ RequestOptions requestOptions = new() { CancellationToken = cancellationToken };
await client.MemoryStores.DeleteScopeAsync(memoryStoreName, content, requestOptions).ConfigureAwait(false);
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs
deleted file mode 100644
index 3a3cecaac9..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/AIProjectClientMemoryOperations.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Azure.AI.Projects;
-using Microsoft.Agents.AI.FoundryMemory.Core.Models;
-
-namespace Microsoft.Agents.AI.FoundryMemory;
-
-///
-/// Implementation of using .
-///
-internal sealed class AIProjectClientMemoryOperations : IFoundryMemoryOperations
-{
- private readonly AIProjectClient _client;
-
- public AIProjectClientMemoryOperations(AIProjectClient client)
- {
- this._client = client;
- }
-
- public Task> SearchMemoriesAsync(
- string memoryStoreName,
- string scope,
- IEnumerable messages,
- int maxMemories,
- CancellationToken cancellationToken)
- {
- return this._client.SearchMemoriesAsync(memoryStoreName, scope, messages, maxMemories, cancellationToken);
- }
-
- public Task UpdateMemoriesAsync(
- string memoryStoreName,
- string scope,
- IEnumerable messages,
- int updateDelay,
- CancellationToken cancellationToken)
- {
- return this._client.UpdateMemoriesAsync(memoryStoreName, scope, messages, updateDelay, cancellationToken);
- }
-
- public Task DeleteScopeAsync(
- string memoryStoreName,
- string scope,
- CancellationToken cancellationToken)
- {
- return this._client.DeleteScopeAsync(memoryStoreName, scope, cancellationToken);
- }
-}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs
new file mode 100644
index 0000000000..76264e7a06
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/CreateMemoryStoreRequest.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Request body for creating a memory store.
+///
+internal sealed class CreateMemoryStoreRequest
+{
+ ///
+ /// Gets or sets the name of the memory store.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets an optional description for the memory store.
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+
+ ///
+ /// Gets or sets the definition for the memory store.
+ ///
+ [JsonPropertyName("definition")]
+ public MemoryStoreDefinitionRequest? Definition { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs
new file mode 100644
index 0000000000..8b4328b796
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreDefinition.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Definition for a memory store specifying the models to use.
+///
+internal sealed class MemoryStoreDefinitionRequest
+{
+ ///
+ /// Gets or sets the kind of memory store definition.
+ ///
+ [JsonPropertyName("kind")]
+ public string Kind { get; set; } = "default";
+
+ ///
+ /// Gets or sets the deployment name of the chat model for memory processing.
+ ///
+ [JsonPropertyName("chat_model")]
+ public string? ChatModel { get; set; }
+
+ ///
+ /// Gets or sets the deployment name of the embedding model for memory search.
+ ///
+ [JsonPropertyName("embedding_model")]
+ public string? EmbeddingModel { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs
new file mode 100644
index 0000000000..6df9c31d2a
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/MemoryStoreResponse.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Response from creating or getting a memory store.
+///
+internal sealed class MemoryStoreResponse
+{
+ ///
+ /// Gets or sets the unique identifier of the memory store.
+ ///
+ [JsonPropertyName("id")]
+ public string? Id { get; set; }
+
+ ///
+ /// Gets or sets the name of the memory store.
+ ///
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the description of the memory store.
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs
new file mode 100644
index 0000000000..6d62469da4
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesError.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
+
+///
+/// Error information for a failed update operation.
+///
+internal sealed class UpdateMemoriesError
+{
+ ///
+ /// Gets or sets the error code.
+ ///
+ [JsonPropertyName("code")]
+ public string? Code { get; set; }
+
+ ///
+ /// Gets or sets the error message.
+ ///
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
index f69474d89d..7ab9bdf68f 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
@@ -17,7 +17,20 @@ internal sealed class UpdateMemoriesResponse
///
/// Gets or sets the status of the update operation.
+ /// Known values are: "queued", "in_progress", "completed", "failed", "superseded".
///
[JsonPropertyName("status")]
public string? Status { get; set; }
+
+ ///
+ /// Gets or sets the update_id that superseded this operation when status is "superseded".
+ ///
+ [JsonPropertyName("superseded_by")]
+ public string? SupersededBy { get; set; }
+
+ ///
+ /// Gets or sets the error information when status is "failed".
+ ///
+ [JsonPropertyName("error")]
+ public UpdateMemoriesError? Error { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
index 8250b12a3f..f63d4a52e8 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryJsonUtilities.cs
@@ -39,7 +39,11 @@ internal static class FoundryMemoryJsonUtilities
[JsonSerializable(typeof(SearchMemoriesOptions))]
[JsonSerializable(typeof(UpdateMemoriesRequest))]
[JsonSerializable(typeof(UpdateMemoriesResponse))]
+[JsonSerializable(typeof(UpdateMemoriesError))]
[JsonSerializable(typeof(DeleteScopeRequest))]
+[JsonSerializable(typeof(CreateMemoryStoreRequest))]
+[JsonSerializable(typeof(MemoryStoreDefinitionRequest))]
+[JsonSerializable(typeof(MemoryStoreResponse))]
[JsonSerializable(typeof(MemoryInputMessage))]
[JsonSerializable(typeof(MemoryInputMessage[]))]
[JsonSerializable(typeof(MemorySearchResult))]
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
index d7919f3ca9..5ef25d49aa 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -34,7 +34,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider
private readonly int _updateDelay;
private readonly bool _enableSensitiveTelemetryData;
- private readonly IFoundryMemoryOperations _operations;
+ private readonly AIProjectClient _client;
private readonly ILogger? _logger;
private readonly FoundryMemoryProviderScope _scope;
@@ -51,39 +51,8 @@ public FoundryMemoryProvider(
FoundryMemoryProviderScope scope,
FoundryMemoryProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
- : this(new AIProjectClientMemoryOperations(Throw.IfNull(client)), scope, options, loggerFactory)
{
- }
-
- ///
- /// Initializes a new instance of the class, with existing state from a serialized JSON element.
- ///
- /// The Azure AI Project client configured for your Foundry project.
- /// A representing the serialized state of the provider.
- /// Optional settings for customizing the JSON deserialization process.
- /// Provider options including memory store name.
- /// Optional logger factory.
- public FoundryMemoryProvider(
- AIProjectClient client,
- JsonElement serializedState,
- JsonSerializerOptions? jsonSerializerOptions = null,
- FoundryMemoryProviderOptions? options = null,
- ILoggerFactory? loggerFactory = null)
- : this(new AIProjectClientMemoryOperations(Throw.IfNull(client)), serializedState, jsonSerializerOptions, options, loggerFactory)
- {
- }
-
- ///
- /// Initializes a new instance of the class with a custom operations implementation.
- ///
- /// This constructor enables testability by allowing injection of mock operations.
- internal FoundryMemoryProvider(
- IFoundryMemoryOperations operations,
- FoundryMemoryProviderScope scope,
- FoundryMemoryProviderOptions? options = null,
- ILoggerFactory? loggerFactory = null)
- {
- Throw.IfNull(operations);
+ Throw.IfNull(client);
Throw.IfNull(scope);
if (string.IsNullOrWhiteSpace(scope.Scope))
@@ -91,7 +60,7 @@ internal FoundryMemoryProvider(
throw new ArgumentException("The Scope property must be provided.", nameof(scope));
}
- var effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+ FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
{
@@ -99,7 +68,7 @@ internal FoundryMemoryProvider(
}
this._logger = loggerFactory?.CreateLogger();
- this._operations = operations;
+ this._client = client;
this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
this._memoryStoreName = effectiveOptions.MemoryStoreName;
@@ -112,17 +81,21 @@ internal FoundryMemoryProvider(
///
/// Initializes a new instance of the class, with existing state from a serialized JSON element.
///
- /// This constructor enables testability by allowing injection of mock operations.
- internal FoundryMemoryProvider(
- IFoundryMemoryOperations operations,
+ /// The Azure AI Project client configured for your Foundry project.
+ /// A representing the serialized state of the provider.
+ /// Optional settings for customizing the JSON deserialization process.
+ /// Provider options including memory store name.
+ /// Optional logger factory.
+ public FoundryMemoryProvider(
+ AIProjectClient client,
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null,
FoundryMemoryProviderOptions? options = null,
ILoggerFactory? loggerFactory = null)
{
- Throw.IfNull(operations);
+ Throw.IfNull(client);
- var effectiveOptions = options ?? new FoundryMemoryProviderOptions();
+ FoundryMemoryProviderOptions effectiveOptions = options ?? new FoundryMemoryProviderOptions();
if (string.IsNullOrWhiteSpace(effectiveOptions.MemoryStoreName))
{
@@ -130,7 +103,7 @@ internal FoundryMemoryProvider(
}
this._logger = loggerFactory?.CreateLogger();
- this._operations = operations;
+ this._client = client;
this._contextPrompt = effectiveOptions.ContextPrompt ?? DefaultContextPrompt;
this._memoryStoreName = effectiveOptions.MemoryStoreName;
@@ -138,8 +111,8 @@ internal FoundryMemoryProvider(
this._updateDelay = effectiveOptions.UpdateDelay;
this._enableSensitiveTelemetryData = effectiveOptions.EnableSensitiveTelemetryData;
- var jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
- var state = serializedState.Deserialize(jso.GetTypeInfo(typeof(FoundryMemoryState))) as FoundryMemoryState;
+ JsonSerializerOptions jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ FoundryMemoryState? state = serializedState.Deserialize(jso.GetTypeInfo(typeof(FoundryMemoryState))) as FoundryMemoryState;
if (state?.Scope == null || string.IsNullOrWhiteSpace(state.Scope.Scope))
{
@@ -155,7 +128,7 @@ public override async ValueTask InvokingAsync(InvokingContext context
Throw.IfNull(context);
#pragma warning disable CA1308 // Lowercase required by service
- var messageItems = context.RequestMessages
+ MemoryInputMessage[] messageItems = context.RequestMessages
.Where(m => !string.IsNullOrWhiteSpace(m.Text))
.Select(m => new MemoryInputMessage
{
@@ -172,14 +145,19 @@ public override async ValueTask InvokingAsync(InvokingContext context
try
{
- var memories = (await this._operations.SearchMemoriesAsync(
+ SearchMemoriesResponse? response = await this._client.SearchMemoriesAsync(
this._memoryStoreName,
this._scope.Scope!,
messageItems,
this._maxMemories,
- cancellationToken).ConfigureAwait(false)).ToList();
+ cancellationToken).ConfigureAwait(false);
+
+ var memories = response?.Memories?
+ .Select(m => m.MemoryItem?.Content ?? string.Empty)
+ .Where(c => !string.IsNullOrWhiteSpace(c))
+ .ToList() ?? [];
- var outputMessageText = memories.Count == 0
+ string? outputMessageText = memories.Count == 0
? null
: $"{this._contextPrompt}\n{string.Join(Environment.NewLine, memories)}";
@@ -220,6 +198,7 @@ public override async ValueTask InvokingAsync(InvokingContext context
this._memoryStoreName,
this.SanitizeLogData(this._scope.Scope));
}
+
return new AIContext();
}
}
@@ -235,7 +214,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
try
{
#pragma warning disable CA1308 // Lowercase required by service
- var messageItems = context.RequestMessages
+ MemoryInputMessage[] messageItems = context.RequestMessages
.Concat(context.ResponseMessages ?? [])
.Where(m => IsAllowedRole(m.Role) && !string.IsNullOrWhiteSpace(m.Text))
.Select(m => new MemoryInputMessage
@@ -251,7 +230,7 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
return;
}
- await this._operations.UpdateMemoriesAsync(
+ UpdateMemoriesResponse? response = await this._client.UpdateMemoriesAsync(
this._memoryStoreName,
this._scope.Scope!,
messageItems,
@@ -261,10 +240,11 @@ await this._operations.UpdateMemoriesAsync(
if (this._logger?.IsEnabled(LogLevel.Information) is true)
{
this._logger.LogInformation(
- "FoundryMemoryProvider: Sent {Count} messages to update memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}'.",
+ "FoundryMemoryProvider: Sent {Count} messages to update memories. MemoryStore: '{MemoryStoreName}', Scope: '{Scope}', UpdateId: '{UpdateId}'.",
messageItems.Length,
this._memoryStoreName,
- this.SanitizeLogData(this._scope.Scope));
+ this.SanitizeLogData(this._scope.Scope),
+ response?.UpdateId);
}
}
catch (Exception ex)
@@ -289,7 +269,7 @@ public async Task EnsureStoredMemoriesDeletedAsync(CancellationToken cancellatio
{
try
{
- await this._operations.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken).ConfigureAwait(false);
+ await this._client.DeleteScopeAsync(this._memoryStoreName, this._scope.Scope!, cancellationToken).ConfigureAwait(false);
}
catch (ClientResultException ex) when (ex.Status == 404)
{
@@ -304,12 +284,52 @@ public async Task EnsureStoredMemoriesDeletedAsync(CancellationToken cancellatio
}
}
+ ///
+ /// Ensures the memory store exists, creating it if necessary.
+ ///
+ /// The deployment name of the chat model for memory processing.
+ /// The deployment name of the embedding model for memory search.
+ /// Optional description for the memory store.
+ /// Cancellation token.
+ public async Task EnsureMemoryStoreCreatedAsync(
+ string chatModel,
+ string embeddingModel,
+ string? description = null,
+ CancellationToken cancellationToken = default)
+ {
+ bool created = await this._client.CreateMemoryStoreIfNotExistsAsync(
+ this._memoryStoreName,
+ description,
+ chatModel,
+ embeddingModel,
+ cancellationToken).ConfigureAwait(false);
+
+ if (created)
+ {
+ if (this._logger?.IsEnabled(LogLevel.Information) is true)
+ {
+ this._logger.LogInformation(
+ "FoundryMemoryProvider: Created memory store '{MemoryStoreName}'.",
+ this._memoryStoreName);
+ }
+ }
+ else
+ {
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: Memory store '{MemoryStoreName}' already exists.",
+ this._memoryStoreName);
+ }
+ }
+ }
+
///
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
- var state = new FoundryMemoryState(this._scope);
+ FoundryMemoryState state = new(this._scope);
- var jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
+ JsonSerializerOptions jso = jsonSerializerOptions ?? FoundryMemoryJsonUtilities.DefaultOptions;
return JsonSerializer.SerializeToElement(state, jso.GetTypeInfo(typeof(FoundryMemoryState)));
}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs
deleted file mode 100644
index af7fd6822c..0000000000
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/IFoundryMemoryOperations.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Agents.AI.FoundryMemory.Core.Models;
-
-namespace Microsoft.Agents.AI.FoundryMemory;
-
-///
-/// Interface for Foundry Memory operations, enabling testability.
-///
-internal interface IFoundryMemoryOperations
-{
- ///
- /// Searches for relevant memories from a memory store based on conversation context.
- ///
- Task> SearchMemoriesAsync(
- string memoryStoreName,
- string scope,
- IEnumerable messages,
- int maxMemories,
- CancellationToken cancellationToken);
-
- ///
- /// Updates memory store with conversation memories.
- ///
- Task UpdateMemoriesAsync(
- string memoryStoreName,
- string scope,
- IEnumerable messages,
- int updateDelay,
- CancellationToken cancellationToken);
-
- ///
- /// Deletes all memories associated with a specific scope from a memory store.
- ///
- Task DeleteScopeAsync(
- string memoryStoreName,
- string scope,
- CancellationToken cancellationToken);
-}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
index 310da61ba5..12307c24ca 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/FoundryMemoryProviderTests.cs
@@ -1,63 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.ClientModel;
-using System.ClientModel.Primitives;
-using System.Collections.Generic;
-using System.IO;
using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Agents.AI.FoundryMemory.Core.Models;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.Logging;
-using Moq;
namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
///
-/// Tests for .
+/// Tests for constructor validation and serialization.
///
+///
+/// Since directly uses ,
+/// integration tests are used to verify the memory operations. These unit tests focus on:
+/// - Constructor parameter validation
+/// - Serialization and deserialization of provider state
+///
public sealed class FoundryMemoryProviderTests
{
- private readonly Mock _operationsMock;
- private readonly Mock> _loggerMock;
- private readonly Mock _loggerFactoryMock;
-
- public FoundryMemoryProviderTests()
- {
- this._operationsMock = new();
- this._loggerMock = new();
- this._loggerFactoryMock = new();
- this._loggerFactoryMock
- .Setup(f => f.CreateLogger(It.IsAny()))
- .Returns(this._loggerMock.Object);
- this._loggerFactoryMock
- .Setup(f => f.CreateLogger(typeof(FoundryMemoryProvider).FullName!))
- .Returns(this._loggerMock.Object);
-
- this._loggerMock
- .Setup(f => f.IsEnabled(It.IsAny()))
- .Returns(true);
- }
-
[Fact]
- public void Constructor_Throws_WhenOperationsIsNull()
+ public void Constructor_Throws_WhenClientIsNull()
{
// Act & Assert
- var ex = Assert.Throws(() => new FoundryMemoryProvider(
- (IFoundryMemoryOperations)null!,
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ null!,
new FoundryMemoryProviderScope { Scope = "test" },
new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
- Assert.Equal("operations", ex.ParamName);
+ Assert.Equal("client", ex.ParamName);
}
[Fact]
public void Constructor_Throws_WhenScopeIsNull()
{
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
// Act & Assert
- var ex = Assert.Throws(() => new FoundryMemoryProvider(
- this._operationsMock.Object,
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
null!,
new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
Assert.Equal("scope", ex.ParamName);
@@ -66,9 +44,12 @@ public void Constructor_Throws_WhenScopeIsNull()
[Fact]
public void Constructor_Throws_WhenScopeValueIsEmpty()
{
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
// Act & Assert
- var ex = Assert.Throws(() => new FoundryMemoryProvider(
- this._operationsMock.Object,
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
new FoundryMemoryProviderScope(),
new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
Assert.StartsWith("The Scope property must be provided.", ex.Message);
@@ -77,257 +58,86 @@ public void Constructor_Throws_WhenScopeValueIsEmpty()
[Fact]
public void Constructor_Throws_WhenMemoryStoreNameIsMissing()
{
+ // Arrange
+ using TestableAIProjectClient testClient = new();
+
// Act & Assert
- var ex = Assert.Throws(() => new FoundryMemoryProvider(
- this._operationsMock.Object,
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
new FoundryMemoryProviderScope { Scope = "test" },
new FoundryMemoryProviderOptions()));
Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
}
[Fact]
- public void DeserializingConstructor_Throws_WithEmptyJsonElement()
+ public void Constructor_Throws_WhenMemoryStoreNameIsNull()
{
// Arrange
- JsonElement jsonElement = JsonSerializer.SerializeToElement(new object(), FoundryMemoryJsonUtilities.DefaultOptions);
+ using TestableAIProjectClient testClient = new();
// Act & Assert
- var ex = Assert.Throws(() => new FoundryMemoryProvider(
- this._operationsMock.Object,
- jsonElement,
- options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
- Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
- }
-
- [Fact]
- public async Task InvokingAsync_PerformsSearch_AndReturnsContextMessageAsync()
- {
- // Arrange
- this._operationsMock
- .Setup(o => o.SearchMemoriesAsync(
- "my-store",
- "user-123",
- It.IsAny>(),
- 5,
- It.IsAny()))
- .ReturnsAsync(["User prefers dark roast coffee", "User is from Seattle"]);
-
- FoundryMemoryProviderScope scope = new() { Scope = "user-123" };
- FoundryMemoryProviderOptions options = new()
- {
- MemoryStoreName = "my-store",
- EnableSensitiveTelemetryData = true
- };
-
- FoundryMemoryProvider sut = new(this._operationsMock.Object, scope, options);
- AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "What are my coffee preferences?")]);
-
- // Act
- AIContext aiContext = await sut.InvokingAsync(invokingContext);
-
- // Assert
- this._operationsMock.Verify(
- o => o.SearchMemoriesAsync("my-store", "user-123", It.IsAny>(), 5, It.IsAny()),
- Times.Once);
-
- Assert.NotNull(aiContext.Messages);
- ChatMessage contextMessage = Assert.Single(aiContext.Messages);
- Assert.Equal(ChatRole.User, contextMessage.Role);
- Assert.Contains("User prefers dark roast coffee", contextMessage.Text);
- Assert.Contains("User is from Seattle", contextMessage.Text);
- }
-
- [Fact]
- public async Task InvokingAsync_ReturnsEmptyContext_WhenNoMemoriesFoundAsync()
- {
- // Arrange
- this._operationsMock
- .Setup(o => o.SearchMemoriesAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(Array.Empty());
-
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
- AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "Hello")]);
-
- // Act
- AIContext aiContext = await sut.InvokingAsync(invokingContext);
-
- // Assert
- Assert.NotNull(aiContext.Messages);
- ChatMessage contextMessage = Assert.Single(aiContext.Messages);
- Assert.True(string.IsNullOrEmpty(contextMessage.Text)); // Text is null or empty when no memories found
- }
-
- [Fact]
- public async Task InvokingAsync_ShouldNotThrow_WhenSearchFailsAsync()
- {
- // Arrange
- this._operationsMock
- .Setup(o => o.SearchMemoriesAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .ThrowsAsync(new InvalidOperationException("Search failed"));
-
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" }, this._loggerFactoryMock.Object);
- AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "Q?")]);
-
- // Act
- AIContext aiContext = await sut.InvokingAsync(invokingContext, CancellationToken.None);
-
- // Assert
- Assert.Null(aiContext.Messages);
- Assert.Null(aiContext.Tools);
- this._loggerMock.Verify(
- l => l.Log(
- LogLevel.Error,
- It.IsAny(),
- It.Is((v, t) => v.ToString()!.Contains("FoundryMemoryProvider: Failed to search for memories due to error")),
- It.IsAny(),
- It.IsAny>()),
- Times.Once);
- }
-
- [Fact]
- public async Task InvokedAsync_PersistsAllowedMessagesAsync()
- {
- // Arrange
- IEnumerable? capturedMessages = null;
- this._operationsMock
- .Setup(o => o.UpdateMemoriesAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .Callback, int, CancellationToken>((_, _, msgs, _, _) => capturedMessages = msgs)
- .Returns(Task.CompletedTask);
-
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
-
- List requestMessages =
- [
- new(ChatRole.User, "User text"),
- new(ChatRole.System, "System text"),
- new(ChatRole.Tool, "Tool text should be ignored")
- ];
- List responseMessages = [new(ChatRole.Assistant, "Assistant text")];
-
- // Act
- await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
-
- // Assert
- this._operationsMock.Verify(
- o => o.UpdateMemoriesAsync("my-store", "user-123", It.IsAny>(), 0, It.IsAny()),
- Times.Once);
-
- Assert.NotNull(capturedMessages);
- List messagesList = [.. capturedMessages];
- Assert.Equal(3, messagesList.Count); // user, system, assistant (tool excluded)
- Assert.Contains(messagesList, m => m.Role == "user" && m.Content == "User text");
- Assert.Contains(messagesList, m => m.Role == "system" && m.Content == "System text");
- Assert.Contains(messagesList, m => m.Role == "assistant" && m.Content == "Assistant text");
- Assert.DoesNotContain(messagesList, m => m.Content == "Tool text should be ignored");
- }
-
- [Fact]
- public async Task InvokedAsync_PersistsNothingForFailedRequestAsync()
- {
- // Arrange
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
-
- List requestMessages =
- [
- new(ChatRole.User, "User text"),
- new(ChatRole.System, "System text")
- ];
-
- // Act
- await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null)
- {
- ResponseMessages = null,
- InvokeException = new InvalidOperationException("Request Failed")
- });
-
- // Assert
- this._operationsMock.Verify(
- o => o.UpdateMemoriesAsync(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny()),
- Times.Never);
+ ArgumentException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ new FoundryMemoryProviderScope { Scope = "test" },
+ null));
+ Assert.StartsWith("The MemoryStoreName option must be provided.", ex.Message);
}
[Fact]
- public async Task InvokedAsync_ShouldNotThrow_WhenStorageFailsAsync()
+ public void DeserializingConstructor_Throws_WhenClientIsNull()
{
- // Arrange
- this._operationsMock
- .Setup(o => o.UpdateMemoriesAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .ThrowsAsync(new InvalidOperationException("Storage failed"));
-
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" }, this._loggerFactoryMock.Object);
+ // Arrange - use source-generated JSON context
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(
+ new TestState { Scope = new TestScope { Scope = "test" } },
+ TestJsonContext.Default.TestState);
- List requestMessages = [new(ChatRole.User, "User text")];
- List responseMessages = [new(ChatRole.Assistant, "Assistant text")];
-
- // Act
- await sut.InvokedAsync(new AIContextProvider.InvokedContext(requestMessages, aiContextProviderMessages: null) { ResponseMessages = responseMessages });
-
- // Assert
- this._loggerMock.Verify(
- l => l.Log(
- LogLevel.Error,
- It.IsAny(),
- It.Is((v, t) => v.ToString()!.Contains("FoundryMemoryProvider: Failed to send messages to update memories due to error")),
- It.IsAny(),
- It.IsAny>()),
- Times.Once);
+ // Act & Assert
+ ArgumentNullException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ null!,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.Equal("client", ex.ParamName);
}
[Fact]
- public async Task EnsureStoredMemoriesDeletedAsync_SendsDeleteRequestAsync()
+ public void DeserializingConstructor_Throws_WithEmptyJsonElement()
{
// Arrange
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
-
- // Act
- await sut.EnsureStoredMemoriesDeletedAsync();
+ using TestableAIProjectClient testClient = new();
+ JsonElement jsonElement = JsonDocument.Parse("{}").RootElement;
- // Assert
- this._operationsMock.Verify(
- o => o.DeleteScopeAsync("my-store", "user-123", It.IsAny()),
- Times.Once);
+ // Act & Assert
+ InvalidOperationException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
}
[Fact]
- public async Task EnsureStoredMemoriesDeletedAsync_Handles404GracefullyAsync()
+ public void DeserializingConstructor_Throws_WithMissingScopeValue()
{
// Arrange
- this._operationsMock
- .Setup(o => o.DeleteScopeAsync(It.IsAny(), It.IsAny(), It.IsAny()))
- .ThrowsAsync(new ClientResultException(new MockPipelineResponse(404)));
-
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+ using TestableAIProjectClient testClient = new();
+ JsonElement jsonElement = JsonSerializer.SerializeToElement(
+ new TestState { Scope = new TestScope() },
+ TestJsonContext.Default.TestState);
- // Act & Assert - should not throw
- await sut.EnsureStoredMemoriesDeletedAsync();
+ // Act & Assert
+ InvalidOperationException ex = Assert.Throws(() => new FoundryMemoryProvider(
+ testClient.Client,
+ jsonElement,
+ options: new FoundryMemoryProviderOptions { MemoryStoreName = "store" }));
+ Assert.StartsWith("The FoundryMemoryProvider state did not contain the required scope property.", ex.Message);
}
[Fact]
public void Serialize_RoundTripsScope()
{
// Arrange
+ using TestableAIProjectClient testClient = new();
FoundryMemoryProviderScope scope = new() { Scope = "user-456" };
- FoundryMemoryProvider sut = new(this._operationsMock.Object, scope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
+ FoundryMemoryProvider sut = new(testClient.Client, scope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
// Act
JsonElement stateElement = sut.Serialize();
@@ -338,106 +148,22 @@ public void Serialize_RoundTripsScope()
Assert.Equal("user-456", scopeElement.GetProperty("scope").GetString());
}
- [Theory]
- [InlineData(true, "user-123")]
- [InlineData(false, "")]
- public async Task InvokingAsync_LogsScopeBasedOnEnableSensitiveTelemetryDataAsync(bool enableSensitiveTelemetryData, string expectedScopeInLog)
+ [Fact]
+ public void DeserializingConstructor_RestoresScope()
{
// Arrange
- this._operationsMock
- .Setup(o => o.SearchMemoriesAsync(
- It.IsAny(),
- It.IsAny(),
- It.IsAny>(),
- It.IsAny(),
- It.IsAny()))
- .ReturnsAsync(["test memory"]);
-
- FoundryMemoryProviderOptions options = new()
- {
- MemoryStoreName = "my-store",
- EnableSensitiveTelemetryData = enableSensitiveTelemetryData
- };
- FoundryMemoryProvider sut = new(this._operationsMock.Object, new FoundryMemoryProviderScope { Scope = "user-123" }, options, this._loggerFactoryMock.Object);
-
- AIContextProvider.InvokingContext invokingContext = new([new ChatMessage(ChatRole.User, "test")]);
+ using TestableAIProjectClient testClient = new();
+ FoundryMemoryProviderScope originalScope = new() { Scope = "restored-user-789" };
+ FoundryMemoryProvider original = new(testClient.Client, originalScope, new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
// Act
- await sut.InvokingAsync(invokingContext, CancellationToken.None);
-
- // Assert
- this._loggerMock.Verify(
- l => l.Log(
- LogLevel.Information,
- It.IsAny(),
- It.Is((v, t) =>
- v.ToString()!.Contains("Retrieved 1 memories") &&
- v.ToString()!.Contains($"Scope: '{expectedScopeInLog}'")),
- It.IsAny(),
- It.IsAny>()),
- Times.Once);
- }
-
- private sealed class MockPipelineResponse : PipelineResponse
- {
- private readonly int _status;
- private readonly MockPipelineResponseHeaders _headers;
-
- public MockPipelineResponse(int status)
- {
- this._status = status;
- this.Content = BinaryData.Empty;
- this._headers = new MockPipelineResponseHeaders();
- }
+ JsonElement serializedState = original.Serialize();
+ FoundryMemoryProvider restored = new(testClient.Client, serializedState, options: new FoundryMemoryProviderOptions { MemoryStoreName = "my-store" });
- public override int Status => this._status;
-
- public override string ReasonPhrase => this._status == 404 ? "Not Found" : "OK";
-
- public override Stream? ContentStream
- {
- get => null;
- set { }
- }
-
- public override BinaryData Content { get; }
-
- protected override PipelineResponseHeaders HeadersCore => this._headers;
-
- public override BinaryData BufferContent(CancellationToken cancellationToken = default) => this.Content;
-
- public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) =>
- new(this.Content);
-
- public override void Dispose()
- {
- }
-
- private sealed class MockPipelineResponseHeaders : PipelineResponseHeaders
- {
- private readonly Dictionary _headers = new(StringComparer.OrdinalIgnoreCase);
-
- public override bool TryGetValue(string name, out string? value)
- {
- return this._headers.TryGetValue(name, out value);
- }
-
- public override bool TryGetValues(string name, out IEnumerable? values)
- {
- if (this._headers.TryGetValue(name, out string? value))
- {
- values = [value];
- return true;
- }
-
- values = null;
- return false;
- }
-
- public override IEnumerator> GetEnumerator()
- {
- return this._headers.GetEnumerator();
- }
- }
+ // Assert - serialize again to verify scope was restored
+ JsonElement restoredState = restored.Serialize();
+ using JsonDocument doc = JsonDocument.Parse(restoredState.GetRawText());
+ Assert.True(doc.RootElement.TryGetProperty("scope", out JsonElement scopeElement));
+ Assert.Equal("restored-user-789", scopeElement.GetProperty("scope").GetString());
}
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
new file mode 100644
index 0000000000..48d022d49a
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
@@ -0,0 +1,215 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.ClientModel.Primitives;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Azure.AI.Projects;
+using Azure.Core;
+
+namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
+
+///
+/// Creates a testable AIProjectClient with a mock HTTP handler.
+///
+internal sealed class TestableAIProjectClient : IDisposable
+<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
+ private readonly MockHttpMessageHandler _handler;
+ private readonly HttpClient _httpClient;
+=======
+ private readonly HttpClient _httpClient;
+>>>>>>> After
+
+{
+ private readonly HttpClient _httpClient;
+
+ public TestableAIProjectClient(
+ string? searchMemoriesResponse = null,
+ string? updateMemoriesResponse = null,
+ HttpStatusCode? searchStatusCode = null,
+ HttpStatusCode? updateStatusCode = null,
+ HttpStatusCode? deleteStatusCode = null,
+ HttpStatusCode? createStoreStatusCode = null,
+ HttpStatusCode? getStoreStatusCode = null)
+ {
+ this.Handler = new MockHttpMessageHandler(
+ searchMemoriesResponse,
+ updateMemoriesResponse,
+ searchStatusCode,
+ updateStatusCode,
+ deleteStatusCode,
+ createStoreStatusCode,
+ getStoreStatusCode);
+
+<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
+ this._httpClient = new HttpClient(this._handler);
+=======
+ this._httpClient = new HttpClient(this.Handler);
+>>>>>>> After
+ this._httpClient = new HttpClient(this.Handler);
+
+ AIProjectClientOptions options = new()
+ {
+ Transport = new HttpClientPipelineTransport(this._httpClient)
+ };
+
+ // Using a valid format endpoint
+ this.Client = new AIProjectClient(
+ new Uri("https://test.services.ai.azure.com/api/projects/test-project"),
+ new MockTokenCredential(),
+ options);
+ }
+
+ public AIProjectClient Client { get; }
+
+
+<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
+ public MockHttpMessageHandler Handler => this._handler;
+=======
+ public MockHttpMessageHandler Handler { get; }
+>>>>>>> After
+ public MockHttpMessageHandler Handler { get; }
+
+ public void Dispose()
+ {
+ this._httpClient.Dispose();
+ this.Handler.Dispose();
+ }
+}
+
+///
+/// Mock HTTP message handler for testing.
+///
+internal sealed class MockHttpMessageHandler : HttpMessageHandler
+{
+ private readonly string? _searchMemoriesResponse;
+ private readonly string? _updateMemoriesResponse;
+ private readonly HttpStatusCode _searchStatusCode;
+ private readonly HttpStatusCode _updateStatusCode;
+ private readonly HttpStatusCode _deleteStatusCode;
+ private readonly HttpStatusCode _createStoreStatusCode;
+ private readonly HttpStatusCode _getStoreStatusCode;
+
+ public MockHttpMessageHandler(
+ string? searchMemoriesResponse = null,
+ string? updateMemoriesResponse = null,
+ HttpStatusCode? searchStatusCode = null,
+ HttpStatusCode? updateStatusCode = null,
+ HttpStatusCode? deleteStatusCode = null,
+ HttpStatusCode? createStoreStatusCode = null,
+ HttpStatusCode? getStoreStatusCode = null)
+ {
+ this._searchMemoriesResponse = searchMemoriesResponse ?? """{"memories":[]}""";
+ this._updateMemoriesResponse = updateMemoriesResponse ?? """{"update_id":"test-update-id","status":"queued"}""";
+ this._searchStatusCode = searchStatusCode ?? HttpStatusCode.OK;
+ this._updateStatusCode = updateStatusCode ?? HttpStatusCode.OK;
+ this._deleteStatusCode = deleteStatusCode ?? HttpStatusCode.NoContent;
+ this._createStoreStatusCode = createStoreStatusCode ?? HttpStatusCode.Created;
+ this._getStoreStatusCode = getStoreStatusCode ?? HttpStatusCode.NotFound;
+ }
+
+ public string? LastRequestUri { get; private set; }
+ public string? LastRequestBody { get; private set; }
+ public HttpMethod? LastRequestMethod { get; private set; }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ this.LastRequestUri = request.RequestUri?.ToString();
+ this.LastRequestMethod = request.Method;
+
+ if (request.Content != null)
+ {
+#if NET472
+ this.LastRequestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+#else
+ this.LastRequestBody = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+#endif
+ }
+
+ string path = request.RequestUri?.AbsolutePath ?? "";
+
+ // Route based on path and method
+ if (path.Contains("/memory-stores/") && path.Contains("/search") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._searchStatusCode, this._searchMemoriesResponse);
+ }
+
+ if (path.Contains("/memory-stores/") && path.Contains("/memories") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._updateStatusCode, this._updateMemoriesResponse);
+ }
+
+ if (path.Contains("/memory-stores/") && path.Contains("/scopes") && request.Method == HttpMethod.Delete)
+ {
+ return CreateResponse(this._deleteStatusCode, "");
+ }
+
+ if (path.Contains("/memory-stores") && request.Method == HttpMethod.Post)
+ {
+ return CreateResponse(this._createStoreStatusCode, """{"name":"test-store","status":"active"}""");
+ }
+
+ if (path.Contains("/memory-stores/") && request.Method == HttpMethod.Get)
+ {
+ return CreateResponse(this._getStoreStatusCode, """{"name":"test-store","status":"active"}""");
+ }
+
+ // Default response
+ return CreateResponse(HttpStatusCode.NotFound, "{}");
+ }
+
+ private static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string? content)
+ {
+ return new HttpResponseMessage(statusCode)
+ {
+ Content = new StringContent(content ?? "{}", Encoding.UTF8, "application/json")
+ };
+ }
+}
+
+///
+/// Mock token credential for testing.
+///
+internal sealed class MockTokenCredential : TokenCredential
+{
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1));
+ }
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(new AccessToken("mock-token", DateTimeOffset.UtcNow.AddHours(1)));
+ }
+}
+
+///
+/// Source-generated JSON serializer context for unit test types.
+///
+[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(TestState))]
+[JsonSerializable(typeof(TestScope))]
+internal sealed partial class TestJsonContext : JsonSerializerContext
+{
+}
+
+///
+/// Test state class for deserialization tests.
+///
+internal sealed class TestState
+{
+ public TestScope? Scope { get; set; }
+}
+
+///
+/// Test scope class for deserialization tests.
+///
+internal sealed class TestScope
+{
+ public string? Scope { get; set; }
+}
From 96fc1ce0bdd5d8e35bd242b3d18c1a5d841db7ef Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Fri, 30 Jan 2026 17:25:25 +0000
Subject: [PATCH 5/8] Add waiting operation for memory store updates
---
.../Program.cs | 69 +++++++++-------
.../Core/Models/UpdateMemoriesResponse.cs | 15 ++++
.../FoundryMemoryProvider.cs | 78 +++++++++++++++++++
.../TestableAIProjectClient.cs | 19 -----
4 files changed, 132 insertions(+), 49 deletions(-)
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
index b4f4270bee..3604c46698 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -62,16 +62,11 @@
Console.WriteLine(await agent.RunAsync("I'm travelling with my sister and we love finding scenic viewpoints.", session));
// Memory extraction in Azure AI Foundry is asynchronous and takes time to process.
-// In production scenarios, you would:
-// 1. Track the UpdateId returned from each memory update operation
-// 2. Poll the GetUpdateResultAsync API to check completion status
-// 3. Wait for status to become "completed" before querying memories
-//
-// For simplicity, this sample uses a fixed delay. For production code, implement proper polling
-// using the Azure.AI.Projects SDK's MemoryStores.GetUpdateResultAsync method.
+// WhenUpdatesCompletedAsync polls all pending updates and waits for them to complete.
Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
-await Task.Delay(TimeSpan.FromSeconds(10));
-Console.WriteLine("Continuing after delay.\n");
+await memoryProvider.WhenUpdatesCompletedAsync();
+
+Console.WriteLine("Updates completed.\n");
Console.WriteLine(await agent.RunAsync("What do you already know about my upcoming trip?", session));
@@ -81,6 +76,10 @@
Console.WriteLine(await agent.RunAsync("Can you recap the personal details you remember?", restoredSession));
Console.WriteLine("\n>> Start a new session that shares the same Foundry Memory scope\n");
+
+Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
+await memoryProvider.WhenUpdatesCompletedAsync();
+
AgentSession newSession = await agent.GetNewSessionAsync();
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
@@ -89,26 +88,36 @@ internal sealed class DebugHttpClientHandler : HttpClientHandler
{
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- // Uncomment to debug HTTP traffic:
- // Console.WriteLine("\n=== HTTP REQUEST ===");
- // Console.WriteLine($"Method: {request.Method}");
- // Console.WriteLine($"URI: {request.RequestUri}");
- //
- // if (request.Content != null)
- // {
- // string body = await request.Content.ReadAsStringAsync(cancellationToken);
- // Console.WriteLine("Body: " + body);
- // }
- //
- // Console.WriteLine("====================\n");
-
- // Uncomment to debug HTTP traffic:
- // Console.WriteLine("\n=== HTTP RESPONSE ===");
- // Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");
- // string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
- // Console.WriteLine("Body: " + responseBody);
- // Console.WriteLine("=====================\n");
-
- return await base.SendAsync(request, cancellationToken);
+ if (!request.RequestUri?.PathAndQuery.Contains("sample-memory-store-name") ?? false)
+ {
+ return await base.SendAsync(request, cancellationToken);
+ }
+
+ Console.WriteLine("\n=== HTTP REQUEST ===");
+ Console.WriteLine($"Method: {request.Method}");
+ Console.WriteLine($"URI: {request.RequestUri}");
+ Console.WriteLine("Headers:");
+ foreach (var header in request.Headers)
+ {
+ Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
+ }
+
+ if (request.Content != null)
+ {
+ string body = await request.Content.ReadAsStringAsync(cancellationToken);
+ Console.WriteLine("Body: " + body);
+ }
+
+ HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
+
+ Console.WriteLine("====================\n");
+
+ Console.WriteLine("\n=== HTTP RESPONSE ===");
+ Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");
+ string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+ Console.WriteLine("Body: " + responseBody);
+ Console.WriteLine("=====================\n");
+
+ return response;
}
}
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
index 7ab9bdf68f..9f6cd9651b 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Core/Models/UpdateMemoriesResponse.cs
@@ -9,6 +9,21 @@ namespace Microsoft.Agents.AI.FoundryMemory.Core.Models;
///
internal sealed class UpdateMemoriesResponse
{
+ /// Status indicating the update is waiting to be processed.
+ internal const string StatusQueued = "queued";
+
+ /// Status indicating the update is currently being processed.
+ internal const string StatusInProgress = "in_progress";
+
+ /// Status indicating the update completed successfully.
+ internal const string StatusCompleted = "completed";
+
+ /// Status indicating the update failed.
+ internal const string StatusFailed = "failed";
+
+ /// Status indicating the update was superseded by a newer update.
+ internal const string StatusSuperseded = "superseded";
+
///
/// Gets or sets the unique identifier of the update operation.
///
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
index 5ef25d49aa..24c604ce8e 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/FoundryMemoryProvider.cs
@@ -2,6 +2,8 @@
using System;
using System.ClientModel;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -38,6 +40,7 @@ public sealed class FoundryMemoryProvider : AIContextProvider
private readonly ILogger? _logger;
private readonly FoundryMemoryProviderScope _scope;
+ private readonly ConcurrentQueue _pendingUpdateIds = new();
///
/// Initializes a new instance of the class.
@@ -237,6 +240,11 @@ public override async ValueTask InvokedAsync(InvokedContext context, Cancellatio
this._updateDelay,
cancellationToken).ConfigureAwait(false);
+ if (response?.UpdateId is not null)
+ {
+ this._pendingUpdateIds.Enqueue(response.UpdateId);
+ }
+
if (this._logger?.IsEnabled(LogLevel.Information) is true)
{
this._logger.LogInformation(
@@ -324,6 +332,76 @@ public async Task EnsureMemoryStoreCreatedAsync(
}
}
+ ///
+ /// Waits for all pending memory update operations to complete.
+ ///
+ ///
+ /// Memory extraction in Azure AI Foundry is asynchronous. This method polls all pending updates
+ /// in parallel and returns when all have completed, failed, or been superseded.
+ ///
+ /// The interval between status checks. Defaults to 5 seconds.
+ /// Cancellation token.
+ /// Thrown if any update operation failed, containing all failures.
+ public async Task WhenUpdatesCompletedAsync(
+ TimeSpan? pollingInterval = null,
+ CancellationToken cancellationToken = default)
+ {
+ TimeSpan interval = pollingInterval ?? TimeSpan.FromSeconds(5);
+
+ // Collect all pending update IDs
+ List updateIds = [];
+ while (this._pendingUpdateIds.TryDequeue(out string? updateId))
+ {
+ updateIds.Add(updateId);
+ }
+
+ if (updateIds.Count == 0)
+ {
+ return;
+ }
+
+ // Poll all updates in parallel
+ await Task.WhenAll(updateIds.Select(updateId => this.WaitForUpdateAsync(updateId, interval, cancellationToken))).ConfigureAwait(false);
+ }
+
+ private async Task WaitForUpdateAsync(string updateId, TimeSpan interval, CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ UpdateMemoriesResponse? response = await this._client.GetUpdateStatusAsync(
+ this._memoryStoreName,
+ updateId,
+ cancellationToken).ConfigureAwait(false);
+
+ string status = response?.Status ?? "unknown";
+
+ if (this._logger?.IsEnabled(LogLevel.Debug) is true)
+ {
+ this._logger.LogDebug(
+ "FoundryMemoryProvider: Update status for '{UpdateId}': {Status}",
+ updateId,
+ status);
+ }
+
+ switch (status)
+ {
+ case UpdateMemoriesResponse.StatusCompleted:
+ case UpdateMemoriesResponse.StatusSuperseded:
+ return;
+ case UpdateMemoriesResponse.StatusFailed:
+ throw new InvalidOperationException($"Memory update operation '{updateId}' failed: {response?.Error?.Message}");
+ case UpdateMemoriesResponse.StatusQueued:
+ case UpdateMemoriesResponse.StatusInProgress:
+ await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown update status '{status}' for update '{updateId}'.");
+ }
+ }
+ }
+
///
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
index 48d022d49a..01df256a30 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
@@ -5,7 +5,6 @@
using System.Net;
using System.Net.Http;
using System.Text;
-using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -18,13 +17,6 @@ namespace Microsoft.Agents.AI.FoundryMemory.UnitTests;
/// Creates a testable AIProjectClient with a mock HTTP handler.
///
internal sealed class TestableAIProjectClient : IDisposable
-<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
- private readonly MockHttpMessageHandler _handler;
- private readonly HttpClient _httpClient;
-=======
- private readonly HttpClient _httpClient;
->>>>>>> After
-
{
private readonly HttpClient _httpClient;
@@ -46,11 +38,6 @@ public TestableAIProjectClient(
createStoreStatusCode,
getStoreStatusCode);
-<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
- this._httpClient = new HttpClient(this._handler);
-=======
- this._httpClient = new HttpClient(this.Handler);
->>>>>>> After
this._httpClient = new HttpClient(this.Handler);
AIProjectClientOptions options = new()
@@ -67,12 +54,6 @@ public TestableAIProjectClient(
public AIProjectClient Client { get; }
-
-<<<<<<< TODO: Unmerged change from project 'Microsoft.Agents.AI.FoundryMemory.UnitTests(net472)', Before:
- public MockHttpMessageHandler Handler => this._handler;
-=======
- public MockHttpMessageHandler Handler { get; }
->>>>>>> After
public MockHttpMessageHandler Handler { get; }
public void Dispose()
From f22a8c574911ac64224b34b915f7c3c7f831ffcd Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Tue, 3 Feb 2026 12:57:24 +0000
Subject: [PATCH 6/8] Fix UTF-8 BOM encoding for FoundryMemory csproj files
---
.../Microsoft.Agents.AI.FoundryMemory.csproj | 2 +-
.../Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj | 2 +-
.../Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
index 0aa2dbb3e6..bc3baac152 100644
--- a/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.FoundryMemory/Microsoft.Agents.AI.FoundryMemory.csproj
@@ -1,4 +1,4 @@
-
+
preview
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
index d4cabd7687..652178aef8 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj
@@ -1,4 +1,4 @@
-
+
True
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
index 9973e6f247..1fe8dc57bd 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/Microsoft.Agents.AI.FoundryMemory.UnitTests.csproj
@@ -1,4 +1,4 @@
-
+
false
From 43aa0533cf602dad909a84fa23582333f7566f42 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Tue, 3 Feb 2026 13:38:01 +0000
Subject: [PATCH 7/8] Update copilot instructions for UTF-8 BOM and fix sample
API rename
---
.github/copilot-instructions.md | 1 +
.../AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 5866f1f895..2c57bd5071 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -12,6 +12,7 @@ When contributing to this repository, please follow these guidelines:
Here are some general guidelines that apply to all code.
+- All new files must be saved with UTF-8 encoding with BOM (Byte Order Mark). This is required for `dotnet format` to work correctly.
- The top of all *.cs files should have a copyright notice: `// Copyright (c) Microsoft. All rights reserved.`
- All public methods and classes should have XML documentation comments.
- After adding, modifying or deleting code, run `dotnet build`, and then fix any reported build errors.
diff --git a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
index 3604c46698..8125ffcb0b 100644
--- a/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
+++ b/dotnet/samples/GettingStarted/AgentWithMemory/AgentWithMemory_Step04_MemoryUsingFoundry/Program.cs
@@ -46,7 +46,7 @@
: new FoundryMemoryProvider(projectClient, ctx.SerializedState, ctx.JsonSerializerOptions, new FoundryMemoryProviderOptions() { MemoryStoreName = memoryStoreName }))
});
-AgentSession session = await agent.GetNewSessionAsync();
+AgentSession session = await agent.CreateSessionAsync();
FoundryMemoryProvider memoryProvider = session.GetService()!;
@@ -80,7 +80,7 @@
Console.WriteLine("\nWaiting for Foundry Memory to process updates...");
await memoryProvider.WhenUpdatesCompletedAsync();
-AgentSession newSession = await agent.GetNewSessionAsync();
+AgentSession newSession = await agent.CreateSessionAsync();
Console.WriteLine(await agent.RunAsync("Summarize what you already know about me.", newSession));
// Debug HTTP handler to log all requests (commented out by default)
From 74f48f8dc65c7e9d2ad5447a4a7db3cabf2c9390 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Wed, 4 Feb 2026 11:36:48 +0000
Subject: [PATCH 8/8] Fix UTF-8 BOM encoding for TestableAIProjectClient.cs
---
.../TestableAIProjectClient.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
index 01df256a30..25c041f754 100644
--- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.UnitTests/TestableAIProjectClient.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft. All rights reserved.
+// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel.Primitives;