From c84173a95be18196cd214f9a4ca0cb2e2848c0f4 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Mon, 13 Jul 2026 22:48:57 +0200 Subject: [PATCH 01/14] samples: add Neo4j Shopping Assistant (standalone, published AgentMemory 1.0.1) The .NET port of the official Neo4j Agent Memory "retail assistant" example (neo4j-labs/agent-memory examples/microsoft_agent_retail_assistant, referenced from the Learn integration page), which is currently Python-only. Wires Neo4jMemoryContextProvider (AIContextProvider), MemoryToolFactory memory tools, and a ProductCatalog of retail tools over a Neo4j :Product graph, via the published AgentMemory + AgentMemory.AgentFramework 1.0.1 NuGet packages. Lives at the repo root rather than under dotnet/samples/: that tree is .NET 10 + Central Package Management + Microsoft.Agents.AI ~1.13 with source ProjectReferences, while AgentMemory currently targets net9.0 + Microsoft.Agents.AI 1.9.0. A repo-native version needs AgentMemory bumped to track the newer Agents.AI/Extensions.AI line first. Cross-linked from dotnet/samples/02-agents/AgentWithMemory/README.md as a "See also" entry, same pattern already used for the cross-folder Custom Memory Implementation link. Verified: dotnet build succeeds (0 warnings, 0 errors) against the published packages, proving the AgentMemory public surface is package-consumable. Matches sibling AgentWithMemory samples' conventions (BOM + copyright file header on .cs files, README sections: Features Demonstrated / Prerequisites / Environment Variables / Run the Sample / Expected Output). Co-Authored-By: Claude Sonnet 5 --- .../02-agents/AgentWithMemory/README.md | 2 + neo4j-shopping-assistant-sample/.gitignore | 2 + .../Neo4jShoppingAssistant.csproj | 32 +++ .../ProductCatalog.cs | 186 ++++++++++++++++++ neo4j-shopping-assistant-sample/Program.cs | 155 +++++++++++++++ neo4j-shopping-assistant-sample/README.md | 72 +++++++ 6 files changed, 449 insertions(+) create mode 100644 neo4j-shopping-assistant-sample/.gitignore create mode 100644 neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj create mode 100644 neo4j-shopping-assistant-sample/ProductCatalog.cs create mode 100644 neo4j-shopping-assistant-sample/Program.cs create mode 100644 neo4j-shopping-assistant-sample/README.md diff --git a/dotnet/samples/02-agents/AgentWithMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/README.md index 3b9ee76928f..7257cbe378e 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/README.md @@ -11,4 +11,6 @@ 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.| > **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents. +> +> **See also**: [Neo4j Shopping Assistant](../../../../neo4j-shopping-assistant-sample/) - a retail assistant that uses [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), the .NET Neo4j memory provider, via its `Neo4jMemoryContextProvider` (`AIContextProvider`). Ships as a **standalone** sample consuming the published NuGet packages (targets `Microsoft.Agents.AI` 1.9.0), so it lives outside this build/CPM tree rather than as a project reference here. diff --git a/neo4j-shopping-assistant-sample/.gitignore b/neo4j-shopping-assistant-sample/.gitignore new file mode 100644 index 00000000000..cd42ee34e87 --- /dev/null +++ b/neo4j-shopping-assistant-sample/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj b/neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj new file mode 100644 index 00000000000..736c752c281 --- /dev/null +++ b/neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj @@ -0,0 +1,32 @@ + + + + + Exe + net9.0 + enable + enable + false + Neo4jShoppingAssistant + + $(NoWarn);OPENAI001 + + + + + + + + + + + + + + diff --git a/neo4j-shopping-assistant-sample/ProductCatalog.cs b/neo4j-shopping-assistant-sample/ProductCatalog.cs new file mode 100644 index 00000000000..7d9136d8c56 --- /dev/null +++ b/neo4j-shopping-assistant-sample/ProductCatalog.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using System.Text; +using Microsoft.Extensions.AI; +using AgentMemory.Neo4j.Infrastructure; +using Neo4j.Driver; + +namespace Neo4jShoppingAssistant; + +/// +/// 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 MemoryToolFactory.CreateAIFunctions() exposes the memory tools. +/// +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)[] 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) => _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 = 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) => _runner.ReadAsync(async r => + { + var 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) => _runner.ReadAsync(async r => + { + var 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) => _runner.ReadAsync(async r => + { + var 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) => _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(SearchProductsAsync, "search_products", + "Search the product catalog with optional category/brand/price filters."), + AIFunctionFactory.Create(GetRecommendationsAsync, "get_recommendations", + "Get personalized recommendations, optionally favoring a preferred brand/category."), + AIFunctionFactory.Create(GetRelatedProductsAsync, "get_related_products", + "Find products related to a given product via the graph."), + AIFunctionFactory.Create(CheckInventoryAsync, "check_inventory", + "Check stock/availability for a product."), + ]; + + private static string Render(string header, IReadOnlyList 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/neo4j-shopping-assistant-sample/Program.cs b/neo4j-shopping-assistant-sample/Program.cs new file mode 100644 index 00000000000..862e4c68c37 --- /dev/null +++ b/neo4j-shopping-assistant-sample/Program.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Neo4j Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET) +// +// The .NET port of the official Neo4j 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 Neo4j memory. It uses the AgentMemory library's Microsoft Agent +// Framework adapter: +// • Neo4jMemoryContextProvider (an AIContextProvider) — recalls memory before each run, persists after +// • MemoryToolFactory.CreateAIFunctions() — memory tools (search/remember/recall) +// • ProductCatalog.CreateAIFunctions() — retail tools over a Neo4j :Product graph +// +// This is a STANDALONE sample consuming the published AgentMemory NuGet packages. +// +// 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 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; +using AgentMemory.Abstractions.Services; +using AgentMemory.AgentFramework; +using AgentMemory.AgentFramework.Tools; +using AgentMemory.Core; +using AgentMemory.Core.Stubs; +using AgentMemory.Neo4j.Infrastructure; +using Neo4jShoppingAssistant; + +// ── 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`). +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; +}); + +using 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 + memory tools + product tools ────────────────────── +var memoryProvider = sp.GetRequiredService(); +var memoryTools = sp.GetRequiredService().CreateAIFunctions(); +var productTools = catalog.CreateAIFunctions(); + +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.", + Tools = [.. memoryTools, .. productTools], + }, + AIContextProviders = [memoryProvider], +}); + +var ownerContext = sp.GetRequiredService(); +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, ownerContext, shopper, 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, ownerContext, shopper, "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. The ambient owner scope keeps any model-invoked memory tools scoped to +// this shopper. The context provider recalls memory before the run and persists after — automatically. +static async Task SayAsync(AIAgent agent, AgentSession session, IWritableMemoryOwnerContext ownerContext, + string userId, string message) +{ + Console.WriteLine($"USER : {message}"); + using (ownerContext.BeginOwnerScope(userId)) + { + var response = await agent.RunAsync(message, session); + Console.WriteLine($"ASSISTANT : {response.Text}\n"); + } +} diff --git a/neo4j-shopping-assistant-sample/README.md b/neo4j-shopping-assistant-sample/README.md new file mode 100644 index 00000000000..a3b577ff220 --- /dev/null +++ b/neo4j-shopping-assistant-sample/README.md @@ -0,0 +1,72 @@ +# Neo4j Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET) + +The **.NET port of the official Neo4j 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 Neo4j memory. + +It uses the [`AgentMemory`](https://www.nuget.org/packages/AgentMemory) library — the .NET equivalent +of the (Python-only) official Neo4j memory provider — 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). +- **`MemoryToolFactory.CreateAIFunctions()`** — memory tools the model can call (search / remember / recall). +- **`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 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.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) + +## Environment Variables + +Matches the other Agent Framework Foundry samples — no secrets in code: + +| 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 is a **standalone** sample: it consumes the **published** `AgentMemory` NuGet packages (which +target `Microsoft.Agents.AI` 1.9.0), so it deliberately does **not** integrate into the agent-framework +repo's .NET 10 / Central-Package-Management / source-reference build. A version that references the +repo's current `Microsoft.Agents.AI` source would require AgentMemory to be rebuilt against that +version first. From b0b277c8528ada539691f3ca9d48f3e155f7e464 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Mon, 13 Jul 2026 23:43:29 +0200 Subject: [PATCH 02/14] samples: move Neo4j shopping assistant into AgentWithMemory as Step06 Relocates the standalone shopping-assistant sample from the repo root into dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory, following that folder's naming/README/solution conventions. Renames its identity from "Neo4j" to "AgentMemory" (the library it actually demonstrates) since this is a community .NET port, not an officially recognized Neo4j integration - Neo4j is still referenced where it's a genuine technical detail (the graph backing store, env vars, Cypher). Adds empty Directory.Build.props/targets markers so it stays isolated from the repo's net10.0/CPM build, and registers it (skipped, like the Mem0 sample) in the CI sample-verification list since it needs a live Neo4j instance. Co-Authored-By: Claude Sonnet 5 --- dotnet/agent-framework-dotnet.slnx | 1 + dotnet/eng/verify-samples/AgentsSamples.cs | 9 +++++++++ ...ntWithMemory_Step06_MemoryUsingAgentMemory.csproj | 12 ++++++++---- .../Directory.Build.props | 1 + .../Directory.Build.targets | 1 + .../ProductCatalog.cs | 2 +- .../Program.cs | 9 +++++---- .../README.md | 11 ++++++----- dotnet/samples/02-agents/AgentWithMemory/README.md | 3 +-- dotnet/samples/AGENTS.md | 2 +- neo4j-shopping-assistant-sample/.gitignore | 2 -- 11 files changed, 34 insertions(+), 19 deletions(-) rename neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj => dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj (69%) create mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props create mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets rename {neo4j-shopping-assistant-sample => dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory}/ProductCatalog.cs (99%) rename {neo4j-shopping-assistant-sample => dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory}/Program.cs (95%) rename {neo4j-shopping-assistant-sample => dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory}/README.md (91%) delete mode 100644 neo4j-shopping-assistant-sample/.gitignore 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/neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj similarity index 69% rename from neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj rename to dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj index 736c752c281..700f238a58d 100644 --- a/neo4j-shopping-assistant-sample/Neo4jShoppingAssistant.csproj +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj @@ -3,8 +3,11 @@ Exe @@ -12,14 +15,15 @@ enable enable false - Neo4jShoppingAssistant + AgentMemoryShoppingAssistant $(NoWarn);OPENAI001 - + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props new file mode 100644 index 00000000000..058246e4086 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props @@ -0,0 +1 @@ + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets new file mode 100644 index 00000000000..058246e4086 --- /dev/null +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets @@ -0,0 +1 @@ + diff --git a/neo4j-shopping-assistant-sample/ProductCatalog.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs similarity index 99% rename from neo4j-shopping-assistant-sample/ProductCatalog.cs rename to dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs index 7d9136d8c56..575a267d487 100644 --- a/neo4j-shopping-assistant-sample/ProductCatalog.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs @@ -6,7 +6,7 @@ using AgentMemory.Neo4j.Infrastructure; using Neo4j.Driver; -namespace Neo4jShoppingAssistant; +namespace AgentMemoryShoppingAssistant; /// /// A small retail product graph plus the shopping tools that query it — the .NET counterpart of the diff --git a/neo4j-shopping-assistant-sample/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs similarity index 95% rename from neo4j-shopping-assistant-sample/Program.cs rename to dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs index 862e4c68c37..da2032aeb96 100644 --- a/neo4j-shopping-assistant-sample/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -1,13 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -// Neo4j Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET) +// Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET) // -// The .NET port of the official Neo4j Agent Memory "retail assistant" example +// 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 Neo4j memory. It uses the AgentMemory library's Microsoft Agent +// 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 // • MemoryToolFactory.CreateAIFunctions() — memory tools (search/remember/recall) @@ -40,7 +41,7 @@ using AgentMemory.Core; using AgentMemory.Core.Stubs; using AgentMemory.Neo4j.Infrastructure; -using Neo4jShoppingAssistant; +using AgentMemoryShoppingAssistant; // ── Model + credentials (Azure OpenAI / Foundry, via env vars) ─────────────────────────────────── var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") diff --git a/neo4j-shopping-assistant-sample/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md similarity index 91% rename from neo4j-shopping-assistant-sample/README.md rename to dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md index a3b577ff220..9ce7d6e477a 100644 --- a/neo4j-shopping-assistant-sample/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md @@ -1,13 +1,14 @@ -# Neo4j Agent Memory — Shopping Assistant (Microsoft Agent Framework, .NET) +# Agent with Memory Using AgentMemory — Shopping Assistant -The **.NET port of the official Neo4j Agent Memory "retail assistant"** example +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 Neo4j memory. +traversal**, backed by durable memory in Neo4j. -It uses the [`AgentMemory`](https://www.nuget.org/packages/AgentMemory) library — the .NET equivalent -of the (Python-only) official Neo4j memory provider — through its Microsoft Agent Framework adapter. +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 diff --git a/dotnet/samples/02-agents/AgentWithMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/README.md index 7257cbe378e..9f29c9dd043 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/README.md @@ -9,8 +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), a .NET port of the Neo4j Labs graph-memory provider (not an officially recognized Neo4j integration), via its `Neo4jMemoryContextProvider` (`AIContextProvider`), to learn customer preferences and recommend products via graph traversal. Ships as a **standalone** sample consuming the published NuGet packages (targets `Microsoft.Agents.AI` 1.9.0), so it does not participate in this repo's Central-Package-Management / source-reference build.| > **See also**: [Memory Search with Foundry Agents](../AgentProviders/foundry/Agent_Step22_MemorySearch/) - demonstrates using the built-in Memory Search tool with Microsoft Foundry agents. -> -> **See also**: [Neo4j Shopping Assistant](../../../../neo4j-shopping-assistant-sample/) - a retail assistant that uses [`AgentMemory`](https://www.nuget.org/packages/AgentMemory), the .NET Neo4j memory provider, via its `Neo4jMemoryContextProvider` (`AIContextProvider`). Ships as a **standalone** sample consuming the published NuGet packages (targets `Microsoft.Agents.AI` 1.9.0), so it lives outside this build/CPM tree rather than as a project reference here. 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 diff --git a/neo4j-shopping-assistant-sample/.gitignore b/neo4j-shopping-assistant-sample/.gitignore deleted file mode 100644 index cd42ee34e87..00000000000 --- a/neo4j-shopping-assistant-sample/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -bin/ -obj/ From f2d7c9c3548762164e806dcd5089fe643d41f758 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 18:39:20 +0200 Subject: [PATCH 03/14] Update dotnet/samples/02-agents/AgentWithMemory/README.md Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- dotnet/samples/02-agents/AgentWithMemory/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/02-agents/AgentWithMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/README.md index 9f29c9dd043..752635bbb24 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/README.md @@ -9,7 +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), a .NET port of the Neo4j Labs graph-memory provider (not an officially recognized Neo4j integration), via its `Neo4jMemoryContextProvider` (`AIContextProvider`), to learn customer preferences and recommend products via graph traversal. Ships as a **standalone** sample consuming the published NuGet packages (targets `Microsoft.Agents.AI` 1.9.0), so it does not participate in this repo's Central-Package-Management / source-reference build.| +|[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. From 71843da4b79c6e2c4aa04a743271aa30000ba5e7 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 18:39:29 +0200 Subject: [PATCH 04/14] Update dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj Co-authored-by: westey <164392973+westey-m@users.noreply.github.com> --- .../AgentWithMemory_Step06_MemoryUsingAgentMemory.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 700f238a58d..ee746b9b41b 100644 --- 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 @@ -22,8 +22,8 @@ - + From 30790670ee50409fe498436ec1a381b21768b39b Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 18:42:03 +0200 Subject: [PATCH 05/14] cleanup :) --- .../Directory.Build.props | 1 - .../Directory.Build.targets | 1 - 2 files changed, 2 deletions(-) delete mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props delete mode 100644 dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props deleted file mode 100644 index 058246e4086..00000000000 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.props +++ /dev/null @@ -1 +0,0 @@ - diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets deleted file mode 100644 index 058246e4086..00000000000 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Directory.Build.targets +++ /dev/null @@ -1 +0,0 @@ - From d607db4c2383563de9f9b2c7fd787ae8cc1d01b2 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 18:54:04 +0200 Subject: [PATCH 06/14] DefaultAzureCredential warning --- .../AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs index da2032aeb96..3131b95e100 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -52,6 +52,9 @@ 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); From 87ebd18cc8b070380d7547a9b5a68a0a7271dea1 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 19:26:41 +0200 Subject: [PATCH 07/14] fixes - simplification userId --- .../Program.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs index 3131b95e100..23f1cc502bb 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -86,7 +86,7 @@ options.ContextFormat.IncludePreferences = true; }); -using var host = builder.Build(); +var host = builder.Build(); await using var hostDisposal = (IAsyncDisposable)host; await using var scope = host.Services.CreateAsyncScope(); @@ -133,7 +133,7 @@ "Nice — what would you recommend for me, and is anything I might like out of stock?", }) { - await SayAsync(agent, sessionA, ownerContext, shopper, turn); + await SayAsync(agent, sessionA, ownerContext, turn); } // ── Session B — a NEW session for the same shopper still recalls her preferences ───────────────── @@ -141,15 +141,18 @@ var sessionB = (await agent.CreateSessionAsync()) .WithMemoryIdentity(userId: shopper, sessionId: "cart-b", applicationId: "retail-demo"); -await SayAsync(agent, sessionB, ownerContext, shopper, "I'm back — remind me what I like and suggest something new."); +await SayAsync(agent, sessionB, ownerContext, "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. The ambient owner scope keeps any model-invoked memory tools scoped to // this shopper. The context provider recalls memory before the run and persists after — automatically. -static async Task SayAsync(AIAgent agent, AgentSession session, IWritableMemoryOwnerContext ownerContext, - string userId, string message) +static async Task SayAsync(AIAgent agent, AgentSession session, IWritableMemoryOwnerContext ownerContext, string message) { + // The user id set via WithMemoryIdentity lives in the session's state bag; read it back rather + // than threading it through as a separate parameter (AgentFrameworkOptions.DefaultUserIdKey = "user_id"). + session.StateBag.TryGetValue(new AgentFrameworkOptions().DefaultUserIdKey, out var userId); + Console.WriteLine($"USER : {message}"); using (ownerContext.BeginOwnerScope(userId)) { From 401ddf1471f4468f365ea95008c7fcfd42a462a1 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 19:41:50 +0200 Subject: [PATCH 08/14] minor doc fix --- .../AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md index 9ce7d6e477a..4267f13e2d6 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/README.md @@ -26,9 +26,10 @@ its Microsoft Agent Framework adapter. - 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) -## Environment Variables -Matches the other Agent Framework Foundry samples — no secrets in code: +## Configuration + +Set the following environment variables: | Variable | Required | Default | Purpose | |---|---|---|---| From 6930a0b5d7d414996de58eccf0fd37d0ce66bbdf Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Tue, 14 Jul 2026 19:51:57 +0200 Subject: [PATCH 09/14] NU1015 fix --- ...emory_Step06_MemoryUsingAgentMemory.csproj | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) 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 index ee746b9b41b..89798e5b57b 100644 --- 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 @@ -3,11 +3,15 @@ Exe @@ -21,6 +25,16 @@ $(NoWarn);OPENAI001 + + + + + + + + + + @@ -33,4 +47,27 @@ + + + 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 + + + From 9fb49b99a2122e5b9d11b49ff95486a26673bae4 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Wed, 15 Jul 2026 20:37:25 +0200 Subject: [PATCH 10/14] PR review fixes-improvements --- ...emory_Step06_MemoryUsingAgentMemory.csproj | 6 ++--- .../Program.cs | 26 +++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) 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 index 89798e5b57b..4de17d79678 100644 --- 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 @@ -15,7 +15,7 @@ --> Exe - net9.0 + net10.0 enable enable false @@ -38,8 +38,8 @@ - - + + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs index 23f1cc502bb..bc70c9626e6 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -103,6 +103,8 @@ var memoryTools = sp.GetRequiredService().CreateAIFunctions(); 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", @@ -117,9 +119,8 @@ Tools = [.. memoryTools, .. productTools], }, AIContextProviders = [memoryProvider], -}); +}).WithMemoryOwnerScoping(sp); -var ownerContext = sp.GetRequiredService(); const string shopper = "shopper-amelia"; // ── Session A — the customer shops; the model calls the tools and remembers preferences ────────── @@ -133,7 +134,7 @@ "Nice — what would you recommend for me, and is anything I might like out of stock?", }) { - await SayAsync(agent, sessionA, ownerContext, turn); + await SayAsync(agent, sessionA, turn); } // ── Session B — a NEW session for the same shopper still recalls her preferences ───────────────── @@ -141,22 +142,15 @@ var sessionB = (await agent.CreateSessionAsync()) .WithMemoryIdentity(userId: shopper, sessionId: "cart-b", applicationId: "retail-demo"); -await SayAsync(agent, sessionB, ownerContext, "I'm back — remind me what I like and suggest something new."); +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. The ambient owner scope keeps any model-invoked memory tools scoped to -// this shopper. The context provider recalls memory before the run and persists after — automatically. -static async Task SayAsync(AIAgent agent, AgentSession session, IWritableMemoryOwnerContext ownerContext, string message) +// 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) { - // The user id set via WithMemoryIdentity lives in the session's state bag; read it back rather - // than threading it through as a separate parameter (AgentFrameworkOptions.DefaultUserIdKey = "user_id"). - session.StateBag.TryGetValue(new AgentFrameworkOptions().DefaultUserIdKey, out var userId); - Console.WriteLine($"USER : {message}"); - using (ownerContext.BeginOwnerScope(userId)) - { - var response = await agent.RunAsync(message, session); - Console.WriteLine($"ASSISTANT : {response.Text}\n"); - } + var response = await agent.RunAsync(message, session); + Console.WriteLine($"ASSISTANT : {response.Text}\n"); } From 3773db8562f33ef41f801c6a0f3ed6fad104776c Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Wed, 15 Jul 2026 21:50:33 +0200 Subject: [PATCH 11/14] Bump AgentMemory to 1.2.0, let the context provider surface memory tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WithMemoryOwnerScoping(sp) (1.1.0) already removed the need to manually wrap agent.RunAsync in ownerContext.BeginOwnerScope(userId). This picks up 1.2.0's ExposeMemoryToolsFromContextProvider option, so Neo4jMemoryContextProvider now appends the memory tools to AIContext.Tools itself on every model call — no more separate MemoryToolFactory wiring, AIContextProviders = [memoryProvider] is enough. Addresses westey-m's PR review suggestion. Co-Authored-By: Claude Sonnet 5 --- ...WithMemory_Step06_MemoryUsingAgentMemory.csproj | 4 ++-- .../Program.cs | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) 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 index 4de17d79678..58f018c6c57 100644 --- 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 @@ -38,8 +38,8 @@ - - + + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs index bc70c9626e6..6ac19fcf609 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -10,8 +10,9 @@ // 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 -// • MemoryToolFactory.CreateAIFunctions() — memory tools (search/remember/recall) +// • 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 // // This is a STANDALONE sample consuming the published AgentMemory NuGet packages. @@ -37,7 +38,6 @@ using OpenAI; using AgentMemory.Abstractions.Services; using AgentMemory.AgentFramework; -using AgentMemory.AgentFramework.Tools; using AgentMemory.Core; using AgentMemory.Core.Stubs; using AgentMemory.Neo4j.Infrastructure; @@ -84,6 +84,7 @@ options.ContextFormat.IncludeEntities = true; options.ContextFormat.IncludeFacts = true; options.ContextFormat.IncludePreferences = true; + options.ExposeMemoryToolsFromContextProvider = true; }); var host = builder.Build(); @@ -98,9 +99,8 @@ await catalog.SeedAsync(); Console.WriteLine("Neo4j schema ready; sample products loaded.\n"); -// ── The shopping assistant: context provider + memory tools + product tools ────────────────────── +// ── The shopping assistant: context provider (recall + memory tools) + product tools ───────────── var memoryProvider = sp.GetRequiredService(); -var memoryTools = sp.GetRequiredService().CreateAIFunctions(); var productTools = catalog.CreateAIFunctions(); // WithMemoryOwnerScoping(sp) scopes the whole invocation (recall, tool calls, persistence) to the @@ -116,7 +116,9 @@ + "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.", - Tools = [.. memoryTools, .. productTools], + // 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); From d5d02776d2ccf33631b962026ea676298224f049 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Thu, 16 Jul 2026 09:32:55 +0200 Subject: [PATCH 12/14] improvements according to pr review comments --- ...hMemory_Step06_MemoryUsingAgentMemory.csproj | 8 ++++---- .../ProductCatalog.cs | 3 ++- .../Program.cs | 2 -- .../README.md | 17 +++++++++-------- 4 files changed, 15 insertions(+), 15 deletions(-) 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 index 58f018c6c57..c094d4dba41 100644 --- 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 @@ -1,10 +1,10 @@ Exe - net10.0 + net10.0 enable enable false From 611564254acd427abfb813c9c9e11c6204615f24 Mon Sep 17 00:00:00 2001 From: Jose Luis Latorre Millas Date: Thu, 16 Jul 2026 17:00:33 +0200 Subject: [PATCH 14/14] Fix CI: pin OpenTelemetry.Api to unblock NU1902 audit failure The sample opts out of central package management, so it was pulling in OpenTelemetry.Api 1.12.0 transitively (via Microsoft.Agents.AI), which has a known moderate-severity vulnerability (GHSA-g94r-2vxg-569j). The repo treats NuGet audit warnings as errors, so restore failed outright and took down every dotnet-build matrix leg plus check-format. Pinned OpenTelemetry.Api to 1.15.3, matching Directory.Packages.props. With restore succeeding, previously-masked analyzer/format issues surfaced and are fixed too: RCS1118 (const local for immutable Cypher queries), CA1859 (List param instead of IReadOnlyList), and IDE1006 naming violations (s_seed field prefix, PascalCase Cypher/Shopper consts). Verified locally with the same mcr.microsoft.com/dotnet/sdk:10.0 image CI uses: dotnet build --warnaserror and dotnet format --verify-no-changes both pass clean, and a full solution build completed ~24 min with zero errors. Co-Authored-By: Claude Sonnet 5 --- ...emory_Step06_MemoryUsingAgentMemory.csproj | 5 ++ .../ProductCatalog.cs | 50 +++++++++++-------- .../Program.cs | 30 +++++------ 3 files changed, 49 insertions(+), 36 deletions(-) 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 index c518da3339f..c4e28440519 100644 --- 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 @@ -45,6 +45,11 @@ + + diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs index 3c067c37539..2b28ec7ade6 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/ProductCatalog.cs @@ -2,8 +2,8 @@ using System.ComponentModel; using System.Text; -using Microsoft.Extensions.AI; using AgentMemory.Neo4j.Infrastructure; +using Microsoft.Extensions.AI; using Neo4j.Driver; namespace AgentMemoryShoppingAssistant; @@ -21,7 +21,7 @@ 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)[] Seed = + 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), @@ -36,7 +36,7 @@ private static readonly (string Name, string Category, string Brand, double Pric ]; /// Seeds the sample product graph (idempotent — safe to run every start). - public Task SeedAsync(CancellationToken ct = default) => _runner.WriteAsync(async r => + public Task SeedAsync(CancellationToken ct = default) => this._runner.WriteAsync(async r => { await r.RunAsync( """ @@ -52,7 +52,7 @@ await r.RunAsync( """, new { - products = Seed.Select(p => (object)new Dictionary + 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, @@ -69,9 +69,9 @@ public Task SearchProductsAsync( [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) => _runner.ReadAsync(async r => + CancellationToken ct = default) => this._runner.ReadAsync(async r => { - var cypher = + const string Cypher = """ MATCH (p:Product) WHERE ANY(w IN split(toLower($query), ' ') WHERE @@ -84,7 +84,7 @@ WHERE ANY(w IN split(toLower($query), ' ') WHERE ORDER BY p.popularity DESC LIMIT 10 """; - var cursor = await r.RunAsync(cypher, new { query, category, brand, maxPrice }); + var cursor = await r.RunAsync(Cypher, new { query, category, brand, maxPrice }); return Render("Matches", await cursor.ToListAsync()); }, ct); @@ -93,9 +93,9 @@ 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) => _runner.ReadAsync(async r => + CancellationToken ct = default) => this._runner.ReadAsync(async r => { - var cypher = + const string Cypher = """ MATCH (p:Product) WHERE p.in_stock = true @@ -105,7 +105,7 @@ public Task GetRecommendationsAsync( ORDER BY onBrand DESC, p.popularity DESC LIMIT $limit """; - var cursor = await r.RunAsync(cypher, new { preferredBrand, category, 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); @@ -113,9 +113,9 @@ public Task GetRecommendationsAsync( [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) => _runner.ReadAsync(async r => + CancellationToken ct = default) => this._runner.ReadAsync(async r => { - var cypher = + const string Cypher = """ MATCH (p:Product {name: $productName}) CALL (p) { @@ -132,20 +132,24 @@ public Task GetRelatedProductsAsync( ORDER BY popularity DESC LIMIT 5 """; - var cursor = await r.RunAsync(cypher, new { productName }); + 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) => _runner.ReadAsync(async r => + 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."; + if (rows.Count == 0) + { + return $"'{productName}' was not found in the catalog."; + } + var rec = rows[0]; var inStock = rec["inStock"].As(); return inStock @@ -156,19 +160,23 @@ public Task CheckInventoryAsync( /// The retail tools as MAF/MEAI s (attach to the agent's ChatOptions.Tools). public IReadOnlyList CreateAIFunctions() => [ - AIFunctionFactory.Create(SearchProductsAsync, "search_products", + AIFunctionFactory.Create(this.SearchProductsAsync, "search_products", "Search the product catalog with optional category/brand/price filters."), - AIFunctionFactory.Create(GetRecommendationsAsync, "get_recommendations", + AIFunctionFactory.Create(this.GetRecommendationsAsync, "get_recommendations", "Get personalized recommendations, optionally favoring a preferred brand/category."), - AIFunctionFactory.Create(GetRelatedProductsAsync, "get_related_products", + AIFunctionFactory.Create(this.GetRelatedProductsAsync, "get_related_products", "Find products related to a given product via the graph."), - AIFunctionFactory.Create(CheckInventoryAsync, "check_inventory", + AIFunctionFactory.Create(this.CheckInventoryAsync, "check_inventory", "Check stock/availability for a product."), ]; - private static string Render(string header, IReadOnlyList rows) + private static string Render(string header, List rows) { - if (rows.Count == 0) return $"{header}: (no matches)"; + if (rows.Count == 0) + { + return $"{header}: (no matches)"; + } + var sb = new StringBuilder().Append(header).Append(':').AppendLine(); foreach (var rec in rows) { diff --git a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs index 988681231f2..a380c1e6783 100644 --- a/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs +++ b/dotnet/samples/02-agents/AgentWithMemory/AgentWithMemory_Step06_MemoryUsingAgentMemory/Program.cs @@ -26,6 +26,12 @@ 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; @@ -34,12 +40,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenAI; -using AgentMemory.Abstractions.Services; -using AgentMemory.AgentFramework; -using AgentMemory.Core; -using AgentMemory.Core.Stubs; -using AgentMemory.Neo4j.Infrastructure; -using AgentMemoryShoppingAssistant; // ── Model + credentials (Azure OpenAI / Foundry, via env vars) ─────────────────────────────────── var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") @@ -67,8 +67,8 @@ builder.Services.AddNeo4jAgentMemory(options => { - options.Uri = Environment.GetEnvironmentVariable("NEO4J_URI") ?? "bolt://localhost:7687"; - options.Username = Environment.GetEnvironmentVariable("NEO4J_USER") ?? "neo4j"; + 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(_ => { }); @@ -78,9 +78,9 @@ builder.Services.TryAddSingleton(embeddingGenerator); builder.Services.AddAgentMemoryFramework(options => { - options.AutoExtractOnPersist = true; - options.ContextFormat.IncludeEntities = true; - options.ContextFormat.IncludeFacts = true; + options.AutoExtractOnPersist = true; + options.ContextFormat.IncludeEntities = true; + options.ContextFormat.IncludeFacts = true; options.ContextFormat.IncludePreferences = true; options.ExposeMemoryToolsFromContextProvider = true; }); @@ -99,7 +99,7 @@ // ── The shopping assistant: context provider (recall + memory tools) + product tools ───────────── var memoryProvider = sp.GetRequiredService(); -var productTools = catalog.CreateAIFunctions(); +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. @@ -121,12 +121,12 @@ AIContextProviders = [memoryProvider], }).WithMemoryOwnerScoping(sp); -const string shopper = "shopper-amelia"; +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"); + .WithMemoryIdentity(userId: Shopper, sessionId: "cart-a", applicationId: "retail-demo"); foreach (var turn in new[] { @@ -140,7 +140,7 @@ // ── 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"); + .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.");