diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 3ba3f3b13b..ac7eb21424 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -38,7 +38,7 @@ - + @@ -104,6 +104,7 @@ + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 9442f98cb1..40c791159b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -187,6 +187,7 @@ + diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs index b19217230a..f629bdf600 100644 --- a/dotnet/eng/verify-samples/AgentsSamples.cs +++ b/dotnet/eng/verify-samples/AgentsSamples.cs @@ -532,6 +532,26 @@ internal static class AgentsSamples ], }, + new SampleDefinition + { + Name = "AgentWithMemory_Step08_MemoryUsingCosmosNoSql", + ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql", + RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "COSMOS_ENDPOINT"], + OptionalEnvironmentVariables = ["FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "COSMOS_DATABASE_NAME"], + MustContain = + [ + "First session:", + "Second session (recalling prior chat history from Cosmos DB):", + ], + ExpectedOutputDescription = + [ + "The output should contain two joke responses.", + "The first joke should be about a pirate (as explicitly requested).", + "The second joke should also be pirate-themed or similar to what the user likes, since chat history from the first session should be recalled from Cosmos DB.", + "The output should not contain error messages or stack traces.", + ], + }, + // ── AgentWithRAG ──────────────────────────────────────────────────── new SampleDefinition diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/AgentWithMemory_Step08_MemoryUsingCosmosNoSql.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/AgentWithMemory_Step08_MemoryUsingCosmosNoSql.csproj new file mode 100644 index 0000000000..6aacaa5662 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/AgentWithMemory_Step08_MemoryUsingCosmosNoSql.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/Program.cs new file mode 100644 index 0000000000..4552d04653 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/Program.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft. All rights reserved. + +// This sample shows how to persist chat history in Azure Cosmos DB for NoSQL using the ChatHistoryMemoryProvider. +// The agent can then use chat history from prior conversations to inform responses in new conversations. + +using System.Text.Json; +using Azure.AI.Projects; +using Azure.Identity; +using CommunityToolkit.VectorData.CosmosNoSql; +using Microsoft.Agents.AI; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.VectorData; + +var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."); +var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini"; +var embeddingDeploymentName = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-large"; +var embeddingDimensions = 3072; +if (Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_DIMENSIONS") is string embeddingDimensionsValue && + (!int.TryParse(embeddingDimensionsValue, out embeddingDimensions) || embeddingDimensions <= 0)) +{ + throw new InvalidOperationException("FOUNDRY_EMBEDDING_DIMENSIONS must be a positive integer."); +} +var cosmosEndpoint = Environment.GetEnvironmentVariable("COSMOS_ENDPOINT") ?? throw new InvalidOperationException("COSMOS_ENDPOINT is not set."); +var cosmosDatabaseName = Environment.GetEnvironmentVariable("COSMOS_DATABASE_NAME") ?? "agent-memory"; + +// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production. +// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid +// latency issues, unintended credential probing, and potential security risks from fallback mechanisms. +DefaultAzureCredential credential = new(); +AIProjectClient aiProjectClient = new(new Uri(endpoint), credential); + +using CosmosClient cosmosClient = new( + cosmosEndpoint, + credential, + new CosmosClientOptions + { + UseSystemTextJsonSerializerWithOptions = JsonSerializerOptions.Default, + }); + +DatabaseResponse databaseResponse = await cosmosClient.CreateDatabaseIfNotExistsAsync(cosmosDatabaseName); + +VectorStore vectorStore = new CosmosNoSqlVectorStore( + databaseResponse.Database, + new CosmosNoSqlVectorStoreOptions + { + JsonSerializerOptions = JsonSerializerOptions.Default, + EmbeddingGenerator = aiProjectClient + .GetProjectOpenAIClient() + .GetEmbeddingClient(embeddingDeploymentName) + .AsIEmbeddingGenerator(), + }); + +var userId = $"sample-{Guid.NewGuid():N}"; + +// Create the agent and add the ChatHistoryMemoryProvider to store chat messages in Cosmos DB. +AIAgent agent = aiProjectClient + .AsAIAgent(new ChatClientAgentOptions + { + ChatOptions = new() { ModelId = deploymentName, Instructions = "You are good at telling jokes." }, + Name = "Joker", + AIContextProviders = [new ChatHistoryMemoryProvider( + vectorStore, + collectionName: "chathistory", + vectorDimensions: embeddingDimensions, + // Callback to configure the initial state of the ChatHistoryMemoryProvider. + // The ChatHistoryMemoryProvider stores its state in the AgentSession and this callback + // will be called whenever the ChatHistoryMemoryProvider cannot find existing state in the session, + // typically the first time it is used with a new session. + _ => new ChatHistoryMemoryProvider.State( + // Configure the scope values under which chat messages will be stored. + // In this case, we are using a per-run user ID and a unique session ID for each new session. + storageScope: new() { UserId = userId, SessionId = Guid.NewGuid().ToString("N") }, + // Configure the scope which would be used to search for relevant prior messages. + // In this case, we are searching for any messages for the user across all sessions. + searchScope: new() { UserId = userId }))] + }); + +// Start a new session for the agent conversation. +AgentSession session = await agent.CreateSessionAsync(); + +// Run the agent with the session that stores conversation history in Cosmos DB. +Console.WriteLine("First session:"); +Console.WriteLine(await agent.RunAsync("I like jokes about Pirates. Tell me a joke about a pirate.", session)); + +// Start a second session. Since we configured the search scope to be across all sessions for the user, +// the agent should remember that the user likes pirate jokes. +AgentSession session2 = await agent.CreateSessionAsync(); + +// Run the agent with the second session. +Console.WriteLine("Second session (recalling prior chat history from Cosmos DB):"); +Console.WriteLine(await agent.RunAsync("Tell me a joke that I might like.", session2)); diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/README.md new file mode 100644 index 0000000000..16775abcd8 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step08_MemoryUsingCosmosNoSql/README.md @@ -0,0 +1,41 @@ +# Agent with Memory Using Azure Cosmos DB for NoSQL + +This sample uses `ChatHistoryMemoryProvider` with `CosmosNoSqlVectorStore` to persist chat history in Azure Cosmos DB for NoSQL and recall relevant messages in a new agent session. + +## Features Demonstrated + +- Authenticating to Microsoft Foundry and Azure Cosmos DB with `DefaultAzureCredential` +- Storing chat messages in an Azure Cosmos DB vector store +- Creating the configured database and chat-history container when they do not exist +- Recalling relevant chat history across agent sessions + +## Prerequisites + +1. [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +2. A Microsoft Foundry project with: + - A chat model deployment (the default is `gpt-5.4-mini`) + - A `text-embedding-3-large` deployment with 3,072 dimensions +3. An Azure Cosmos DB for NoSQL account with [vector search enabled](https://learn.microsoft.com/azure/cosmos-db/nosql/vector-search) +4. An Azure identity that can create the configured database and container and read and write items +5. Azure CLI authentication (`az login`) + +## Configuration + +Set the following environment variables: + +| Variable | Description | Default | +|---|---|---| +| `FOUNDRY_PROJECT_ENDPOINT` | Microsoft Foundry project endpoint | *(required)* | +| `COSMOS_ENDPOINT` | Azure Cosmos DB account endpoint | *(required)* | +| `FOUNDRY_MODEL` | Chat model deployment name | `gpt-5.4-mini` | +| `FOUNDRY_EMBEDDING_MODEL` | Embedding model deployment name | `text-embedding-3-large` | +| `FOUNDRY_EMBEDDING_DIMENSIONS` | Number of dimensions produced by the embedding deployment | `3072` | +| `COSMOS_DATABASE_NAME` | Database used to store agent memory | `agent-memory` | + +## Run the Sample + +```bash +dotnet run +``` + +The first session stores the user's preference for pirate jokes. The second session uses a different `AgentSession` but the same per-run user search scope, allowing the agent to retrieve that preference from Azure Cosmos DB without recalling data from earlier sample runs. \ No newline at end of file diff --git a/dotnet/samples/02-agents/AgentWithMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/README.md index f1dc4064a5..16096f44fa 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/README.md @@ -11,6 +11,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem |[Bounded Chat History with Overflow](./AgentWithMemory_Step05_BoundedChatHistory/)|This sample demonstrates how to create a bounded chat history provider that overflows older messages to a vector store and recalls them as memories.| |[Memory Using AgentMemory](./AgentWithMemory_Step06_MemoryUsingAgentMemory/)|This sample demonstrates a retail shopping assistant built with [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), an unofficial .NET port of the Neo4j Labs graph-memory provider, to learn customer preferences and recommend products via graph traversal.| |[File Based Memory](./AgentWithMemory_Step07_FileMemoryProvider/)|This sample demonstrates how to use the `FileMemoryProvider` to give an agent tools for storing and recalling memories as files, and how to configure the folder that those memory files are written to.| +|[Memory with Azure Cosmos DB for NoSQL](./AgentWithMemory_Step08_MemoryUsingCosmosNoSql/)|This sample demonstrates how to persist and retrieve chat history across sessions with Azure Cosmos DB for NoSQL.| > **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents.