chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.DocumentLayoutAnalysis.PageSegmenter;
namespace VectorStoreRAG;
/// <summary>
/// Class that loads text from a PDF file into a vector store.
/// </summary>
/// <typeparam name="TKey">The type of the data model key.</typeparam>
/// <param name="uniqueKeyGenerator">A function to generate unique keys with.</param>
/// <param name="vectorStoreRecordCollection">The collection to load the data into.</param>
/// <param name="chatCompletionService">The chat completion service to use for generating text from images.</param>
internal sealed class DataLoader<TKey>(
UniqueKeyGenerator<TKey> uniqueKeyGenerator,
VectorStoreCollection<TKey, TextSnippet<TKey>> vectorStoreRecordCollection,
IChatCompletionService chatCompletionService) : IDataLoader where TKey : notnull
{
/// <inheritdoc/>
public async Task LoadPdf(string pdfPath, int batchSize, int betweenBatchDelayInMs, CancellationToken cancellationToken)
{
// Create the collection if it doesn't exist.
await vectorStoreRecordCollection.EnsureCollectionExistsAsync(cancellationToken).ConfigureAwait(false);
// Load the text and images from the PDF file and split them into batches.
var sections = LoadTextAndImages(pdfPath, cancellationToken);
var batches = sections.Chunk(batchSize);
// Process each batch of content items.
foreach (var batch in batches)
{
// Convert any images to text.
var textContentTasks = batch.Select(async content =>
{
if (content.Text != null)
{
return content;
}
var textFromImage = await ConvertImageToTextWithRetryAsync(
chatCompletionService,
content.Image!.Value,
cancellationToken).ConfigureAwait(false);
return new RawContent { Text = textFromImage, PageNumber = content.PageNumber };
});
var textContent = await Task.WhenAll(textContentTasks).ConfigureAwait(false);
// Map each paragraph to a TextSnippet.
var records = textContent.Select(content => new TextSnippet<TKey>
{
Key = uniqueKeyGenerator.GenerateKey(),
// The vector store will automatically generate the embedding for this text.
// See the TextEmbedding field on the TextSnippet class.
Text = content.Text,
ReferenceDescription = $"{new FileInfo(pdfPath).Name}#page={content.PageNumber}",
ReferenceLink = $"{new Uri(new FileInfo(pdfPath).FullName).AbsoluteUri}#page={content.PageNumber}",
});
// Upsert the records into the vector store.
await vectorStoreRecordCollection.UpsertAsync(records, cancellationToken: cancellationToken).ConfigureAwait(false);
await Task.Delay(betweenBatchDelayInMs, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Read the text and images from each page in the provided PDF file.
/// </summary>
/// <param name="pdfPath">The pdf file to read the text and images from.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>The text and images from the pdf file, plus the page number that each is on.</returns>
private static IEnumerable<RawContent> LoadTextAndImages(string pdfPath, CancellationToken cancellationToken)
{
using (PdfDocument document = PdfDocument.Open(pdfPath))
{
foreach (Page page in document.GetPages())
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
foreach (var image in page.GetImages())
{
if (image.TryGetPng(out var png))
{
yield return new RawContent { Image = png, PageNumber = page.Number };
}
else
{
Console.WriteLine($"Unsupported image format on page {page.Number}");
}
}
var blocks = DefaultPageSegmenter.Instance.GetBlocks(page.GetWords());
foreach (var block in blocks)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
yield return new RawContent { Text = block.Text, PageNumber = page.Number };
}
}
}
}
/// <summary>
/// Add a simple retry mechanism to image to text.
/// </summary>
/// <param name="chatCompletionService">The chat completion service to use for generating text from images.</param>
/// <param name="imageBytes">The image to generate the text for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>The generated text.</returns>
private static async Task<string> ConvertImageToTextWithRetryAsync(
IChatCompletionService chatCompletionService,
ReadOnlyMemory<byte> imageBytes,
CancellationToken cancellationToken)
{
var tries = 0;
while (true)
{
try
{
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage([
new TextContent("Whats in this image?"),
new ImageContent(imageBytes, "image/png"),
]);
var result = await chatCompletionService.GetChatMessageContentsAsync(chatHistory, cancellationToken: cancellationToken).ConfigureAwait(false);
return string.Join("\n", result.Select(x => x.Content));
}
catch (HttpOperationException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
tries++;
if (tries < 3)
{
Console.WriteLine($"Failed to generate text from image. Error: {ex}");
Console.WriteLine("Retrying text to image conversion...");
await Task.Delay(10_000, cancellationToken).ConfigureAwait(false);
}
else
{
throw;
}
}
}
}
/// <summary>
/// Private model for returning the content items from a PDF file.
/// </summary>
private sealed class RawContent
{
public string? Text { get; init; }
public ReadOnlyMemory<byte>? Image { get; init; }
public int PageNumber { get; init; }
}
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VectorStoreRAG;
/// <summary>
/// Interface for loading data into a data store.
/// </summary>
internal interface IDataLoader
{
/// <summary>
/// Load the text from a PDF file into the data store.
/// </summary>
/// <param name="pdfPath">The pdf file to load.</param>
/// <param name="batchSize">Maximum number of parallel threads to generate embeddings and upload records.</param>
/// <param name="betweenBatchDelayInMs">The number of milliseconds to delay between batches to avoid throttling.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>An async task that completes when the loading is complete.</returns>
Task LoadPdf(string pdfPath, int batchSize, int betweenBatchDelayInMs, CancellationToken cancellationToken);
}
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.Configuration;
namespace VectorStoreRAG.Options;
/// <summary>
/// Helper class to load all configuration settings for the VectorStoreRAG project.
/// </summary>
internal sealed class ApplicationConfig
{
private readonly AzureOpenAIConfig _azureOpenAIConfig;
private readonly AzureOpenAIEmbeddingsConfig _azureOpenAIEmbeddingsConfig = new();
private readonly OpenAIConfig _openAIConfig = new();
private readonly OpenAIEmbeddingsConfig _openAIEmbeddingsConfig = new();
private readonly RagConfig _ragConfig = new();
private readonly AzureAISearchConfig _azureAISearchConfig = new();
private readonly CosmosConfig _cosmosMongoConfig = new();
private readonly CosmosConfig _cosmosNoSqlConfig = new();
private readonly QdrantConfig _qdrantConfig = new();
private readonly RedisConfig _redisConfig = new();
private readonly WeaviateConfig _weaviateConfig = new();
public ApplicationConfig(ConfigurationManager configurationManager)
{
this._azureOpenAIConfig = new();
configurationManager
.GetRequiredSection($"AIServices:{AzureOpenAIConfig.ConfigSectionName}")
.Bind(this._azureOpenAIConfig);
configurationManager
.GetRequiredSection($"AIServices:{AzureOpenAIEmbeddingsConfig.ConfigSectionName}")
.Bind(this._azureOpenAIEmbeddingsConfig);
configurationManager
.GetRequiredSection($"AIServices:{OpenAIConfig.ConfigSectionName}")
.Bind(this._openAIConfig);
configurationManager
.GetRequiredSection($"AIServices:{OpenAIEmbeddingsConfig.ConfigSectionName}")
.Bind(this._openAIEmbeddingsConfig);
configurationManager
.GetRequiredSection(RagConfig.ConfigSectionName)
.Bind(this._ragConfig);
configurationManager
.GetRequiredSection($"VectorStores:{AzureAISearchConfig.ConfigSectionName}")
.Bind(this._azureAISearchConfig);
configurationManager
.GetRequiredSection($"VectorStores:{CosmosConfig.MongoConfigSectionName}")
.Bind(this._cosmosMongoConfig);
configurationManager
.GetRequiredSection($"VectorStores:{CosmosConfig.NoSqlConfigSectionName}")
.Bind(this._cosmosNoSqlConfig);
configurationManager
.GetRequiredSection($"VectorStores:{QdrantConfig.ConfigSectionName}")
.Bind(this._qdrantConfig);
configurationManager
.GetRequiredSection($"VectorStores:{RedisConfig.ConfigSectionName}")
.Bind(this._redisConfig);
configurationManager
.GetRequiredSection($"VectorStores:{WeaviateConfig.ConfigSectionName}")
.Bind(this._weaviateConfig);
}
public AzureOpenAIConfig AzureOpenAIConfig => this._azureOpenAIConfig;
public AzureOpenAIEmbeddingsConfig AzureOpenAIEmbeddingsConfig => this._azureOpenAIEmbeddingsConfig;
public OpenAIConfig OpenAIConfig => this._openAIConfig;
public OpenAIEmbeddingsConfig OpenAIEmbeddingsConfig => this._openAIEmbeddingsConfig;
public RagConfig RagConfig => this._ragConfig;
public AzureAISearchConfig AzureAISearchConfig => this._azureAISearchConfig;
public CosmosConfig CosmosMongoConfig => this._cosmosMongoConfig;
public CosmosConfig CosmosNoSqlConfig => this._cosmosNoSqlConfig;
public QdrantConfig QdrantConfig => this._qdrantConfig;
public RedisConfig RedisConfig => this._redisConfig;
public WeaviateConfig WeaviateConfig => this._weaviateConfig;
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Azure AI Search service settings.
/// </summary>
internal sealed class AzureAISearchConfig
{
public const string ConfigSectionName = "AzureAISearch";
[Required]
public string Endpoint { get; set; } = string.Empty;
[Required]
public string ApiKey { get; set; } = string.Empty;
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Azure OpenAI service settings.
/// </summary>
internal sealed class AzureOpenAIConfig
{
public const string ConfigSectionName = "AzureOpenAI";
[Required]
public string ChatDeploymentName { get; set; } = string.Empty;
[Required]
public string Endpoint { get; set; } = string.Empty;
}
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Azure OpenAI Embeddings service settings.
/// </summary>
internal sealed class AzureOpenAIEmbeddingsConfig
{
public const string ConfigSectionName = "AzureOpenAIEmbeddings";
[Required]
public string DeploymentName { get; set; } = string.Empty;
[Required]
public string Endpoint { get; set; } = string.Empty;
}
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Azure CosmosDB service settings for use with CosmosMongo and CosmosNoSql.
/// </summary>
internal sealed class CosmosConfig
{
public const string MongoConfigSectionName = "CosmosMongoDB";
public const string NoSqlConfigSectionName = "CosmosNoSql";
[Required]
public string ConnectionString { get; set; } = string.Empty;
[Required]
public string DatabaseName { get; set; } = string.Empty;
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// OpenAI service settings.
/// </summary>
internal sealed class OpenAIConfig
{
public const string ConfigSectionName = "OpenAI";
[Required]
public string ModelId { get; set; } = string.Empty;
[Required]
public string ApiKey { get; set; } = string.Empty;
[Required]
public string? OrgId { get; set; } = null;
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// OpenAI Embeddings service settings.
/// </summary>
internal sealed class OpenAIEmbeddingsConfig
{
public const string ConfigSectionName = "OpenAIEmbeddings";
[Required]
public string ModelId { get; set; } = string.Empty;
[Required]
public string ApiKey { get; set; } = string.Empty;
[Required]
public string? OrgId { get; set; } = null;
}
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Qdrant service settings.
/// </summary>
internal sealed class QdrantConfig
{
public const string ConfigSectionName = "Qdrant";
[Required]
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 6334;
public bool Https { get; set; } = false;
public string ApiKey { get; set; } = string.Empty;
}
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Contains settings to control the RAG experience.
/// </summary>
internal sealed class RagConfig
{
public const string ConfigSectionName = "Rag";
[Required]
public string AIChatService { get; set; } = string.Empty;
[Required]
public string AIEmbeddingService { get; set; } = string.Empty;
[Required]
public bool BuildCollection { get; set; } = true;
[Required]
public string CollectionName { get; set; } = string.Empty;
[Required]
public int DataLoadingBatchSize { get; set; } = 2;
[Required]
public int DataLoadingBetweenBatchDelayInMilliseconds { get; set; } = 0;
[Required]
public string[]? PdfFilePaths { get; set; }
[Required]
public string VectorStoreType { get; set; } = string.Empty;
}
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Redis service settings.
/// </summary>
internal sealed class RedisConfig
{
public const string ConfigSectionName = "Redis";
[Required]
public string ConnectionConfiguration { get; set; } = string.Empty;
}
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
namespace VectorStoreRAG.Options;
/// <summary>
/// Weaviate service settings.
/// </summary>
internal sealed class WeaviateConfig
{
public const string ConfigSectionName = "Weaviate";
[Required]
public string Endpoint { get; set; } = string.Empty;
}
@@ -0,0 +1,154 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Globalization;
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.SemanticKernel;
using OpenAI;
using VectorStoreRAG;
using VectorStoreRAG.Options;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Configure configuration and load the application configuration.
builder.Configuration.AddUserSecrets<Program>();
builder.Services.Configure<RagConfig>(builder.Configuration.GetSection(RagConfig.ConfigSectionName));
var appConfig = new ApplicationConfig(builder.Configuration);
// Create a cancellation token and source to pass to the application service to allow them
// to request a graceful application shutdown.
CancellationTokenSource appShutdownCancellationTokenSource = new();
CancellationToken appShutdownCancellationToken = appShutdownCancellationTokenSource.Token;
builder.Services.AddKeyedSingleton("AppShutdown", appShutdownCancellationTokenSource);
// Register the kernel with the dependency injection container
// and add Chat Completion and Text Embedding Generation services.
var kernelBuilder = builder.Services.AddKernel();
switch (appConfig.RagConfig.AIChatService)
{
case "AzureOpenAI":
kernelBuilder.AddAzureOpenAIChatCompletion(
appConfig.AzureOpenAIConfig.ChatDeploymentName,
appConfig.AzureOpenAIConfig.Endpoint,
new AzureCliCredential());
break;
case "OpenAI":
kernelBuilder.AddOpenAIChatCompletion(
appConfig.OpenAIConfig.ModelId,
appConfig.OpenAIConfig.ApiKey,
appConfig.OpenAIConfig.OrgId);
break;
default:
throw new NotSupportedException($"AI Chat Service type '{appConfig.RagConfig.AIChatService}' is not supported.");
}
switch (appConfig.RagConfig.AIEmbeddingService)
{
case "AzureOpenAIEmbeddings":
builder.Services.AddSingleton<IEmbeddingGenerator>(
sp => new AzureOpenAIClient(new Uri(appConfig.AzureOpenAIEmbeddingsConfig.Endpoint), new AzureCliCredential())
.GetEmbeddingClient(appConfig.AzureOpenAIEmbeddingsConfig.DeploymentName)
.AsIEmbeddingGenerator());
break;
case "OpenAIEmbeddings":
builder.Services.AddSingleton<IEmbeddingGenerator>(
sp => new OpenAIClient(appConfig.OpenAIEmbeddingsConfig.ApiKey)
.GetEmbeddingClient(appConfig.OpenAIEmbeddingsConfig.ModelId)
.AsIEmbeddingGenerator());
break;
default:
throw new NotSupportedException($"AI Embedding Service type '{appConfig.RagConfig.AIEmbeddingService}' is not supported.");
}
// Add the configured vector store record collection type to the
// dependency injection container.
switch (appConfig.RagConfig.VectorStoreType)
{
case "AzureAISearch":
kernelBuilder.Services.AddAzureAISearchCollection<TextSnippet<string>>(
appConfig.RagConfig.CollectionName,
new Uri(appConfig.AzureAISearchConfig.Endpoint),
new AzureKeyCredential(appConfig.AzureAISearchConfig.ApiKey));
break;
case "CosmosMongoDB":
kernelBuilder.Services.AddCosmosMongoCollection<TextSnippet<string>>(
appConfig.RagConfig.CollectionName,
appConfig.CosmosMongoConfig.ConnectionString,
appConfig.CosmosMongoConfig.DatabaseName);
break;
case "CosmosNoSql":
kernelBuilder.Services.AddCosmosNoSqlCollection<string, TextSnippet<string>>(
appConfig.RagConfig.CollectionName,
appConfig.CosmosNoSqlConfig.ConnectionString,
appConfig.CosmosNoSqlConfig.DatabaseName);
break;
case "InMemory":
kernelBuilder.Services.AddInMemoryVectorStoreRecordCollection<string, TextSnippet<string>>(
appConfig.RagConfig.CollectionName);
break;
case "Qdrant":
kernelBuilder.Services.AddQdrantCollection<Guid, TextSnippet<Guid>>(
appConfig.RagConfig.CollectionName,
appConfig.QdrantConfig.Host,
appConfig.QdrantConfig.Port,
appConfig.QdrantConfig.Https,
appConfig.QdrantConfig.ApiKey);
break;
case "Redis":
kernelBuilder.Services.AddRedisJsonCollection<TextSnippet<string>>(
appConfig.RagConfig.CollectionName,
appConfig.RedisConfig.ConnectionConfiguration);
break;
case "Weaviate":
kernelBuilder.Services.AddWeaviateCollection<TextSnippet<Guid>>(
// Weaviate collection names must start with an upper case letter.
char.ToUpper(appConfig.RagConfig.CollectionName[0], CultureInfo.InvariantCulture) + appConfig.RagConfig.CollectionName.Substring(1),
endpoint: new Uri(appConfig.WeaviateConfig.Endpoint),
apiKey: null);
break;
default:
throw new NotSupportedException($"Vector store type '{appConfig.RagConfig.VectorStoreType}' is not supported.");
}
// Register all the other required services.
switch (appConfig.RagConfig.VectorStoreType)
{
case "AzureAISearch":
case "CosmosMongoDB":
case "CosmosNoSql":
case "InMemory":
case "Redis":
RegisterServices<string>(builder, kernelBuilder, appConfig);
break;
case "Qdrant":
case "Weaviate":
RegisterServices<Guid>(builder, kernelBuilder, appConfig);
break;
default:
throw new NotSupportedException($"Vector store type '{appConfig.RagConfig.VectorStoreType}' is not supported.");
}
// Build and run the host.
using IHost host = builder.Build();
await host.RunAsync(appShutdownCancellationToken).ConfigureAwait(false);
static void RegisterServices<TKey>(HostApplicationBuilder builder, IKernelBuilder kernelBuilder, ApplicationConfig vectorStoreRagConfig)
where TKey : notnull
{
// Add a text search implementation that uses the registered vector store record collection for search.
kernelBuilder.AddVectorStoreTextSearch<TextSnippet<TKey>>();
// Add the key generator and data loader to the dependency injection container.
builder.Services.AddSingleton<UniqueKeyGenerator<Guid>>(new UniqueKeyGenerator<Guid>(() => Guid.NewGuid()));
builder.Services.AddSingleton<UniqueKeyGenerator<string>>(new UniqueKeyGenerator<string>(() => Guid.NewGuid().ToString()));
builder.Services.AddSingleton<IDataLoader, DataLoader<TKey>>();
// Add the main service for this application.
builder.Services.AddHostedService<RAGChatService<TKey>>();
}
@@ -0,0 +1,187 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Data;
using Microsoft.SemanticKernel.PromptTemplates.Handlebars;
using VectorStoreRAG.Options;
namespace VectorStoreRAG;
/// <summary>
/// Main service class for the application.
/// </summary>
/// <typeparam name="TKey">The type of the data model key.</typeparam>
/// <param name="dataLoader">Used to load data into the vector store.</param>
/// <param name="vectorStoreTextSearch">Used to search the vector store.</param>
/// <param name="kernel">Used to make requests to the LLM.</param>
/// <param name="ragConfigOptions">The configuration options for the application.</param>
/// <param name="appShutdownCancellationTokenSource">Used to gracefully shut down the entire application when cancelled.</param>
internal sealed class RAGChatService<TKey>(
IDataLoader dataLoader,
VectorStoreTextSearch<TextSnippet<TKey>> vectorStoreTextSearch,
Kernel kernel,
IOptions<RagConfig> ragConfigOptions,
[FromKeyedServices("AppShutdown")] CancellationTokenSource appShutdownCancellationTokenSource) : IHostedService
{
private Task? _dataLoaded;
private Task? _chatLoop;
/// <summary>
/// Start the service.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>An async task that completes when the service is started.</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
// Start to load all the configured PDFs into the vector store.
if (ragConfigOptions.Value.BuildCollection)
{
this._dataLoaded = this.LoadDataAsync(cancellationToken);
}
else
{
this._dataLoaded = Task.CompletedTask;
}
// Start the chat loop.
this._chatLoop = this.ChatLoopAsync(cancellationToken);
return Task.CompletedTask;
}
/// <summary>
/// Stop the service.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>An async task that completes when the service is stopped.</returns>
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
/// <summary>
/// Contains the main chat loop for the application.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>An async task that completes when the chat loop is shut down.</returns>
private async Task ChatLoopAsync(CancellationToken cancellationToken)
{
var pdfFiles = string.Join(", ", ragConfigOptions.Value.PdfFilePaths ?? []);
// Wait for the data to be loaded before starting the chat loop.
while (this._dataLoaded != null && !this._dataLoaded.IsCompleted && !cancellationToken.IsCancellationRequested)
{
await Task.Delay(1_000, cancellationToken).ConfigureAwait(false);
}
// If data loading failed, don't start the chat loop.
if (this._dataLoaded != null && this._dataLoaded.IsFaulted)
{
Console.WriteLine("Failed to load data");
return;
}
Console.WriteLine("PDF loading complete\n");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Assistant > Press enter with no prompt to exit.");
// Add a search plugin to the kernel which we will use in the template below
// to do a vector search for related information to the user query.
kernel.Plugins.Add(vectorStoreTextSearch.CreateWithGetTextSearchResults("SearchPlugin"));
// Start the chat loop.
while (!cancellationToken.IsCancellationRequested)
{
// Prompt the user for a question.
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Assistant > What would you like to know from the loaded PDFs: ({pdfFiles})?");
// Read the user question.
Console.ForegroundColor = ConsoleColor.White;
Console.Write("User > ");
var question = Console.ReadLine();
// Exit the application if the user didn't type anything.
if (string.IsNullOrWhiteSpace(question))
{
appShutdownCancellationTokenSource.Cancel();
break;
}
// Invoke the LLM with a template that uses the search plugin to
// 1. get related information to the user query from the vector store
// 2. add the information to the LLM prompt.
var response = kernel.InvokePromptStreamingAsync(
promptTemplate: """
Please use this information to answer the question:
{{#with (SearchPlugin-GetTextSearchResults question)}}
{{#each this}}
Name: {{Name}}
Value: {{Value}}
Link: {{Link}}
-----------------
{{/each}}
{{/with}}
Include citations to the relevant information where it is referenced in the response.
Question: {{question}}
""",
arguments: new KernelArguments()
{
{ "question", question },
},
templateFormat: "handlebars",
promptTemplateFactory: new HandlebarsPromptTemplateFactory(),
cancellationToken: cancellationToken);
// Stream the LLM response to the console with error handling.
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("\nAssistant > ");
try
{
await foreach (var message in response.ConfigureAwait(false))
{
Console.Write(message);
}
Console.WriteLine();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Call to LLM failed with error: {ex}");
}
}
}
/// <summary>
/// Load all configured PDFs into the vector store.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.</param>
/// <returns>An async task that completes when the loading is complete.</returns>
private async Task LoadDataAsync(CancellationToken cancellationToken)
{
try
{
foreach (var pdfFilePath in ragConfigOptions.Value.PdfFilePaths ?? [])
{
Console.WriteLine($"Loading PDF into vector store: {pdfFilePath}");
await dataLoader.LoadPdf(
pdfFilePath,
ragConfigOptions.Value.DataLoadingBatchSize,
ragConfigOptions.Value.DataLoadingBetweenBatchDelayInMilliseconds,
cancellationToken).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Console.WriteLine($"Failed to load PDFs: {ex}");
throw;
}
}
}
@@ -0,0 +1,182 @@
# Vector Store RAG Demo
This sample demonstrates how to ingest text from pdf files into a vector store and ask questions about the content
using an LLM while using RAG to supplement the LLM with additional information from the vector store.
## Configuring the Sample
The sample can be configured in various ways:
1. You can choose your preferred vector store by setting the `Rag:VectorStoreType` configuration setting in the `appsettings.json` file to one of the following values:
1. AzureAISearch
1. CosmosMongoDB
1. CosmosNoSql
1. InMemory
1. Qdrant
1. Redis
1. Weaviate
1. You can choose your preferred AI Chat service by settings the `Rag:AIChatService` configuration setting in the `appsettings.json` file to one of the following values:
1. AzureOpenAI
1. OpenAI
1. You can choose your preferred AI Embedding service by settings the `Rag:AIEmbeddingService` configuration setting in the `appsettings.json` file to one of the following values:
1. AzureOpenAIEmbeddings
1. OpenAIEmbeddings
1. You can choose whether to load data into the vector store by setting the `Rag:BuildCollection` configuration setting in the `appsettings.json` file to `true`. If you set this to `false`, the sample will assume that data was already loaded previously and it will go straight into the chat experience.
1. You can choose the name of the collection to use by setting the `Rag:CollectionName` configuration setting in the `appsettings.json` file.
1. You can choose the pdf file to load into the vector store by setting the `Rag:PdfFilePaths` array in the `appsettings.json` file.
1. You can choose the number of records to process per batch when loading data into the vector store by setting the `Rag:DataLoadingBatchSize` configuration setting in the `appsettings.json` file.
1. You can choose the number of milliseconds to wait between batches when loading data into the vector store by setting the `Rag:DataLoadingBetweenBatchDelayInMilliseconds` configuration setting in the `appsettings.json` file.
## Dependency Setup
To run this sample, you need to setup your source data, setup your vector store and AI services, and setup secrets for these.
### Source PDF File
You will need to supply some source pdf files to load into the vector store.
Once you have a file ready, update the `PdfFilePaths` array in the `appsettings.json` file with the path to the file.
```json
{
"Rag": {
"PdfFilePaths": [ "sourcedocument.pdf" ],
}
}
```
Why not try the semantic kernel documentation as your input.
You can download it as a PDF from the https://learn.microsoft.com/en-us/semantic-kernel/overview/ page.
See the Download PDF button at the bottom of the page.
### Azure OpenAI Chat Completion
For Azure OpenAI Chat Completion, you need to add the following secrets:
```cli
dotnet user-secrets set "AIServices:AzureOpenAI:Endpoint" "https://<yourservice>.openai.azure.com"
dotnet user-secrets set "AIServices:AzureOpenAI:ChatDeploymentName" "<your deployment name>"
```
Note that the code doesn't use an API Key to communicate with Azure OpenAI, but rather an `AzureCliCredential` so no api key secret is required.
### OpenAI Chat Completion
For OpenAI Chat Completion, you need to add the following secrets:
```cli
dotnet user-secrets set "AIServices:OpenAI:ModelId" "<your model id>"
dotnet user-secrets set "AIServices:OpenAI:ApiKey" "<your api key>"
```
Optionally, you can also provide an Org Id
```cli
dotnet user-secrets set "AIServices:OpenAI:OrgId" "<your org id>"
```
### Azure OpenAI Embeddings
For Azure OpenAI Embeddings, you need to add the following secrets:
```cli
dotnet user-secrets set "AIServices:AzureOpenAIEmbeddings:Endpoint" "https://<yourservice>.openai.azure.com"
dotnet user-secrets set "AIServices:AzureOpenAIEmbeddings:DeploymentName" "<your deployment name>"
```
Note that the code doesn't use an API Key to communicate with Azure OpenAI, but rather an `AzureCliCredential` so no api key secret is required.
### OpenAI Embeddings
For OpenAI Embeddings, you need to add the following secrets:
```cli
dotnet user-secrets set "AIServices:OpenAIEmbeddings:ModelId" "<your model id>"
dotnet user-secrets set "AIServices:OpenAIEmbeddings:ApiKey" "<your api key>"
```
Optionally, you can also provide an Org Id
```cli
dotnet user-secrets set "AIServices:OpenAIEmbeddings:OrgId" "<your org id>"
```
### Azure AI Search
If you want to use Azure AI Search as your vector store, you will need to create an instance of Azure AI Search and add
the following secrets here:
```cli
dotnet user-secrets set "VectorStores:AzureAISearch:Endpoint" "https://<yourservice>.search.windows.net"
dotnet user-secrets set "VectorStores:AzureAISearch:ApiKey" "<yoursecret>"
```
### Azure CosmosDB MongoDB
If you want to use Azure CosmosDB MongoDB as your vector store, you will need to create an instance of Azure CosmosDB MongoDB and add
the following secrets here:
```cli
dotnet user-secrets set "VectorStores:CosmosMongoDB:ConnectionString" "<yourconnectionstring>"
dotnet user-secrets set "VectorStores:CosmosMongoDB:DatabaseName" "<yourdbname>"
```
### Azure CosmosDB NoSQL
If you want to use Azure CosmosDB NoSQL as your vector store, you will need to create an instance of Azure CosmosDB NoSQL and add
the following secrets here:
```cli
dotnet user-secrets set "VectorStores:CosmosNoSql:ConnectionString" "<yourconnectionstring>"
dotnet user-secrets set "VectorStores:CosmosNoSql:DatabaseName" "<yourdbname>"
```
### Qdrant
If you want to use Qdrant as your vector store, you will need to have an instance of Qdrant available.
You can use the following command to start a Qdrant instance in docker, and this will work with the default configured settings:
```cli
docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
```
If you want to use a different instance of Qdrant, you can update the appsettings.json file or add the following secrets to reconfigure:
```cli
dotnet user-secrets set "VectorStores:Qdrant:Host" "<yourservice>"
dotnet user-secrets set "VectorStores:Qdrant:Port" "6334"
dotnet user-secrets set "VectorStores:Qdrant:Https" "true"
dotnet user-secrets set "VectorStores:Qdrant:ApiKey" "<yoursecret>"
```
### Redis
If you want to use Redis as your vector store, you will need to have an instance of Redis available.
You can use the following command to start a Redis instance in docker, and this will work with the default configured settings:
```cli
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
```
If you want to use a different instance of Redis, you can update the appsettings.json file or add the following secret to reconfigure:
```cli
dotnet user-secrets set "VectorStores:Redis:ConnectionConfiguration" "<yourredisconnectionconfiguration>"
```
### Weaviate
If you want to use Weaviate as your vector store, you will need to have an instance of Weaviate available.
You can use the following command to start a Weaviate instance in docker, and this will work with the default configured settings:
```cli
docker run -d --name weaviate -p 8080:8080 -p 50051:50051 cr.weaviate.io/semitechnologies/weaviate:1.26.4
```
If you want to use a different instance of Weaviate, you can update the appsettings.json file or add the following secret to reconfigure:
```cli
dotnet user-secrets set "VectorStores:Weaviate:Endpoint" "<yourweaviateurl>"
```
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Data;
namespace VectorStoreRAG;
/// <summary>
/// Data model for storing a section of text with an embedding and an optional reference link.
/// </summary>
/// <typeparam name="TKey">The type of the data model key.</typeparam>
internal sealed class TextSnippet<TKey>
{
[VectorStoreKey]
public required TKey Key { get; set; }
[TextSearchResultValue]
[VectorStoreData]
public string? Text { get; set; }
[TextSearchResultName]
[VectorStoreData]
public string? ReferenceDescription { get; set; }
[TextSearchResultLink]
[VectorStoreData]
public string? ReferenceLink { get; set; }
/// <summary>
/// The text embedding for this snippet. This is used to search the vector store.
/// While this is a string property it has the vector attribute, which means whatever
/// text it contains will be converted to a vector and stored as a vector in the vector store.
/// </summary>
[VectorStoreVector(1536)]
public string? TextEmbedding => this.Text;
}
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All rights reserved.
namespace VectorStoreRAG;
/// <summary>
/// Class for generating unique keys via a provided function.
/// </summary>
/// <typeparam name="TKey">The type of key to generate.</typeparam>
/// <param name="generator">The function to generate the key with.</param>
internal sealed class UniqueKeyGenerator<TKey>(Func<TKey> generator)
where TKey : notnull
{
/// <summary>
/// Generate a unique key.
/// </summary>
/// <returns>The unique key that was generated.</returns>
public TKey GenerateKey() => generator();
}
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);SKEXP0001;SKEXP0010</NoWarn>
<UserSecretsId>c4203b00-7179-47c1-8701-ee352e381412</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" />
<PackageReference Include="PdfPig" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureAISearch" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.CosmosMongoDB" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.CosmosNoSql" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.InMemory" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Qdrant" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Redis" />
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Weaviate" />
<PackageReference Include="SharpCompress" /> <!-- Pin to patched version; overrides transitive 0.30.1 from MongoDB.Driver (GHSA-6c8g-7p36-r338) -->
<PackageReference Include="Snappier" /> <!-- Pin to patched version; overrides transitive 1.0.0 from MongoDB.Driver (GHSA-pggp-6c3x-2xmx) -->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj" />
<ProjectReference Include="..\..\..\src\Extensions\PromptTemplates.Handlebars\PromptTemplates.Handlebars.csproj" />
<ProjectReference Include="..\..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,63 @@
{
"Logging": {
"LogLevel": {
"Default": "None"
}
},
"AIServices": {
"AzureOpenAI": {
"Endpoint": "",
"ChatDeploymentName": "gpt-4o"
},
"AzureOpenAIEmbeddings": {
"Endpoint": "",
"DeploymentName": "text-embedding-ada-002"
},
"OpenAI": {
"ModelId": "gpt-4o",
"ApiKey": "",
"OrgId": null
},
"OpenAIEmbeddings": {
"ModelId": "text-embedding-3-small",
"ApiKey": "",
"OrgId": null
}
},
"VectorStores": {
"AzureAISearch": {
"Endpoint": "",
"ApiKey": ""
},
"CosmosMongoDB": {
"ConnectionString": "",
"DatabaseName": ""
},
"CosmosNoSql": {
"ConnectionString": "",
"DatabaseName": ""
},
"Qdrant": {
"Host": "localhost",
"Port": 6334,
"Https": false,
"ApiKey": ""
},
"Redis": {
"ConnectionConfiguration": "localhost:6379"
},
"Weaviate": {
"Endpoint": "http://localhost:8080/v1/"
}
},
"Rag": {
"AIChatService": "OpenAI",
"AIEmbeddingService": "OpenAIEmbeddings",
"BuildCollection": true,
"CollectionName": "pdfcontent",
"DataLoadingBatchSize": 10,
"DataLoadingBetweenBatchDelayInMilliseconds": 1000,
"PdfFilePaths": [ "sourcedocument.pdf" ],
"VectorStoreType": "InMemory"
}
}