diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 27cf8605740..d720b835b74 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -200,6 +200,7 @@ + diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs index 2f36405002b..c9701304ef7 100644 --- a/dotnet/eng/verify-samples/AgentsSamples.cs +++ b/dotnet/eng/verify-samples/AgentsSamples.cs @@ -427,6 +427,15 @@ internal static class AgentsSamples ], }, + new SampleDefinition + { + Name = "AgentWithMemory_Step06_MemoryUsingAgentMemory", + ProjectPath = "samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory", + RequiredEnvironmentVariables = ["AZURE_OPENAI_ENDPOINT"], + OptionalEnvironmentVariables = ["AZURE_OPENAI_API_KEY", "FOUNDRY_MODEL", "FOUNDRY_EMBEDDING_MODEL", "NEO4J_URI", "NEO4J_USER", "NEO4J_PASSWORD"], + SkipReason = "Requires a running Neo4j instance; standalone sample outside the repo's CPM build.", + }, + // ── AgentWithRAG ──────────────────────────────────────────────────── new SampleDefinition diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj new file mode 100644 index 00000000000..c4e28440519 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj @@ -0,0 +1,78 @@ + + + + + Exe + net10.0 + enable + enable + false + AgentMemoryShoppingAssistant + + $(NoWarn);OPENAI001 + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs new file mode 100644 index 00000000000..2b28ec7ade6 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using System.Text; +using AgentMemory.Neo4j.Infrastructure; +using Microsoft.Extensions.AI; +using Neo4j.Driver; + +namespace AgentMemoryShoppingAssistant; + +/// +/// A small retail product graph plus the shopping tools that query it — the .NET counterpart of the +/// Python retail-assistant's get_product_tools. Products live in Neo4j as :Product nodes +/// linked to :ProductCategory / :ProductBrand nodes, so recommendations and "related +/// products" come from graph traversals. Cypher runs through the public +/// seam. Exposed as s so a real chat model can call them during a run — the same +/// way Neo4jMemoryContextProvider surfaces the memory tools through AIContext.Tools when +/// ExposeMemoryToolsFromContextProvider is enabled. +/// +public sealed class ProductCatalog(INeo4jTransactionRunner runner) +{ + private readonly INeo4jTransactionRunner _runner = runner; + + private static readonly (string Name, string Category, string Brand, double Price, bool InStock, int Inventory, string Description, int Popularity)[] s_seed = + [ + ("Nike Air Zoom Pegasus 40", "shoes", "Nike", 130, true, 40, "Everyday running shoe with responsive cushioning.", 95), + ("Nike Revolution 7", "shoes", "Nike", 70, true, 60, "Lightweight, budget-friendly running shoe.", 80), + ("Adidas Ultraboost Light", "shoes", "Adidas", 190, true, 25, "Premium running shoe with Boost cushioning.", 90), + ("Asics Gel-Kayano 31", "shoes", "Asics", 165, false, 0, "Stability running shoe for overpronation.", 70), + ("Sony WH-1000XM5", "electronics", "Sony", 350, true, 18, "Industry-leading noise-cancelling headphones.", 92), + ("Bose QuietComfort Ultra", "electronics", "Bose", 330, true, 12, "Premium noise-cancelling over-ear headphones.", 85), + ("Apple AirPods Pro 2", "electronics", "Apple", 250, true, 50, "Wireless earbuds with active noise cancellation.", 88), + ("Garmin Forerunner 265", "electronics", "Garmin", 450, true, 9, "GPS running watch with training metrics.", 78), + ("Nike Dri-FIT Running Tee", "apparel", "Nike", 35, true, 120, "Breathable, moisture-wicking running shirt.", 65), + ("Adidas Own the Run Jacket","apparel", "Adidas", 80, true, 33, "Lightweight, water-repellent running jacket.", 60), + ]; + + /// Seeds the sample product graph (idempotent — safe to run every start). + public Task SeedAsync(CancellationToken ct = default) => this._runner.WriteAsync(async r => + { + await r.RunAsync( + """ + UNWIND $products AS row + MERGE (p:Product {name: row.name}) + SET p.category = row.category, p.brand = row.brand, p.price = row.price, + p.in_stock = row.in_stock, p.inventory = row.inventory, + p.description = row.description, p.popularity = row.popularity + MERGE (c:ProductCategory {name: row.category}) + MERGE (b:ProductBrand {name: row.brand}) + MERGE (p)-[:IN_CATEGORY]->(c) + MERGE (p)-[:MADE_BY]->(b) + """, + new + { + products = s_seed.Select(p => (object)new Dictionary + { + ["name"] = p.Name, ["category"] = p.Category, ["brand"] = p.Brand, ["price"] = p.Price, + ["in_stock"] = p.InStock, ["inventory"] = p.Inventory, ["description"] = p.Description, + ["popularity"] = p.Popularity, + }).ToList(), + }); + }, ct); + + // ── Tools (also usable directly in the scripted demo) ──────────────────────────────────────── + + [Description("Search the product catalog for items matching a query, with optional category, brand, and max-price filters.")] + public Task SearchProductsAsync( + [Description("What the customer is looking for, e.g. 'running shoes'.")] string query, + [Description("Optional category filter: shoes, electronics, apparel.")] string? category = null, + [Description("Optional brand filter, e.g. 'Nike'.")] string? brand = null, + [Description("Optional maximum price.")] double? maxPrice = null, + CancellationToken ct = default) => this._runner.ReadAsync(async r => + { + const string Cypher = + """ + MATCH (p:Product) + WHERE ANY(w IN split(toLower($query), ' ') WHERE + toLower(p.name) CONTAINS w OR toLower(p.description) CONTAINS w OR toLower(p.category) CONTAINS w) + AND ($category IS NULL OR p.category = $category) + AND ($brand IS NULL OR p.brand = $brand) + AND ($maxPrice IS NULL OR p.price <= $maxPrice) + RETURN p.name AS name, p.brand AS brand, p.category AS category, + p.price AS price, p.in_stock AS inStock + ORDER BY p.popularity DESC + LIMIT 10 + """; + var cursor = await r.RunAsync(Cypher, new { query, category, brand, maxPrice }); + return Render("Matches", await cursor.ToListAsync()); + }, ct); + + [Description("Get personalized product recommendations, optionally biased toward a preferred brand and/or category.")] + public Task GetRecommendationsAsync( + [Description("The customer's preferred brand (from their saved preferences), if known.")] string? preferredBrand = null, + [Description("Optional category to recommend within.")] string? category = null, + [Description("How many recommendations to return.")] int limit = 5, + CancellationToken ct = default) => this._runner.ReadAsync(async r => + { + const string Cypher = + """ + MATCH (p:Product) + WHERE p.in_stock = true + AND ($category IS NULL OR p.category = $category) + WITH p, (CASE WHEN $preferredBrand IS NOT NULL AND p.brand = $preferredBrand THEN 1 ELSE 0 END) AS onBrand + RETURN p.name AS name, p.brand AS brand, p.category AS category, p.price AS price, p.in_stock AS inStock + ORDER BY onBrand DESC, p.popularity DESC + LIMIT $limit + """; + var cursor = await r.RunAsync(Cypher, new { preferredBrand, category, limit }); + var header = preferredBrand is null ? "Recommended for you" : $"Recommended for you (favoring {preferredBrand})"; + return Render(header, await cursor.ToListAsync()); + }, ct); + + [Description("Find products related to a given product — same category or same brand — via graph traversal.")] + public Task GetRelatedProductsAsync( + [Description("The exact product name to find related items for.")] string productName, + CancellationToken ct = default) => this._runner.ReadAsync(async r => + { + const string Cypher = + """ + MATCH (p:Product {name: $productName}) + CALL (p) { + MATCH (p)-[:IN_CATEGORY]->(c)<-[:IN_CATEGORY]-(rel:Product) WHERE rel <> p + RETURN rel, 'same category' AS reason + UNION + MATCH (p)-[:MADE_BY]->(b)<-[:MADE_BY]-(rel:Product) WHERE rel <> p + RETURN rel, 'same brand' AS reason + } + WITH rel, collect(DISTINCT reason) AS reasons + RETURN rel.name AS name, rel.brand AS brand, rel.category AS category, + rel.price AS price, rel.in_stock AS inStock, rel.popularity AS popularity, + reduce(s = '', x IN reasons | CASE WHEN s = '' THEN x ELSE s + ', ' + x END) AS reason + ORDER BY popularity DESC + LIMIT 5 + """; + var cursor = await r.RunAsync(Cypher, new { productName }); + return Render($"Related to {productName}", await cursor.ToListAsync()); + }, ct); + + [Description("Check whether a product is in stock and how many units are available.")] + public Task CheckInventoryAsync( + [Description("The exact product name to check.")] string productName, + CancellationToken ct = default) => this._runner.ReadAsync(async r => + { + var cursor = await r.RunAsync( + "MATCH (p:Product {name: $productName}) RETURN p.name AS name, p.in_stock AS inStock, p.inventory AS inventory", + new { productName }); + var rows = await cursor.ToListAsync(); + if (rows.Count == 0) + { + return $"'{productName}' was not found in the catalog."; + } + + var rec = rows[0]; + var inStock = rec["inStock"].As(); + return inStock + ? $"{rec["name"].As()}: In stock ({rec["inventory"].As()} available)." + : $"{rec["name"].As()}: Out of stock."; + }, ct); + + /// The retail tools as MAF/MEAI s (attach to the agent's ChatOptions.Tools). + public IReadOnlyList CreateAIFunctions() => + [ + AIFunctionFactory.Create(this.SearchProductsAsync, "search_products", + "Search the product catalog with optional category/brand/price filters."), + AIFunctionFactory.Create(this.GetRecommendationsAsync, "get_recommendations", + "Get personalized recommendations, optionally favoring a preferred brand/category."), + AIFunctionFactory.Create(this.GetRelatedProductsAsync, "get_related_products", + "Find products related to a given product via the graph."), + AIFunctionFactory.Create(this.CheckInventoryAsync, "check_inventory", + "Check stock/availability for a product."), + ]; + + private static string Render(string header, List rows) + { + if (rows.Count == 0) + { + return $"{header}: (no matches)"; + } + + var sb = new StringBuilder().Append(header).Append(':').AppendLine(); + foreach (var rec in rows) + { + var stock = rec["inStock"].As() ? "in stock" : "out of stock"; + var reason = rec.Keys.Contains("reason") ? $" [{rec["reason"].As()}]" : string.Empty; + sb.Append(" • ") + .Append(rec["name"].As()) + .Append(" — ").Append(rec["brand"].As()) + .Append(", ").Append(rec["category"].As()) + .Append(", $").Append(rec["price"].As().ToString("0")) + .Append(", ").Append(stock).Append(reason) + .AppendLine(); + } + return sb.ToString().TrimEnd(); + } +} diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs new file mode 100644 index 00000000000..a380c1e6783 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET) +// +// A .NET port of the Neo4j Labs "agent-memory" retail-assistant example +// (https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant, +// referenced from https://learn.microsoft.com/en-us/agent-framework/integrations/neo4j-memory). +// +// A shopping assistant that LEARNS a customer's preferences and RECOMMENDS products via graph +// traversal, backed by DURABLE memory in Neo4j. It uses the AgentMemory library — a .NET port of the +// Python memory provider, not an officially recognized Neo4j integration — and its Microsoft Agent +// Framework adapter: +// • Neo4jMemoryContextProvider (an AIContextProvider) — recalls memory before each run, persists +// after, and (via ExposeMemoryToolsFromContextProvider) surfaces the memory tools (search/remember/ +// recall) itself through AIContext.Tools +// • ProductCatalog.CreateAIFunctions() — retail tools over a Neo4j :Product graph +// +// Configuration (environment variables, matching the other Foundry samples): +// AZURE_OPENAI_ENDPOINT (required) — your Azure OpenAI / Foundry endpoint +// AZURE_OPENAI_API_KEY (optional) — API key; if unset, DefaultAzureCredential (az login) is used +// FOUNDRY_MODEL (default: gpt-4o-mini) — chat model deployment +// FOUNDRY_EMBEDDING_MODEL (default: text-embedding-3-small) — embedding model deployment (1536 dims) +// NEO4J_URI (default: bolt://localhost:7687) +// NEO4J_USER (default: neo4j) +// NEO4J_PASSWORD (default: password) + +using System.ClientModel; +using System.ClientModel.Primitives; +using AgentMemory.Abstractions.Services; +using AgentMemory.AgentFramework; +using AgentMemory.Core; +using AgentMemory.Core.Stubs; +using AgentMemory.Neo4j.Infrastructure; +using AgentMemoryShoppingAssistant; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenAI; + +// ── Model + credentials (Azure OpenAI / Foundry, via env vars) ─────────────────────────────────── +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +var apiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY"); +var chatModel = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o-mini"; +var embeddingModel = Environment.GetEnvironmentVariable("FOUNDRY_EMBEDDING_MODEL") ?? "text-embedding-3-small"; + +var clientOptions = new OpenAIClientOptions { Endpoint = new Uri(endpoint) }; +// API key if provided, otherwise Azure credential (dev: `az login`). +// 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. +OpenAIClient openAI = string.IsNullOrWhiteSpace(apiKey) + ? new OpenAIClient(new BearerTokenPolicy(new DefaultAzureCredential(), "https://ai.azure.com/.default"), clientOptions) + : new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions); + +IChatClient chatClient = openAI.GetChatClient(chatModel).AsIChatClient(); +IEmbeddingGenerator> embeddingGenerator = + openAI.GetEmbeddingClient(embeddingModel).AsIEmbeddingGenerator(); + +// ── AgentMemory (Neo4j) DI ─────────────────────────────────────────────────────────────────────── +var builder = Host.CreateApplicationBuilder(args); +builder.Logging.SetMinimumLevel(LogLevel.Warning); + +builder.Services.AddNeo4jAgentMemory(options => +{ + options.Uri = Environment.GetEnvironmentVariable("NEO4J_URI") ?? "bolt://localhost:7687"; + options.Username = Environment.GetEnvironmentVariable("NEO4J_USER") ?? "neo4j"; + options.Password = Environment.GetEnvironmentVariable("NEO4J_PASSWORD") ?? "password"; +}); +builder.Services.AddAgentMemoryCore(_ => { }); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.TryAddSingleton(chatClient); +builder.Services.TryAddSingleton(embeddingGenerator); +builder.Services.AddAgentMemoryFramework(options => +{ + options.AutoExtractOnPersist = true; + options.ContextFormat.IncludeEntities = true; + options.ContextFormat.IncludeFacts = true; + options.ContextFormat.IncludePreferences = true; + options.ExposeMemoryToolsFromContextProvider = true; +}); + +var host = builder.Build(); +await using var hostDisposal = (IAsyncDisposable)host; + +await using var scope = host.Services.CreateAsyncScope(); +var sp = scope.ServiceProvider; + +// ── Setup: schema + sample product graph ───────────────────────────────────────────────────────── +var catalog = new ProductCatalog(sp.GetRequiredService()); +await sp.GetRequiredService().BootstrapAsync(); +await catalog.SeedAsync(); +Console.WriteLine("Neo4j schema ready; sample products loaded.\n"); + +// ── The shopping assistant: context provider (recall + memory tools) + product tools ───────────── +var memoryProvider = sp.GetRequiredService(); +var productTools = catalog.CreateAIFunctions(); + +// WithMemoryOwnerScoping(sp) scopes the whole invocation (recall, tool calls, persistence) to the +// owner set via WithMemoryIdentity below — no manual BeginOwnerScope wrapping needed per turn. +AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions +{ + Name = "ShoppingAssistant", + ChatOptions = new ChatOptions + { + ModelId = chatModel, + Instructions = + "You are a helpful shopping assistant for an online store. Learn and remember the customer's " + + "preferences (brands, budget, categories) using the memory tools, and recommend products that " + + "fit using the product tools. Explain why each recommendation matches, and suggest alternatives " + + "when something is out of stock.", + // memoryProvider appends the six memory tools (search_memory, remember_fact, ...) to this list + // on every model call via AIContext.Tools — see ExposeMemoryToolsFromContextProvider above. + Tools = [.. productTools], + }, + AIContextProviders = [memoryProvider], +}).WithMemoryOwnerScoping(sp); + +const string Shopper = "shopper-amelia"; + +// ── Session A — the customer shops; the model calls the tools and remembers preferences ────────── +Console.WriteLine(">> Session A\n"); +var sessionA = (await agent.CreateSessionAsync()) + .WithMemoryIdentity(userId: Shopper, sessionId: "cart-a", applicationId: "retail-demo"); + +foreach (var turn in new[] +{ + "Hi! I'm looking for running shoes. I love Nike and want to stay under $150.", + "Nice — what would you recommend for me, and is anything I might like out of stock?", +}) +{ + await SayAsync(agent, sessionA, turn); +} + +// ── Session B — a NEW session for the same shopper still recalls her preferences ───────────────── +Console.WriteLine(">> Session B — a brand-new session; memory is durable\n"); +var sessionB = (await agent.CreateSessionAsync()) + .WithMemoryIdentity(userId: Shopper, sessionId: "cart-b", applicationId: "retail-demo"); + +await SayAsync(agent, sessionB, "I'm back — remind me what I like and suggest something new."); + +Console.WriteLine("=== Done. Preferences + messages persist in Neo4j across sessions. ==="); + +// One conversational turn. Owner scoping (recall, tool calls, and persistence) is guaranteed +// automatically by the WithMemoryOwnerScoping-wrapped agent — no manual BeginOwnerScope needed here. +static async Task SayAsync(AIAgent agent, AgentSession session, string message) +{ + Console.WriteLine($"USER : {message}"); + var response = await agent.RunAsync(message, session); + Console.WriteLine($"ASSISTANT : {response.Text}\n"); +} diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md new file mode 100644 index 00000000000..46800724722 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md @@ -0,0 +1,75 @@ +# Agent with Memory Using AgentMemory — Shopping Assistant + +A **.NET port of the Neo4j Labs "agent-memory" retail assistant** example +([`microsoft_agent_retail_assistant`](https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant), +referenced from the [Learn integration page](https://learn.microsoft.com/en-us/agent-framework/integrations/neo4j-memory)). +A shopping assistant that **learns a customer's preferences** and **recommends products via graph +traversal**, backed by durable memory in Neo4j. + +It uses the [`AgentMemory`](https://www.nuget.org/packages/AgentMemory) library — a .NET port of the +(Python-only) Neo4j Labs memory provider, **not an officially recognized Neo4j integration** — through +its Microsoft Agent Framework adapter. + +## Features Demonstrated + +- **`Neo4jMemoryContextProvider`** (an `AIContextProvider`) — recalls relevant memory before each run, + persists new memory after (the same bidirectional pattern as the official provider), and — via + `ExposeMemoryToolsFromContextProvider = true` — surfaces the memory tools (search / remember / recall) + itself through `AIContext.Tools`. +- **`ProductCatalog.CreateAIFunctions()`** — retail tools over a Neo4j `:Product` graph (search / + recommend / related / inventory). +- Preference learning that persists across a brand-new `AgentSession` for the same shopper. +- Graph-based product recommendations and "related products" via traversal. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- A **Neo4j 5.x** instance (the sample bootstraps the schema and seeds sample products) +- An **Azure OpenAI / Foundry** deployment (a chat model + an embedding model) + + +## Configuration + +Set the following environment variables: + +| Variable | Required | Default | Purpose | +|---|---|---|---| +| `AZURE_OPENAI_ENDPOINT` | ✅ | — | Azure OpenAI / Foundry endpoint | +| `AZURE_OPENAI_API_KEY` | — | — | API key; if unset, `DefaultAzureCredential` (`az login`) is used | +| `FOUNDRY_MODEL` | — | `gpt-4o-mini` | chat model deployment | +| `FOUNDRY_EMBEDDING_MODEL` | — | `text-embedding-3-small` | embedding model deployment (1536 dims) | +| `NEO4J_URI` | — | `bolt://localhost:7687` | Neo4j bolt URI | +| `NEO4J_USER` | — | `neo4j` | Neo4j user | +| `NEO4J_PASSWORD` | — | `password` | Neo4j password | + +> Ensure the embedding model's dimensions match the Neo4j vector-index dimensions AgentMemory bootstraps +> (default 1536, which matches `text-embedding-3-small`). + +## Run the Sample + +```bash +docker run -d --name neo4j -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password neo4j:5.26 + +export AZURE_OPENAI_ENDPOINT="https://.openai.azure.com" +export AZURE_OPENAI_API_KEY="" # or omit and `az login` +export FOUNDRY_MODEL="gpt-4o-mini" + +dotnet run +``` + +## Expected Output + +1. The sample bootstraps the Neo4j schema and seeds a small product graph (`:Product`, + `:ProductCategory`, `:ProductBrand` nodes). +2. **Session A** — the shopper says she wants running shoes, loves Nike, and has a $150 budget; the + agent calls the memory tools to remember this and the product tools to recommend matching items. +3. **Session B** — a brand-new session for the same shopper (`shopper-amelia`) still recalls her + preferences and can suggest something new, because memory persists in Neo4j across sessions. + +## Note on packaging + +This sample is part of the repo's solution and targets .NET 10 like every other sample, but it +deliberately opts out of **Central Package Management** and does **not** reference `Microsoft.Agents.AI` +via the repo's in-source project — it consumes the **published** `AgentMemory` NuGet packages instead +(which target `Microsoft.Agents.AI` 1.9.0). A version that references the repo's current +`Microsoft.Agents.AI` source would require AgentMemory to be rebuilt against that version first. diff --git a/dotnet/samples/02-agents/AgentWithMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/README.md index 3b9ee76928f..752635bbb24 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/README.md @@ -9,6 +9,7 @@ These samples show how to create an agent with the Agent Framework that uses Mem |[Custom Memory Implementation](../../01-get-started/04_memory/)|This sample demonstrates how to create a custom memory component and attach it to an agent.| |[Memory with Microsoft Foundry](./AgentWithMemory_Step04_MemoryUsingFoundry/)|This sample demonstrates how to create and run an agent that uses Microsoft Foundry's managed memory service to extract and retrieve individual memories.| |[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.| > **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents. diff --git a/dotnet/samples/AGENTS.md b/dotnet/samples/AGENTS.md index 348e4c53385..e5432596eb1 100644 --- a/dotnet/samples/AGENTS.md +++ b/dotnet/samples/AGENTS.md @@ -30,7 +30,7 @@ dotnet/samples/ │ │ └── openai/ # OpenAI provider samples │ ├── AgentOpenTelemetry/ # OpenTelemetry integration │ ├── AgentSkills/ # Agent skills patterns -│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Foundry) +│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Valkey, Foundry, AgentMemory) │ ├── AgentWithRAG/ # RAG patterns (text, vector store, Foundry) │ ├── AGUI/ # AG-UI protocol samples │ ├── DeclarativeAgents/ # Declarative agent definitions