From ada5b83c8087ad0d11d5d4bdd7b444c9f39c4ff3 Mon Sep 17 00:00:00 2001
From: westey <164392973+westey-m@users.noreply.github.com>
Date: Tue, 4 Nov 2025 16:24:26 +0000
Subject: [PATCH] .NET: Add rag samples with sample TextSearchStore (#1664)
* Port store for adding text to a vector store to AF
* Fix typo.
* Change TextSearchStore to sample, and add sample to use it and do rag with a custom schema
* Add more tests and fix broken ones
* Fix merge issue
* Fix sample after merge.
* Convert TextSearchStore to use Dynamic mode to be AOT compatible.
* Add some more clarification on when to use assistant messages in rag searches.
---
dotnet/Directory.Packages.props | 2 +
dotnet/agent-framework-dotnet.slnx | 5 +
.../Catalog/AgentWithTextSearchRag/Program.cs | 18 +-
.../AgentWithRAG_Step01_BasicTextRAG.csproj | 22 +
.../Program.cs | 107 +++++
.../TextSearchStore/TextSearchDocument.cs | 51 +++
.../TextSearchStore/TextSearchStore.cs | 392 ++++++++++++++++++
.../TextSearchStore/TextSearchStoreOptions.cs | 140 +++++++
.../TextSearchStoreUpsertOptions.cs | 17 +
...ithRAG_Step02_ExternalDataSourceRAG.csproj | 22 +
.../Program.cs | 134 ++++++
.../README.md | 60 +++
.../GettingStarted/AgentWithRAG/README.md | 8 +
.../Agent_Step18_TextSearchRag/Program.cs | 22 +-
.../Data/TextSearchProvider.cs | 28 +-
.../Data/TextSearchProviderOptions.cs | 20 +
.../Microsoft.Agents.AI.csproj | 2 +
.../Data/TextSearchProviderTests.cs | 71 +++-
18 files changed, 1079 insertions(+), 42 deletions(-)
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreOptions.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStoreUpsertOptions.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/AgentWithRAG_Step02_ExternalDataSourceRAG.csproj
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/Program.cs
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step02_ExternalDataSourceRAG/README.md
create mode 100644 dotnet/samples/GettingStarted/AgentWithRAG/README.md
diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 08532904c4..69d3e03d31 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -65,9 +65,11 @@
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 1bfb5814f9..de8aef42fc 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -67,6 +67,11 @@
+
+
+
+
+
diff --git a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
index 931ce50014..3e86534edd 100644
--- a/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
+++ b/dotnet/samples/Catalog/AgentWithTextSearchRag/Program.cs
@@ -52,9 +52,9 @@ static Task> MockSearchAsync(st
{
results.Add(new()
{
- Name = "Contoso Outdoors Return Policy",
- Link = "https://contoso.com/policies/returns",
- Value = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
+ SourceName = "Contoso Outdoors Return Policy",
+ SourceLink = "https://contoso.com/policies/returns",
+ Text = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
});
}
@@ -62,9 +62,9 @@ static Task> MockSearchAsync(st
{
results.Add(new()
{
- Name = "Contoso Outdoors Shipping Guide",
- Link = "https://contoso.com/help/shipping",
- Value = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
+ SourceName = "Contoso Outdoors Shipping Guide",
+ SourceLink = "https://contoso.com/help/shipping",
+ Text = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
});
}
@@ -72,9 +72,9 @@ static Task> MockSearchAsync(st
{
results.Add(new()
{
- Name = "TrailRunner Tent Care Instructions",
- Link = "https://contoso.com/manuals/trailrunner-tent",
- Value = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
+ SourceName = "TrailRunner Tent Care Instructions",
+ SourceLink = "https://contoso.com/manuals/trailrunner-tent",
+ Text = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
});
}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj
new file mode 100644
index 0000000000..0c8a9f2dfc
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/AgentWithRAG_Step01_BasicTextRAG.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
new file mode 100644
index 0000000000..611e11c22c
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/Program.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use TextSearchProvider to add retrieval augmented generation (RAG) capabilities to an AI agent.
+// The sample uses an In-Memory vector store, which can easily be replaced with any other vector store that implements the Microsoft.Extensions.VectorData abstractions.
+// The TextSearchProvider runs a search against the vector store via the TextSearchStore before each model invocation and injects the results into the model context.
+// The TextSearchStore is a sample store implementation that hardcodes a storage schema and uses the vector store to store and retrieve documents.
+
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Data;
+using Microsoft.Agents.AI.Samples;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.VectorData;
+using Microsoft.SemanticKernel.Connectors.InMemory;
+using OpenAI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+var embeddingDeploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-3-large";
+
+AzureOpenAIClient azureOpenAIClient = new(
+ new Uri(endpoint),
+ new AzureCliCredential());
+
+// Create an In-Memory vector store that uses the Azure OpenAI embedding model to generate embeddings.
+VectorStore vectorStore = new InMemoryVectorStore(new()
+{
+ EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
+});
+
+// Create a store that defines a storage schema, and uses the vector store to store and retrieve documents.
+TextSearchStore textSearchStore = new(vectorStore, "product-and-policy-info", 3072);
+
+// Upload sample documents into the store.
+await textSearchStore.UpsertDocumentsAsync(GetSampleDocuments());
+
+// Create an adapter function that the TextSearchProvider can use to run searches against the TextSearchStore.
+Func>> SearchAdapter = async (text, ct) =>
+{
+ // Here we are limiting the search results to the single top result to demonstrate that we are accurately matching
+ // specific search results for each question, but in a real world case, more results should be used.
+ var searchResults = await textSearchStore.SearchAsync(text, 1, ct);
+ return searchResults.Select(r => new TextSearchProvider.TextSearchResult
+ {
+ SourceName = r.SourceName,
+ SourceLink = r.SourceLink,
+ Text = r.Text ?? string.Empty,
+ RawRepresentation = r
+ });
+};
+
+// Configure the options for the TextSearchProvider.
+TextSearchProviderOptions textSearchOptions = new()
+{
+ // Run the search prior to every model invocation.
+ SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
+};
+
+// Create the AI agent with the TextSearchProvider as the AI context provider.
+AIAgent agent = azureOpenAIClient
+ .GetChatClient(deploymentName)
+ .CreateAIAgent(new ChatClientAgentOptions
+ {
+ Instructions = "You are a helpful support specialist for Contoso Outdoors. Answer questions using the provided context and cite the source document when available.",
+ AIContextProviderFactory = ctx => ctx.SerializedState.ValueKind is not System.Text.Json.JsonValueKind.Null and not System.Text.Json.JsonValueKind.Undefined
+ ? new TextSearchProvider(SearchAdapter, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
+ : new TextSearchProvider(SearchAdapter, textSearchOptions)
+ });
+
+AgentThread thread = agent.GetNewThread();
+
+Console.WriteLine(">> Asking about returns\n");
+Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
+
+Console.WriteLine("\n>> Asking about shipping\n");
+Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
+
+Console.WriteLine("\n>> Asking about product care\n");
+Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
+
+// Produces some sample search documents.
+// Each one contains a source name and link, which the agent can use to cite sources in its responses.
+static IEnumerable GetSampleDocuments()
+{
+ yield return new TextSearchDocument
+ {
+ SourceId = "return-policy-001",
+ SourceName = "Contoso Outdoors Return Policy",
+ SourceLink = "https://contoso.com/policies/returns",
+ Text = "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection."
+ };
+ yield return new TextSearchDocument
+ {
+ SourceId = "shipping-guide-001",
+ SourceName = "Contoso Outdoors Shipping Guide",
+ SourceLink = "https://contoso.com/help/shipping",
+ Text = "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout."
+ };
+ yield return new TextSearchDocument
+ {
+ SourceId = "tent-care-001",
+ SourceName = "TrailRunner Tent Care Instructions",
+ SourceLink = "https://contoso.com/manuals/trailrunner-tent",
+ Text = "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating."
+ };
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs
new file mode 100644
index 0000000000..773d3ff6f3
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchDocument.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Samples;
+
+///
+/// Represents a document that can be used for Retrieval Augmented Generation (RAG) that stores textual data.
+///
+public sealed class TextSearchDocument
+{
+ ///
+ /// Gets or sets an optional list of namespaces that the document should belong to.
+ ///
+ ///
+ /// A namespace is a logical grouping of documents, e.g. may include a group id to scope the document to a specific group of users.
+ ///
+ public IList Namespaces { get; set; } = [];
+
+ ///
+ /// Gets or sets the content as text.
+ ///
+ public string? Text { get; set; }
+
+ ///
+ /// Gets or sets an optional source ID for the document.
+ ///
+ ///
+ /// This ID should be unique within the collection that the document is stored in, and can
+ /// be used to map back to the source artifact for this document.
+ /// If updates need to be made later or the source document was deleted and this document
+ /// also needs to be deleted, this id can be used to find the document again.
+ ///
+ public string? SourceId { get; set; }
+
+ ///
+ /// Gets or sets an optional name for the source document.
+ ///
+ ///
+ /// This can be used to provide display names for citation links when the document is referenced as
+ /// part of a response to a query.
+ ///
+ public string? SourceName { get; set; }
+
+ ///
+ /// Gets or sets an optional link back to the source of the document.
+ ///
+ ///
+ /// This can be used to provide citation links when the document is referenced as
+ /// part of a response to a query.
+ ///
+ public string? SourceLink { get; set; }
+}
diff --git a/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs
new file mode 100644
index 0000000000..502c17dba1
--- /dev/null
+++ b/dotnet/samples/GettingStarted/AgentWithRAG/AgentWithRAG_Step01_BasicTextRAG/TextSearchStore/TextSearchStore.cs
@@ -0,0 +1,392 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Linq.Expressions;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.VectorData;
+
+namespace Microsoft.Agents.AI.Samples;
+
+///
+/// A class that allows for easy storage and retrieval of documents in a Vector Store for Retrieval Augmented Generation (RAG).
+///
+///
+///
+/// This class provides an opinionated schema for storing documents in a vector store. It is valuable for simple scenarios
+/// where you want to store text + embedding, or a reference to an external document + embedding without needing to customize the schema.
+/// If you want to control the schema yourself, use an implementation of directly instead.
+///
+///
+/// This class and its related types are currently provided as a sample implementation, but may be promoted to a first-class supported API in future releases.
+///
+///
+public sealed partial class TextSearchStore : IDisposable
+{
+#if NET
+ [GeneratedRegex(@"\p{L}+", RegexOptions.IgnoreCase, "en-US")]
+ private static partial Regex AnyLanguageWordRegex();
+
+ private static readonly Func> s_defaultWordSegmenter = text => AnyLanguageWordRegex().Matches(text).Select(x => x.Value).ToList();
+#else
+ private static readonly Regex s_anyLanguageWordRegex = new(@"\p{L}+", RegexOptions.Compiled);
+ private static Regex AnyLanguageWordRegex() => s_anyLanguageWordRegex;
+
+ private static readonly Func> s_defaultWordSegmenter = text =>
+ {
+ List words = new();
+ foreach (Match word in AnyLanguageWordRegex().Matches(text))
+ {
+ words.Add(word.Value);
+ }
+ return words;
+ };
+#endif
+
+ private readonly VectorStore _vectorStore;
+ private readonly TextSearchStoreOptions _options;
+ private readonly Func> _wordSegmenter;
+
+ private readonly VectorStoreCollection