diff --git a/03-AIPatternsAndApplications/01-embeddings-semantic-search.md b/03-AIPatternsAndApplications/01-embeddings-semantic-search.md index 9f3ad53b..53117dac 100644 --- a/03-AIPatternsAndApplications/01-embeddings-semantic-search.md +++ b/03-AIPatternsAndApplications/01-embeddings-semantic-search.md @@ -15,13 +15,14 @@ _Click the image to watch the video_ Traditional search works by matching keywords. If you search for "running shoes," it finds documents containing those exact words. But what if someone searches for: + - "sneakers for jogging" - "athletic footwear" - "shoes for my morning run" These all mean the same thing, but keyword search would miss them. -**Semantic search** solves this by understanding *meaning*, not just words. +**Semantic search** solves this by understanding _meaning_, not just words. --- @@ -33,7 +34,7 @@ An **embedding** is a way to represent text (or images, or other data) as a list "I love pizza" → [0.12, -0.45, 0.89, 0.23, -0.67, ...] ``` -These numbers capture the *meaning* of the text. Similar meanings produce similar numbers. +These numbers capture the _meaning_ of the text. Similar meanings produce similar numbers. > **Learn more:** [Understand embeddings in .NET](https://learn.microsoft.com/dotnet/ai/conceptual/embeddings) explains how embedding models work and when to use them. @@ -129,6 +130,7 @@ static float CosineSimilarity(ReadOnlyMemory a, ReadOnlyMemory b) ``` Cosine similarity returns a value between -1 and 1: + - **1.0** = Identical meaning - **0.0** = Unrelated - **-1.0** = Opposite meaning @@ -185,10 +187,12 @@ Notice: "sneakers for jogging" ranks high even though it shares no words with th ## Part 4: Vector Stores For real applications, you don't want to: + 1. Generate embeddings for your entire dataset on every search 2. Loop through all embeddings to find matches **Vector stores** (or vector databases) solve this by: + - Storing embeddings persistently - Using optimized algorithms for fast similarity search - Scaling to millions of documents @@ -218,6 +222,7 @@ public class Product ``` Key attributes: + - `[VectorStoreKey]` - Unique identifier - `[VectorStoreData]` - Searchable/filterable data - `[VectorStoreVector(dimensions, distanceFunction)]` - The embedding vector @@ -337,7 +342,7 @@ The code stays the same - just swap the vector store implementation! | Sample | Description | |--------|-------------| -| [RAGSimple-02MEAIVectorsMemory](../samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/) | In-memory vector store with Azure OpenAI | +| [RAGSimple-02MEAIVectorsMemory](../samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/) | Local sqlite-vec vector store with Azure OpenAI | | [RAGSimple-03MEAIVectorsAISearch](../samples/CoreSamples/RAGSimple-03MEAIVectorsAISearch/) | Azure AI Search vector store | | [RAGSimple-04MEAIVectorsQdrant](../samples/CoreSamples/RAGSimple-04MEAIVectorsQdrant/) | Qdrant vector store | diff --git a/03-AIPatternsAndApplications/02-retrieval-augmented-generation.md b/03-AIPatternsAndApplications/02-retrieval-augmented-generation.md index 4d77ee24..a1d84084 100644 --- a/03-AIPatternsAndApplications/02-retrieval-augmented-generation.md +++ b/03-AIPatternsAndApplications/02-retrieval-augmented-generation.md @@ -28,9 +28,11 @@ Language models are trained on data from a specific point in time. They don't kn RAG has two phases: ### Phase 1: Retrieval + Find relevant documents from your knowledge base using semantic search. ### Phase 2: Generation + Pass those documents to the AI along with the user's question. ![How RAG Works](./images/how-rag-works.png) @@ -366,7 +368,8 @@ IEmbeddingGenerator> embeddingGenerator = | Sample | Description | |--------|-------------| -| [RAGSimple-02MEAIVectorsMemory](../samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/) | RAG with in-memory vectors | +| [RAGSimple-02MEAIVectorsMemory](../samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/) | RAG with a local sqlite-vec store | +| [DataIngestion-01-Simple](../samples/CoreSamples/DataIngestion-01-Simple/) | Document ingestion pipeline (read → chunk → embed → store) with sqlite-vec | | [RAGSimple-03MEAIVectorsAISearch](../samples/CoreSamples/RAGSimple-03MEAIVectorsAISearch/) | RAG with Azure AI Search | | [RAGSimple-04MEAIVectorsQdrant](../samples/CoreSamples/RAGSimple-04MEAIVectorsQdrant/) | RAG with Qdrant | | [RAGSimple-15Ollama-DeepSeekR1](../samples/CoreSamples/RAGSimple-15Ollama-DeepSeekR1/) | RAG with DeepSeek reasoning | diff --git a/03-AIPatternsAndApplications/readme.md b/03-AIPatternsAndApplications/readme.md index 3dcacf5b..f376cf12 100644 --- a/03-AIPatternsAndApplications/readme.md +++ b/03-AIPatternsAndApplications/readme.md @@ -25,18 +25,23 @@ _Click the image to watch the video_ This lesson is divided into five parts: ### [Part 1: Embeddings and Semantic Search](./01-embeddings-semantic-search.md) + Understand how AI represents meaning as vectors and build search that finds by intent. ### [Part 2: Retrieval-Augmented Generation (RAG)](./02-retrieval-augmented-generation.md) + Ground AI responses in your own documents and data. ### [Part 3: Vision and Document Understanding](./03-vision-document-understanding.md) + Process images, PDFs, and visual content with multimodal AI. ### [Part 4: Combining Patterns](./04-combining-patterns.md) + Build sophisticated applications that combine multiple patterns. ### [Part 5: Local Model Runners](./05-LocalModelRunners.md) + Run AI models locally using AI Toolkit, Docker Model Runner, and Foundry Local. --- @@ -65,7 +70,7 @@ All code samples for this lesson are located in the **[`samples/CoreSamples/`](. | Category | Samples | Description | |----------|---------|-------------| -| **Embeddings & RAG** | [RAGSimple-02MEAIVectorsMemory](../samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/) | In-memory vector store | +| **Embeddings & RAG** | [RAGSimple-02MEAIVectorsMemory](../samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/) | Local sqlite-vec vector store | | | [RAGSimple-03MEAIVectorsAISearch](../samples/CoreSamples/RAGSimple-03MEAIVectorsAISearch/) | Azure AI Search | | | [RAGSimple-04MEAIVectorsQdrant](../samples/CoreSamples/RAGSimple-04MEAIVectorsQdrant/) | Qdrant vector store | | **Vision** | [Vision-01MEAI-AzureOpenAI](../samples/CoreSamples/Vision-01MEAI-AzureOpenAI/) | Vision with Azure OpenAI | diff --git a/04-AgentsWithMAF/02-agents-with-tools.md b/04-AgentsWithMAF/02-agents-with-tools.md index 54a3b0be..1a5ef6ce 100644 --- a/04-AgentsWithMAF/02-agents-with-tools.md +++ b/04-AgentsWithMAF/02-agents-with-tools.md @@ -374,6 +374,7 @@ static string GetOrderStatus(string orderId) { ... } |--------|-------------| | [MAF-BackgroundResponses-02-Tools](../samples/MAF/MAF-BackgroundResponses-02-Tools/) | Agent with function tools | | [MAF-BackgroundResponses-03-Complex](../samples/MAF/MAF-BackgroundResponses-03-Complex/) | Complex tool scenarios | +| [MAF-ImageGen-03-Foundry](../samples/MAF/MAF-ImageGen-03-Foundry/) | Agent with an image-generation tool (GPT-Image-2 on Microsoft Foundry) | --- diff --git a/04-AgentsWithMAF/03-multi-agent-workflows.md b/04-AgentsWithMAF/03-multi-agent-workflows.md index fc0f5e2c..273661cf 100644 --- a/04-AgentsWithMAF/03-multi-agent-workflows.md +++ b/04-AgentsWithMAF/03-multi-agent-workflows.md @@ -379,6 +379,7 @@ catch (AgentException ex) | [MAF02](../samples/MAF/MAF02/) | Simple sequential workflow | | [MAF-MultiAgents](../samples/MAF/MAF-MultiAgents/) | Multi-model orchestration | | [MAF-MultiAgents-Factory-01](../samples/MAF/MAF-MultiAgents-Factory-01/) | Advanced workflow patterns | +| [A2A-01](../samples/MAF/A2A-01/) | Agent-to-Agent (A2A) protocol: host a MAF agent and call it as a remote agent | --- diff --git a/04-AgentsWithMAF/04-model-context-protocol.md b/04-AgentsWithMAF/04-model-context-protocol.md index beb962e4..a2cf804a 100644 --- a/04-AgentsWithMAF/04-model-context-protocol.md +++ b/04-AgentsWithMAF/04-model-context-protocol.md @@ -414,6 +414,8 @@ Need to integrate with external service? |--------|-------------| | [MCP-01-HuggingFace](../samples/CoreSamples/MCP-01-HuggingFace/) | MCP with Azure OpenAI | | [MCP-02-HuggingFace-Ollama](../samples/CoreSamples/MCP-02-HuggingFace-Ollama/) | MCP with local Ollama models | +| [MCP-03-MicrosoftLearn](../samples/CoreSamples/MCP-03-MicrosoftLearn/) | Before/after demo grounding answers with the Microsoft Learn MCP server | +| [MAF-MCP-01](../samples/MAF/MAF-MCP-01/) | A MAF agent that owns Microsoft Learn MCP tools | | Aspire MCP Sample | See [04-PracticalSamples](../04-PracticalSamples/) for Aspire + MCP integration | --- diff --git a/samples/CoreSamples/BasicChat-05AIFoundryModels/README.md b/samples/CoreSamples/BasicChat-05AIFoundryModels/README.md new file mode 100644 index 00000000..90a409e1 --- /dev/null +++ b/samples/CoreSamples/BasicChat-05AIFoundryModels/README.md @@ -0,0 +1,66 @@ +# BasicChat-05AIFoundryModels — Foundry model swap + Integrated Security + +A Foundations / MEAI sample. One `IChatClient`, one +**Microsoft Foundry** endpoint, many models — swap the model by changing a single string, +and authenticate the way you should in Foundry: **Integrated Security (Microsoft Entra ID)**. + +## What it shows + +1. **Chat with a deployment name + endpoint + apikey** — the familiar key-based path. +2. **Foundry model swap** — change only `AzureOpenAI:Deployment` to switch + **`gpt-5.5` → `grok-4`** (same code, same endpoint, same `IChatClient`). +3. **Integrated Security (recommended)** — drop the key entirely and use + `AzureCliCredential` / Microsoft Entra ID. This is the way to work in Foundry. +4. **Simple one-shot prompt** — the sample asks a hardcoded question so the demo + stays focused on `IChatClient`, model swapping, and auth. + +> Then the demo switches to **`BasicChat-03Ollama`** to run the same `IChatClient` +> against a local model — zero changes to app logic. + +## Config (User Secrets) + +```bash +cd samples/CoreSamples/BasicChat-05AIFoundryModels + +# Foundry endpoint (one endpoint hosts many models) +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" + +# Model = deployment name. Swap this to change models (gpt-5.5 -> grok-4 -> ...) +dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5.5" + +# Auth mode: "integrated" (recommended) or "apikey" +dotnet user-secrets set "AzureOpenAI:AuthMode" "integrated" + +# Only needed when AuthMode=apikey +dotnet user-secrets set "AzureOpenAI:ApiKey" "" +``` + +## Run the demo (suggested order) + +```bash +# 1. Key auth with gpt-5.5 +dotnet user-secrets set "AzureOpenAI:AuthMode" "apikey" +dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5.5" +dotnet run app.cs + +# 2. Foundry model swap: gpt-5.5 -> grok-4 (same endpoint, same code) +dotnet user-secrets set "AzureOpenAI:Deployment" "grok-4" +dotnet run app.cs + +# 3. Integrated Security (the recommended way) — no key in config +dotnet user-secrets set "AzureOpenAI:AuthMode" "integrated" +az login +dotnet run app.cs +``` + +The console prints the active **model** and **auth** mode, runs one hardcoded question, +and labels the answer with `AI []:` so the model swap is visible at runtime. + +## Suggested prompt + +Use this question to make the model identity visible after the deployment swap: + +`what is your model name?` + +> Integrated Security requires the signed-in account to have the **Azure AI Developer** +> role on the Foundry resource. diff --git a/samples/CoreSamples/BasicChat-05AIFoundryModels/app.cs b/samples/CoreSamples/BasicChat-05AIFoundryModels/app.cs index 4a55cdb1..82155031 100644 --- a/samples/CoreSamples/BasicChat-05AIFoundryModels/app.cs +++ b/samples/CoreSamples/BasicChat-05AIFoundryModels/app.cs @@ -5,52 +5,49 @@ #:package Microsoft.Extensions.Configuration.UserSecrets@10.0.3 #:property UserSecretsId=genai-beginners-dotnet -using Azure.AI.OpenAI; +using Azure; +using Azure.AI.OpenAI; using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; -using System.Text; var config = new ConfigurationBuilder().AddUserSecrets().Build(); + +// One Microsoft Foundry endpoint can host MANY models behind it. var endpoint = config["AzureOpenAI:Endpoint"] ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets. See: https://github.com/microsoft/Generative-AI-for-beginners-dotnet/blob/main/01-IntroductionToGenerativeAI/setup-azure-openai.md"); -var deploymentName = config["AzureOpenAI:Deployment"] ?? "gpt-5-mini"; -IChatClient client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) +// Swap the model by changing ONLY the deployment name. +// In Microsoft Foundry you deploy several models behind the same endpoint, e.g.: +// gpt-5.5 -> grok-4.3 -> phi-4 -> ... (same code, same endpoint, just this string). +//var deploymentName = config["AzureOpenAI:Deployment"] ?? "gpt-5-mini"; +var deploymentName = "Kimi-K2.6"; + +// Auth mode. "integrated" = Microsoft Entra ID (recommended for Foundry: no keys to leak). +// "apikey" = key from the Foundry portal (endpoint + deployment + apikey). +var authMode = config["AzureOpenAI:AuthMode"] ?? "integrated"; + +AzureOpenAIClient azureClient = authMode.Equals("apikey", StringComparison.OrdinalIgnoreCase) + // Key auth: deployment name + endpoint + apikey. + ? new AzureOpenAIClient(new Uri(endpoint), + new AzureKeyCredential(config["AzureOpenAI:ApiKey"] + ?? throw new InvalidOperationException("Set AzureOpenAI:ApiKey in User Secrets when AuthMode=apikey."))) + // Integrated Security (recommended): Microsoft Entra ID via `az login`, no secrets. + : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()); + +IChatClient client = azureClient .GetChatClient(deploymentName) - .AsIChatClient() - .AsBuilder() - .Build(); - -var history = new List -{ - new(ChatRole.Assistant, "You are a useful chatbot. If you don't know an answer, say 'I don't know!'. Always reply in a funny way. Use emojis if possible.") -}; - -while (true) -{ - Console.Write("Q: "); - var userQ = Console.ReadLine(); - if (string.IsNullOrEmpty(userQ)) - { - break; - } - history.Add(new ChatMessage(ChatRole.User, userQ)); - - var sb = new StringBuilder(); - var result = client.GetStreamingResponseAsync(history); - Console.Write("AI: "); - await foreach (var item in result) - { - // validate if the item is null or has no contents - if (item == null || item.Contents.Count == 0) - { - continue; // skip to the next item if it's null or empty - } - sb.Append(item); - Console.Write(item.Contents[0].ToString()); - } - Console.WriteLine(); - - history.Add(new ChatMessage(ChatRole.Assistant, sb.ToString())); -} + .AsIChatClient(); + +Console.WriteLine($"Microsoft Foundry chat · model: {deploymentName} · auth: {authMode}"); +Console.WriteLine("(swap the model with AzureOpenAI:Deployment, e.g. gpt-5.5 -> grok-4)"); +Console.WriteLine("(switch auth with AzureOpenAI:AuthMode = apikey or integrated)"); +Console.WriteLine(); + +var question = "what is your model name?"; +Console.WriteLine($"Q: {question}"); + +var response = await client.GetResponseAsync(question); + +Console.WriteLine(); +Console.WriteLine($"AI [{deploymentName}]: {response.Text}"); diff --git a/samples/CoreSamples/CoreGenerativeAITechniques.sln b/samples/CoreSamples/CoreGenerativeAITechniques.sln index c4279146..3f75c9cf 100644 --- a/samples/CoreSamples/CoreGenerativeAITechniques.sln +++ b/samples/CoreSamples/CoreGenerativeAITechniques.sln @@ -65,6 +65,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCP-02-HuggingFace-Ollama", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoGeneration-AzureSora2-01", "VideoGeneration-AzureSora2-01\VideoGeneration-AzureSora2-01.csproj", "{8E3D5B50-B346-43C5-B71C-43C4427B39C9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCP-03-MicrosoftLearn", "MCP-03-MicrosoftLearn\MCP-03-MicrosoftLearn.csproj", "{06279A2D-4582-4854-ADDB-FE81DEEC5443}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataIngestion-01-Simple", "DataIngestion-01-Simple\DataIngestion-01-Simple.csproj", "{E5E21F6D-230A-4862-9B38-AAE41242B25C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -327,6 +331,30 @@ Global {8E3D5B50-B346-43C5-B71C-43C4427B39C9}.Release|x64.Build.0 = Release|Any CPU {8E3D5B50-B346-43C5-B71C-43C4427B39C9}.Release|x86.ActiveCfg = Release|Any CPU {8E3D5B50-B346-43C5-B71C-43C4427B39C9}.Release|x86.Build.0 = Release|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Debug|x64.ActiveCfg = Debug|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Debug|x64.Build.0 = Debug|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Debug|x86.ActiveCfg = Debug|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Debug|x86.Build.0 = Debug|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Release|Any CPU.Build.0 = Release|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Release|x64.ActiveCfg = Release|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Release|x64.Build.0 = Release|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Release|x86.ActiveCfg = Release|Any CPU + {06279A2D-4582-4854-ADDB-FE81DEEC5443}.Release|x86.Build.0 = Release|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Debug|x64.Build.0 = Debug|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Debug|x86.Build.0 = Debug|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Release|Any CPU.Build.0 = Release|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Release|x64.ActiveCfg = Release|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Release|x64.Build.0 = Release|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Release|x86.ActiveCfg = Release|Any CPU + {E5E21F6D-230A-4862-9B38-AAE41242B25C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/CoreSamples/DataIngestion-01-Simple/DataIngestion-01-Simple.csproj b/samples/CoreSamples/DataIngestion-01-Simple/DataIngestion-01-Simple.csproj new file mode 100644 index 00000000..6371f59a --- /dev/null +++ b/samples/CoreSamples/DataIngestion-01-Simple/DataIngestion-01-Simple.csproj @@ -0,0 +1,30 @@ + + + + Exe + net10.0 + DataIngestion_01_Simple + enable + enable + genai-beginners-dotnet + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/CoreSamples/DataIngestion-01-Simple/Program.cs b/samples/CoreSamples/DataIngestion-01-Simple/Program.cs new file mode 100644 index 00000000..0b3eae76 --- /dev/null +++ b/samples/CoreSamples/DataIngestion-01-Simple/Program.cs @@ -0,0 +1,164 @@ +// This sample shows the full RAG data-ingestion process using the official .NET +// building blocks: +// +// Microsoft.Extensions.DataIngestion -> read -> chunk -> enrich -> embed -> write +// Microsoft.Extensions.VectorData -> store the chunks + embeddings and search them +// +// The pipeline reads Markdown files from the ./data folder, splits them into semantic +// chunks, enriches each chunk with an AI-generated summary, generates embeddings, and +// writes everything into a local sqlite-vec vector store via the ElBruno VectorData +// connector (no Semantic Kernel). You can then ask questions and the app runs a semantic +// search over the ingested content. +// +// To run the sample (Azure OpenAI, keyless via Azure CLI), set these user secrets: +// dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +// dotnet user-secrets set "AzureOpenAI:ChatDeployment" "gpt-5-mini" +// dotnet user-secrets set "AzureOpenAI:EmbeddingDeployment" "text-embedding-3-small" +// Then sign in with: az login + +using Azure.AI.OpenAI; +using Azure.Identity; +using ElBruno.Connectors.SqliteVec; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DataIngestion; +using Microsoft.Extensions.DataIngestion.Chunkers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.VectorData; +using Microsoft.ML.Tokenizers; + +var config = new ConfigurationBuilder().AddUserSecrets().Build(); +var endpoint = config["AzureOpenAI:Endpoint"] + ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets."); +var chatModel = config["AzureOpenAI:ChatDeployment"] ?? "gpt-5-mini"; +var embeddingModel = config["AzureOpenAI:EmbeddingDeployment"] ?? "text-embedding-3-small"; + +using ILoggerFactory loggerFactory = + LoggerFactory.Create(builder => builder.AddSimpleConsole()); + +// Create the Azure OpenAI clients (keyless: uses your `az login` credentials). +var azureClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()); +IChatClient chatClient = azureClient.GetChatClient(chatModel).AsIChatClient(); +IEmbeddingGenerator> embeddingGenerator = + azureClient.GetEmbeddingClient(embeddingModel).AsIEmbeddingGenerator(); + +// --- READ ------------------------------------------------------------------ +// Read Markdown documents into a unified, LLM-friendly representation. +IngestionDocumentReader reader = new MarkdownReader(); + +// --- CHUNK ----------------------------------------------------------------- +// Split documents into semantic chunks so related content stays together. +IngestionChunkerOptions chunkerOptions = new(TiktokenTokenizer.CreateForModel(chatModel)) +{ + MaxTokensPerChunk = 2000, + OverlapTokens = 0 +}; +IngestionChunker chunker = new SemanticSimilarityChunker(embeddingGenerator, chunkerOptions); + +// --- ENRICH ---------------------------------------------------------------- +// Generate a short AI summary for each chunk to improve retrieval quality. +EnricherOptions enricherOptions = new(chatClient) { LoggerFactory = loggerFactory }; +IngestionChunkProcessor summaryEnricher = new SummaryEnricher(enricherOptions); + +// --- EMBED + WRITE --------------------------------------------------------- +// Store the chunks (and their embeddings) in a local sqlite-vec vector store using the +// official VectorData abstraction backed by the ElBruno connector. A small custom +// IngestionChunkWriter embeds each chunk with the Azure OpenAI generator and upserts it. +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "buildingblocks-vectors.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +var collection = new SqliteVecVectorStoreCollection( + "buildingblocks", + vectorStoreConnectionString, + embeddingGenerator); + +await collection.EnsureCollectionDeletedAsync(); +await collection.EnsureCollectionExistsAsync(); + +using IngestionChunkWriter writer = new SqliteVecChunkWriter(collection, embeddingGenerator); + +// --- PIPELINE -------------------------------------------------------------- +// Chain reader -> chunker -> (enrich) -> writer into one workflow. +using IngestionPipeline pipeline = + new(reader, chunker, writer, loggerFactory: loggerFactory) + { + ChunkProcessors = { summaryEnricher } + }; + +Console.WriteLine("Ingesting documents from ./data ...\n"); +await foreach (IngestionResult result in pipeline.ProcessAsync( + new DirectoryInfo("./data"), searchPattern: "*.md")) +{ + Console.WriteLine($"Processed '{result.DocumentId}' - Succeeded: {result.Succeeded}"); +} +Console.WriteLine(); + +// --- SEARCH ---------------------------------------------------------------- +// Query the same VectorData collection with natural-language questions. We embed the +// question ourselves and run the similarity search over the stored chunk vectors. +while (true) +{ + Console.Write("Enter your question (or 'exit' to quit): "); + string? query = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(query) || query.Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + Console.WriteLine("Searching...\n"); + var queryEmbedding = await embeddingGenerator.GenerateVectorAsync(query); + await foreach (VectorSearchResult result in + collection.SearchAsync(queryEmbedding, top: 3)) + { + Console.WriteLine($"Score: {result.Score:F3}"); + Console.WriteLine($" {result.Record.Content}\n"); + } +} + +// The chunk record stored in the vector store. text-embedding-3-small produces +// 1536-dimension vectors compared with cosine similarity. The key is a string because the +// sqlite-vec connector stores the key metadata column as TEXT. +internal sealed class ChunkRecord +{ + [VectorStoreKey] + public string Key { get; set; } = ""; + + [VectorStoreData] + public string Content { get; set; } = ""; + + [VectorStoreVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Embedding { get; set; } +} + +// Minimal IngestionChunkWriter that embeds each chunk and writes it to the sqlite-vec +// collection — the SK-free counterpart of the built-in VectorStoreWriter. +internal sealed class SqliteVecChunkWriter : IngestionChunkWriter +{ + private readonly SqliteVecVectorStoreCollection _collection; + private readonly IEmbeddingGenerator> _embeddingGenerator; + + public SqliteVecChunkWriter( + SqliteVecVectorStoreCollection collection, + IEmbeddingGenerator> embeddingGenerator) + { + _collection = collection; + _embeddingGenerator = embeddingGenerator; + } + + public override async Task WriteAsync( + IAsyncEnumerable> chunks, + CancellationToken cancellationToken) + { + await foreach (var chunk in chunks.WithCancellation(cancellationToken)) + { + var embedding = await _embeddingGenerator.GenerateVectorAsync( + chunk.Content, cancellationToken: cancellationToken); + + await _collection.UpsertAsync(new ChunkRecord + { + Key = Guid.NewGuid().ToString(), + Content = chunk.Content, + Embedding = embedding + }, cancellationToken); + } + } +} diff --git a/samples/CoreSamples/DataIngestion-01-Simple/README.md b/samples/CoreSamples/DataIngestion-01-Simple/README.md new file mode 100644 index 00000000..a32d31b8 --- /dev/null +++ b/samples/CoreSamples/DataIngestion-01-Simple/README.md @@ -0,0 +1,38 @@ +# DataIngestion-01 — RAG ingestion with DataIngestion + VectorData + +This sample demonstrates the full Retrieval-Augmented Generation (RAG) data pipeline +using the official .NET building blocks: + +- **`Microsoft.Extensions.DataIngestion`** — the ingestion pipeline: + **read → chunk → enrich → embed → write** +- **`Microsoft.Extensions.VectorData`** — the vector store abstraction used to + **store** the chunks/embeddings and **search** them + +## The pipeline + +| Stage | Building block | What it does | +| --- | --- | --- | +| **Read** | `MarkdownReader` | Reads Markdown files from `./data` into a unified document model | +| **Chunk** | `SemanticSimilarityChunker` | Splits documents into semantically coherent chunks | +| **Enrich** | `SummaryEnricher` | Adds an AI-generated summary to each chunk to improve retrieval | +| **Embed + Write** | custom `IngestionChunkWriter` + `SqliteVecVectorStoreCollection` | Generates embeddings and writes chunks into a local sqlite-vec vector store (ElBruno connector, no Semantic Kernel) | +| **Search** | `VectorStoreCollection.SearchAsync` | Runs semantic search over the ingested content | + +The reader, chunker, enricher and writer are composed into a single +`IngestionPipeline` that processes every Markdown file in `./data`. + +## Run it + +Set the Azure OpenAI endpoint and deployments (keyless auth via Azure CLI): + +```bash +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +dotnet user-secrets set "AzureOpenAI:ChatDeployment" "gpt-5-mini" +dotnet user-secrets set "AzureOpenAI:EmbeddingDeployment" "text-embedding-3-small" +az login +dotnet run +``` + +After ingestion, type a question and the app returns the most relevant chunks with +their similarity scores. The vector data is persisted to a local `buildingblocks-vectors.db` +sqlite-vec file next to the app binary. diff --git a/samples/CoreSamples/DataIngestion-01-Simple/data/dotnet-ai-building-blocks.md b/samples/CoreSamples/DataIngestion-01-Simple/data/dotnet-ai-building-blocks.md new file mode 100644 index 00000000..4497e1b4 --- /dev/null +++ b/samples/CoreSamples/DataIngestion-01-Simple/data/dotnet-ai-building-blocks.md @@ -0,0 +1,36 @@ +# The .NET AI building blocks + +Microsoft provides a set of `Microsoft.Extensions.*` building blocks that make it easy +to add intelligence to C# applications without leaving the .NET ecosystem you already +know. + +## Microsoft.Extensions.AI (MEAI) + +`Microsoft.Extensions.AI` is the foundation. It defines provider-agnostic abstractions +such as `IChatClient` (to chat with a model) and `IEmbeddingGenerator` (to create +embeddings). Because your application code depends on the abstraction instead of a +specific SDK, you can swap providers — for example Azure OpenAI in the cloud and Ollama +running locally — without changing your application logic. + +## Microsoft.Extensions.VectorData + +`Microsoft.Extensions.VectorData` provides a consistent abstraction over many vector +stores, including in-memory, SQLite, Qdrant, Azure AI Search, and Cosmos DB. You annotate +a model with attributes like `[VectorStoreKey]`, `[VectorStoreData]`, and +`[VectorStoreVector]`, and then use a `VectorStoreCollection` to upsert records and run +semantic similarity searches. The same code works across every supported store. + +## Microsoft.Extensions.DataIngestion + +`Microsoft.Extensions.DataIngestion` provides the building blocks for Retrieval-Augmented +Generation (RAG) ingestion pipelines. A pipeline reads documents, chunks them, optionally +enriches the chunks with AI (summaries, sentiment, keywords), generates embeddings, and +writes the result into a vector store. The `IngestionPipeline` type chains readers, +chunkers, processors, and writers together so you can build complete workflows with very +little code. + +## Microsoft Agent Framework (MAF) + +The Microsoft Agent Framework builds on top of these foundations to create agents and +multi-agent workflows. An agent combines a chat model, tools, and memory into a single +reusable unit, and workflows let multiple agents collaborate to solve more complex tasks. diff --git a/samples/CoreSamples/MCP-03-MicrosoftLearn/MCP-03-MicrosoftLearn.csproj b/samples/CoreSamples/MCP-03-MicrosoftLearn/MCP-03-MicrosoftLearn.csproj new file mode 100644 index 00000000..c12fdfa8 --- /dev/null +++ b/samples/CoreSamples/MCP-03-MicrosoftLearn/MCP-03-MicrosoftLearn.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + MCP_03_MicrosoftLearn + enable + enable + genai-beginners-dotnet + + + + + + + + + + + + + + diff --git a/samples/CoreSamples/MCP-03-MicrosoftLearn/Program.cs b/samples/CoreSamples/MCP-03-MicrosoftLearn/Program.cs new file mode 100644 index 00000000..f812a38c --- /dev/null +++ b/samples/CoreSamples/MCP-03-MicrosoftLearn/Program.cs @@ -0,0 +1,125 @@ +// This sample shows the value of grounding a model with the public Microsoft Learn +// MCP Server (https://learn.microsoft.com/api/mcp). It asks the SAME question twice: +// +// 1. WITHOUT MCP -> the model answers only from its (frozen) training knowledge, +// so for fast-moving topics the answer is often stale or vague. +// 2. WITH MCP -> the model calls the Microsoft Learn documentation tools +// (microsoft_docs_search, microsoft_docs_fetch, +// microsoft_code_sample_search) and answers from the live docs, +// including an up-to-date version and a docs link. +// +// The Microsoft Learn MCP Server is a public, keyless, streamable-HTTP MCP server, +// so no API key or Authorization header is required to connect to it. +// +// To run the sample, set the following user secrets (Azure OpenAI, keyless via Azure CLI): +// dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +// dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" +// Then sign in with: az login + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using ModelContextProtocol.Client; + +var config = new ConfigurationBuilder().AddUserSecrets().Build(); +var endpoint = config["AzureOpenAI:Endpoint"] + ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets."); +var deploymentName = config["AzureOpenAI:Deployment"] ?? "gpt-5-mini"; + +// The question is intentionally about a fast-moving topic so the difference between +// "model knowledge only" and "grounded in Microsoft Learn" is easy to see live. +const string question = + "What is the latest version of Microsoft Agent Framework for C#? " + + "Answer with the version number and a Microsoft Learn docs link."; + +// A short system instruction so the model commits to an answer (instead of asking +// clarifying questions) and, when tools are available, grounds itself in the docs. +const string systemPrompt = + "You are a .NET documentation assistant. Answer directly and concisely. " + + "Do not ask clarifying questions. 'Microsoft Agent Framework for C#' refers to the " + + "Microsoft.Agents.AI NuGet packages. If documentation tools are available, you MUST " + + "use them to find the current version and cite a Microsoft Learn link."; + +// Create an IChatClient and enable automatic function (tool) invocation. +IChatClient client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient() + .AsBuilder() + .UseFunctionInvocation() + .Build(); + +// ===================================================================== +// 1. BEFORE: ask WITHOUT MCP. The model can only use its training data. +// ===================================================================== +Console.WriteLine("============================================================"); +Console.WriteLine(" BEFORE - no MCP (answer from model knowledge only)"); +Console.WriteLine("============================================================"); +Console.WriteLine($"Question: {question}"); +Console.WriteLine(); + +var beforeMessages = new List +{ + new(ChatRole.System, systemPrompt), + new(ChatRole.User, question) +}; + +// Stream the answer token-by-token so the response appears live in the console. +await foreach (var update in client.GetStreamingResponseAsync( + beforeMessages, + new ChatOptions { ModelId = deploymentName })) +{ + Console.Write(update.Text); +} +Console.WriteLine(); + +// A clear visual delimiter so the two answers are easy to tell apart on screen. +Console.WriteLine(); +Console.WriteLine(new string('=', 60)); +Console.WriteLine(new string('=', 60)); +Console.WriteLine(); + +// ===================================================================== +// 2. AFTER: ask WITH the Microsoft Learn MCP Server tools attached. +// ===================================================================== + +// Connect to the public Microsoft Learn MCP Server (no auth needed). +var clientTransport = new HttpClientTransport(new HttpClientTransportOptions +{ + Name = "Microsoft Learn MCP", + Endpoint = new Uri("https://learn.microsoft.com/api/mcp") +}); +await using var mcpClient = await McpClient.CreateAsync(clientTransport); + +// Discover the tools the server exposes. +var tools = await mcpClient.ListToolsAsync(); + +Console.WriteLine("============================================================"); +Console.WriteLine(" AFTER - with Microsoft Learn MCP (grounded in live docs)"); +Console.WriteLine("============================================================"); +Console.WriteLine("Connected to the Microsoft Learn MCP Server. Available tools:"); +foreach (var tool in tools) +{ + Console.WriteLine($" - {tool.Name}: {tool.Description}"); +} +Console.WriteLine(); +Console.WriteLine($"Question: {question}"); +Console.WriteLine(); +Console.WriteLine("Asking the model (it will call the MCP tools as needed)..."); +Console.WriteLine(); + +var afterMessages = new List +{ + new(ChatRole.System, systemPrompt), + new(ChatRole.User, question) +}; + +// Stream the grounded answer. Tool calls happen automatically behind the scenes; the +// final text streams in live once the model has gathered what it needs from the docs. +await foreach (var update in client.GetStreamingResponseAsync( + afterMessages, + new ChatOptions { Tools = [.. tools], ModelId = deploymentName })) +{ + Console.Write(update.Text); +} +Console.WriteLine(); diff --git a/samples/CoreSamples/MCP-03-MicrosoftLearn/README.md b/samples/CoreSamples/MCP-03-MicrosoftLearn/README.md new file mode 100644 index 00000000..f7945f93 --- /dev/null +++ b/samples/CoreSamples/MCP-03-MicrosoftLearn/README.md @@ -0,0 +1,46 @@ +# MCP-03 — Microsoft Learn MCP Server (C# MCP SDK) + +This sample uses the **C# MCP SDK** (`ModelContextProtocol`) to connect to the public +**[Microsoft Learn MCP Server](https://learn.microsoft.com/training/support/mcp)** and lets +an Azure OpenAI model call its documentation tools through +**Microsoft.Extensions.AI** function invocation. + +The Microsoft Learn MCP Server is a public, **keyless**, streamable-HTTP MCP server at +`https://learn.microsoft.com/api/mcp`. It exposes tools such as: + +- `microsoft_docs_search` — search official Microsoft/Azure documentation +- `microsoft_docs_fetch` — fetch a full docs page as Markdown +- `microsoft_code_sample_search` — find official code samples + +## What it shows + +The sample asks the **same question twice** to make the value of MCP obvious: + +``` +What is the latest version of Microsoft Agent Framework for C#? +``` + +1. **BEFORE — no MCP:** the model answers only from its frozen training knowledge, so + for a fast-moving topic the answer is often stale or vague. +2. **AFTER — with the Microsoft Learn MCP Server:** the model calls the Learn tools and + answers from the live documentation, including an up-to-date version and a docs link. + +Under the hood: + +1. Create an `McpClient` over an HTTP transport to the Learn MCP endpoint. +2. Discover the available tools with `ListToolsAsync()`. +3. Pass those tools into `ChatOptions` and enable `UseFunctionInvocation()`. +4. The model decides when to call the Learn tools to ground its answer. + +## Run it + +Set the Azure OpenAI endpoint and deployment (keyless auth via Azure CLI): + +```bash +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" +az login +dotnet run +``` + +> No API key or token is needed for the Microsoft Learn MCP Server itself. diff --git a/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/Program.cs b/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/Program.cs index 77ec9cc9..5eb58bb4 100644 --- a/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/Program.cs +++ b/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/Program.cs @@ -1,52 +1,101 @@ -// This sample demonstrates RAG using Azure OpenAI for embeddings with simple in-memory cosine similarity search. -// To use Ollama instead, replace the Azure OpenAI code with: -// new OllamaEmbeddingGenerator(new Uri("http://localhost:11434/"), "all-minilm") -// Or see RAGSimple-01SK or RAGSimple-10SKOllama samples for complete Ollama examples. +// This sample demonstrates semantic search using the OFFICIAL .NET VectorData abstraction +// (VectorStoreCollection + VectorData attributes) with an ElBruno sqlite-vec backed store +// instead of Semantic Kernel connectors or a hand-rolled cosine-similarity loop. It is the +// *same* abstraction the chat app uses — swap the backing store with no changes to the +// search code. +// +// Keyless: uses your `az login` credentials (Microsoft Entra ID). Reuses the standard secrets: +// dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.services.ai.azure.com/" +// dotnet user-secrets set "AzureOpenAI:EmbeddingDeployment" "text-embedding-3-small" +// Then sign in with: az login using Azure.AI.OpenAI; +using Azure.Identity; +using ElBruno.Connectors.SqliteVec; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; -using System.ClientModel; -using System.Numerics.Tensors; +using Microsoft.Extensions.VectorData; -// get movie list and prepare in-memory storage -var movieData = MovieFactory.GetMovieVectorList(); - -// get embeddings generator and generate embeddings for movies var config = new ConfigurationBuilder().AddUserSecrets().Build(); -var endpoint = config["endpoint"]; -var apiKey = new ApiKeyCredential(config["apikey"]); -var embeddingModelName = config["embeddingModelName"] ?? "text-embedding-3-small"; +var endpoint = config["AzureOpenAI:Endpoint"] + ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets."); +var embeddingDeployment = config["AzureOpenAI:EmbeddingDeployment"] ?? "text-embedding-3-small"; +// Keyless Azure OpenAI embeddings (Microsoft Entra ID via `az login`) — no API key in config. IEmbeddingGenerator> generator = - new AzureOpenAIClient(new Uri(endpoint), apiKey) - .GetEmbeddingClient(embeddingModelName) + new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetEmbeddingClient(embeddingDeployment) .AsIEmbeddingGenerator(); -// generate embeddings for all movies and store them in memory -foreach (var movie in movieData) +// --- The official VectorData abstraction: a typed collection over a supported local store --- +var vectorStorePath = Path.Combine(AppContext.BaseDirectory, "movie-vectors.db"); +var vectorStoreConnectionString = $"Data Source={vectorStorePath}"; +var movies = new SqliteVecVectorStoreCollection( + "movies", + vectorStoreConnectionString, + generator); + +await movies.EnsureCollectionDeletedAsync(); +await movies.EnsureCollectionExistsAsync(); + +// Show the catalog we're searching over (titles only) so the search results have context. +var movieList = MovieFactory.GetMovieList(); +Console.WriteLine("Movies in the vector store:"); +foreach (var movie in movieList) { - movie.Vector = await generator.GenerateVectorAsync(movie.Description); + Console.WriteLine($" - {movie.Title}"); } +Console.WriteLine(); -// perform the search using cosine similarity -var query = "A family friendly movie that includes ogres and dragons"; -var queryEmbedding = await generator.GenerateVectorAsync(query); +// Embed each movie description and upsert it into the collection. +foreach (var movie in movieList) +{ + var embedding = await generator.GenerateVectorAsync(movie.Description!); + await movies.UpsertAsync(new MovieVectorRecord + { + Key = movie.Key, + Title = movie.Title!, + Description = movie.Description!, + Embedding = embedding + }); +} -var results = movieData - .Select(movie => (Movie: movie, Score: CosineSimilarity(queryEmbedding.Span, movie.Vector.Span))) - .OrderByDescending(x => x.Score) - .Take(2); +// Search the collection — embeddings + similarity handled by the VectorData building block. +// The same query path works for any natural-language question: embed it, then search. +string[] queries = +[ + "A family friendly movie that includes ogres and dragons", + "A movie about a hacker who discovers reality is a simulation" +]; -foreach (var (movie, score) in results) +foreach (var query in queries) { - Console.WriteLine($"Title: {movie.Title}"); - Console.WriteLine($"Description: {movie.Description}"); - Console.WriteLine($"Score: {score}"); - Console.WriteLine(); + var queryEmbedding = await generator.GenerateVectorAsync(query); + + Console.WriteLine($"Query: {query}\n"); + await foreach (var result in movies.SearchAsync(queryEmbedding, top: 2)) + { + Console.WriteLine($"Score: {result.Score:F3}"); + Console.WriteLine($" {result.Record.Title}"); + Console.WriteLine($" {result.Record.Description}\n"); + } + + Console.WriteLine(new string('-', 60) + "\n"); } -static float CosineSimilarity(ReadOnlySpan a, ReadOnlySpan b) +// The data model annotated with the VectorData attributes. text-embedding-3-small +// produces 1536-dimension vectors compared with cosine similarity. +internal sealed class MovieVectorRecord { - return TensorPrimitives.CosineSimilarity(a, b); + [VectorStoreKey] + public string Key { get; set; } = ""; + + [VectorStoreData] + public string Title { get; set; } = ""; + + [VectorStoreData] + public string Description { get; set; } = ""; + + [VectorStoreVector(1536, DistanceFunction = DistanceFunction.CosineSimilarity)] + public ReadOnlyMemory Embedding { get; set; } } \ No newline at end of file diff --git a/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/RAGSimple-02MEAIVectorsMemory.csproj b/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/RAGSimple-02MEAIVectorsMemory.csproj index 65ae7268..87a79799 100644 --- a/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/RAGSimple-02MEAIVectorsMemory.csproj +++ b/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/RAGSimple-02MEAIVectorsMemory.csproj @@ -9,8 +9,10 @@ - - + + + + diff --git a/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/README.md b/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/README.md index d0968290..1939e30f 100644 --- a/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/README.md +++ b/samples/CoreSamples/RAGSimple-02MEAIVectorsMemory/README.md @@ -1,21 +1,22 @@ # RAG Simple - MEAI Vectors Memory -This sample demonstrates Retrieval-Augmented Generation (RAG) using Microsoft.Extensions.AI with Azure OpenAI and an in-memory vector store. +This sample demonstrates semantic search / RAG building blocks using `Microsoft.Extensions.AI`, the official `Microsoft.Extensions.VectorData` abstractions, and a local sqlite-vec store backed by `ElBruno.Connectors.SqliteVec`. ## Prerequisites - .NET 10.0 SDK -- Azure OpenAI / Microsoft Foundry endpoint and API key +- Azure OpenAI / Microsoft Foundry endpoint +- Azure CLI signed in with `az login` ## Setup -Set your Azure OpenAI credentials using user secrets: +Set your Azure OpenAI endpoint using user secrets: ```bash cd samples/CoreSamples/RAGSimple-02MEAIVectorsMemory -dotnet user-secrets set "endpoint" "https://.services.ai.azure.com/" -dotnet user-secrets set "apikey" "" -dotnet user-secrets set "embeddingModelName" "text-embedding-3-small" +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.services.ai.azure.com/" +dotnet user-secrets set "AzureOpenAI:EmbeddingDeployment" "text-embedding-3-small" +az login ``` ## Running the Sample @@ -26,18 +27,22 @@ dotnet run ## What This Sample Does -1. Creates an in-memory vector store +1. Creates a local sqlite-vec backed vector collection 2. Loads a collection of movies with descriptions -3. Generates embeddings for each movie using Azure OpenAI's text-embedding-3-small model +3. Generates embeddings for each movie using Azure OpenAI's `text-embedding-3-small` model 4. Performs a vector search based on a query 5. Returns the most relevant movies ## Code Overview The sample uses: + - **Microsoft.Extensions.AI** for embeddings generation - **Azure.AI.OpenAI** for connecting to Azure OpenAI / Microsoft Foundry -- **Microsoft.Extensions.VectorData** for in-memory vector storage +- **Microsoft.Extensions.VectorData** for the official record/search abstraction +- **ElBruno.Connectors.SqliteVec** for the local sqlite-vec store implementation + +> Note: the sample name is kept for continuity in the course material, but the implementation no longer uses Semantic Kernel's in-memory connector. ## Alternative Providers diff --git a/samples/MAF/A2A-01/A2A-01.csproj b/samples/MAF/A2A-01/A2A-01.csproj new file mode 100644 index 00000000..d0a4bfd0 --- /dev/null +++ b/samples/MAF/A2A-01/A2A-01.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + A2A_01 + enable + enable + genai-beginners-dotnet + + + + + + + + + + + + + diff --git a/samples/MAF/A2A-01/Program.cs b/samples/MAF/A2A-01/Program.cs new file mode 100644 index 00000000..a3804932 --- /dev/null +++ b/samples/MAF/A2A-01/Program.cs @@ -0,0 +1,66 @@ +// A2A-01 — a minimal end-to-end Agent-to-Agent (A2A) sample in one console run. +// +// This single process does BOTH sides of the A2A protocol: +// 1. SERVER: hosts a "writer-agent" (Azure OpenAI) and exposes it over the +// Agent-to-Agent (A2A) HTTP+JSON protocol binding. +// 2. CLIENT: connects to that same endpoint with an A2AClient, wraps it as a +// standard AIAgent, and calls RunAsync — exactly how a remote app (even one +// written in another language/framework) would talk to the agent. +// +// This is the "HTTP of agent communication": the client never references the +// agent's implementation, only its A2A endpoint. +// +// To run the sample, set the following user secrets (Azure OpenAI, keyless via Azure CLI): +// dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +// dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" +// Then sign in with: az login + +using A2A; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +const string agentName = "writer-agent"; +const string baseUrl = "http://localhost:5099"; +const string a2aPath = "/a2a/writer-agent"; + +var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseUrls(baseUrl); +builder.Logging.ClearProviders(); // keep the console output focused on the demo + +var endpoint = builder.Configuration["AzureOpenAI:Endpoint"] + ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets."); +var deployment = builder.Configuration["AzureOpenAI:Deployment"] ?? "gpt-5-mini"; + +// 1. SERVER — build the agent and expose it over the A2A protocol. +IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deployment) + .AsIChatClient(); + +AIAgent writerAgent = chatClient.AsAIAgent( + name: agentName, + instructions: "You are a creative writer. Keep answers short and vivid."); + +var app = builder.Build(); +app.MapA2A(writerAgent, a2aPath); + +await app.StartAsync(); +Console.WriteLine($"A2A server is hosting '{agentName}' at {baseUrl}{a2aPath}"); +Console.WriteLine(); + +// 2. CLIENT — talk to the agent purely over the A2A protocol. +var a2aClient = new A2AClient(new Uri(baseUrl + a2aPath)); +AIAgent remoteAgent = a2aClient.AsAIAgent( + name: "remote-writer", + description: "The writer agent, reached over A2A."); + +const string prompt = "Write a two-line poem about .NET and AI."; +Console.WriteLine($"Client -> A2A: {prompt}"); +Console.WriteLine(); + +AgentResponse response = await remoteAgent.RunAsync(prompt); +Console.WriteLine("A2A -> Client:"); +Console.WriteLine(response.Text); + +await app.StopAsync(); diff --git a/samples/MAF/A2A-01/README.md b/samples/MAF/A2A-01/README.md new file mode 100644 index 00000000..11e17299 --- /dev/null +++ b/samples/MAF/A2A-01/README.md @@ -0,0 +1,40 @@ +# A2A-01 — Minimal Agent-to-Agent (A2A) in one console run + +This sample shows the **[Agent-to-Agent (A2A) protocol](https://a2a-protocol.org/latest/)** +end to end inside a single process. It is the natural way to *close* a talk on agents: +after building agents and giving them tools, A2A is how agents talk to **other agents** — +even ones written in a different language or framework. It's the "HTTP of agent communication." + +## What it shows + +A single console app plays **both** roles: + +1. **Server** — builds a `writer-agent` (Azure OpenAI) and exposes it over A2A with + `app.MapA2A(writerAgent, "/a2a/writer-agent")`. +2. **Client** — connects to that same endpoint with an `A2AClient`, wraps it as a standard + `AIAgent`, and calls `RunAsync(...)`. The client never references the agent's + implementation — only its A2A endpoint. + +```text +[ A2AClient ] --(A2A protocol)--> [ /a2a/writer-agent ] --> [ writer-agent (Azure OpenAI) ] +``` + +## Run it + +Set the Azure OpenAI endpoint and deployment (keyless auth via Azure CLI): + +```bash +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" +az login +dotnet run +``` + +> The A2A packages used here are **preview** (`Microsoft.Agents.AI.A2A`, +> `Microsoft.Agents.AI.Hosting.A2A.AspNetCore`). The sample uses a fixed local URL +> (`http://localhost:5099`) so the in-process client can reach the hosted agent. + +## Learn more + +- [Agent-to-Agent (A2A) with the Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/agents/providers/agent-to-agent) +- [Hosting agents over A2A](https://learn.microsoft.com/agent-framework/hosting/agent-to-agent) diff --git a/samples/MAF/MAF-Demos.slnx b/samples/MAF/MAF-Demos.slnx index 52cf411c..2d161531 100644 --- a/samples/MAF/MAF-Demos.slnx +++ b/samples/MAF/MAF-Demos.slnx @@ -14,6 +14,7 @@ + @@ -38,6 +39,12 @@ + + + + + + diff --git a/samples/MAF/MAF-ImageGen-03-Foundry/MAF-ImageGen-03-Foundry.csproj b/samples/MAF/MAF-ImageGen-03-Foundry/MAF-ImageGen-03-Foundry.csproj new file mode 100644 index 00000000..47479f8d --- /dev/null +++ b/samples/MAF/MAF-ImageGen-03-Foundry/MAF-ImageGen-03-Foundry.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + MAF_ImageGen_03_Foundry + enable + enable + genai-beginners-dotnet + + + + + + + + + + + + + diff --git a/samples/MAF/MAF-ImageGen-03-Foundry/Program.cs b/samples/MAF/MAF-ImageGen-03-Foundry/Program.cs new file mode 100644 index 00000000..b5d0ab80 --- /dev/null +++ b/samples/MAF/MAF-ImageGen-03-Foundry/Program.cs @@ -0,0 +1,139 @@ +// MAF-ImageGen-03-Foundry — a Microsoft Agent Framework agent that generates images with GPT-Image-2. +// +// A Microsoft Agent Framework (MAF) AIAgent is given an image +// tool that wraps ElBruno.Text2Image.Foundry's GptImage2Generator (MEAI IImageGenerator over +// Microsoft Foundry / GPT-Image-2). The agent decides when to call the tool, generates the +// image, and saves it as a PNG. +// +// Two building blocks, composed: +// - IChatClient (Microsoft.Extensions.AI) — the agent's brain, keyless via `az login`. +// - IImageGenerator (ElBruno.Text2Image.Foundry / GPT-Image-2) — the image tool. +// +// To run the sample, set the following user secrets: +// dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.services.ai.azure.com/" +// dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" # chat model (keyless) +// dotnet user-secrets set "AzureOpenAI:ApiKey" "" # GPT-Image-2 uses key auth +// Optional (defaults shown): +// dotnet user-secrets set "AzureOpenAI:ImageDeployment" "gpt-image-2" +// Then sign in for the keyless chat client: az login +// +// Usage: +// dotnet run # uses the built-in default prompt +// dotnet run -- "a corgi astronaut floating over Mars, comic style" + +using System.ComponentModel; +using Azure.AI.OpenAI; +using Azure.Identity; +using ElBruno.Text2Image.Foundry; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; + +var config = new ConfigurationBuilder().AddUserSecrets().Build(); + +var endpoint = config["AzureOpenAI:Endpoint"] + ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets."); +var chatDeployment = config["AzureOpenAI:Deployment"] ?? "gpt-5-mini"; +var imageApiKey = config["AzureOpenAI:ApiKey"] + ?? throw new InvalidOperationException("Set AzureOpenAI:ApiKey in User Secrets (GPT-Image-2 uses key auth)."); +var imageDeployment = config["AzureOpenAI:ImageDeployment"] ?? "gpt-image-2"; +var imageModelName = config["AzureOpenAI:ImageModelName"] ?? "GPT-Image-2"; + +// --- Image generation building block: IImageGenerator over Microsoft Foundry / GPT-Image-2 --- +// GptImage2Generator builds an AzureOpenAIClient that expects the BARE resource URL (it appends +// "/openai/deployments/{deployment}/images/generations" itself), so strip any "/openai" suffix. +using var httpClient = new HttpClient(); +ElBruno.Text2Image.IImageGenerator imageGenerator = new GptImage2Generator( + endpoint: NormalizeEndpoint(endpoint), + apiKey: imageApiKey, + httpClient: httpClient, + modelName: imageModelName, + deploymentName: imageDeployment, + timeoutSeconds: 300); + +var outputDir = Path.Combine(AppContext.BaseDirectory, "images"); +Directory.CreateDirectory(outputDir); + +// Tracks the most recent image saved by the tool, so we can open it after the agent finishes. +string? lastImagePath = null; + +// The tool the agent can call: generate an image from a prompt and save it as a PNG. +[Description("Generates an image from a detailed text prompt using GPT-Image-2 and saves it as a PNG. Returns the saved file path.")] +async Task GenerateImage( + [Description("A vivid, detailed description of the image to generate.")] string prompt) +{ + Console.WriteLine($"\n[tool] Generating image with GPT-Image-2...\n[tool] Prompt: {prompt}\n"); + var result = await imageGenerator.GenerateAsync(prompt, options: null, CancellationToken.None); + + var path = Path.Combine(outputDir, $"image-{DateTime.Now:yyyyMMdd-HHmmss}.png"); + await File.WriteAllBytesAsync(path, result.ImageBytes); + lastImagePath = path; + + Console.WriteLine($"[tool] Image generated in {result.InferenceTimeMs}ms and saved to:\n[tool] {path}\n"); + return path; +} + +// --- The agent: a chat model (keyless) composed with the image tool, via MAF --- +IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(chatDeployment) + .AsIChatClient(); + +AIAgent imageAgent = chatClient.AsAIAgent( + name: "ImageStudio", + instructions: + "You are a creative studio agent. When the user asks for an image, call the GenerateImage " + + "tool with a vivid, detailed prompt, then tell the user the file path where the image was " + + "saved. Do not ask clarifying questions; infer a good prompt and generate the image.", + tools: [AIFunctionFactory.Create(GenerateImage)]); + +// The request: a CLI argument, or the default built-in prompt. +var request = args.Length > 0 + ? string.Join(' ', args) + : "Create an incident-response hero image: a calm on-call engineer reviewing a runbook next to a " + + "rising error-rate line chart crossing a 5% threshold. Flat vector style, blue and teal palette, " + + "professional, high contrast, no text artifacts."; + +Console.WriteLine($"User: {request}\n"); +Console.WriteLine("Agent is thinking (it will call the GPT-Image-2 tool as needed)..."); + +AgentResponse response = await imageAgent.RunAsync(request); +Console.WriteLine($"\nAgent: {response.Text}"); + +// Open the generated image in the OS default viewer. +if (lastImagePath is not null && File.Exists(lastImagePath)) +{ + Console.WriteLine($"\nOpening image: {lastImagePath}"); + OpenFile(lastImagePath); +} + +// Strip any "/openai..." suffix so GptImage2Generator gets the bare Foundry resource URL. +static string NormalizeEndpoint(string endpoint) +{ + var trimmed = endpoint.Trim().TrimEnd('/'); + var openAiIndex = trimmed.IndexOf("/openai", StringComparison.OrdinalIgnoreCase); + return openAiIndex >= 0 ? trimmed[..openAiIndex] : trimmed; +} + +// Open a file using the OS default application (cross-platform). +static void OpenFile(string path) +{ + try + { + if (OperatingSystem.IsWindows()) + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); + } + else if (OperatingSystem.IsMacOS()) + { + System.Diagnostics.Process.Start("open", path); + } + else if (OperatingSystem.IsLinux()) + { + System.Diagnostics.Process.Start("xdg-open", path); + } + } + catch (Exception ex) + { + Console.WriteLine($"Could not open the image automatically: {ex.Message}"); + } +} diff --git a/samples/MAF/MAF-ImageGen-03-Foundry/README.md b/samples/MAF/MAF-ImageGen-03-Foundry/README.md new file mode 100644 index 00000000..dafeb8ff --- /dev/null +++ b/samples/MAF/MAF-ImageGen-03-Foundry/README.md @@ -0,0 +1,64 @@ +# MAF-ImageGen-03-Foundry + +A **Microsoft Agent Framework (MAF)** agent that generates images with **GPT-Image-2** on +**Microsoft Foundry**. + +A MAF `AIAgent` is given an **image tool** that wraps +[`ElBruno.Text2Image.Foundry`](https://www.nuget.org/packages/ElBruno.Text2Image.Foundry)'s +`GptImage2Generator`. The agent decides when to call the tool, generates the image, and +saves it as a PNG. + +## Building blocks + +| Block | Type | Auth | +|-------|------|------| +| The agent's brain | `IChatClient` (Microsoft.Extensions.AI) | keyless (`az login`) | +| The image tool | `GptImage2Generator` (ElBruno.Text2Image.Foundry / GPT-Image-2) | API key | + +The chat model runs **keyless** (Microsoft Entra ID via `az login`). GPT-Image-2 image +generation uses **key auth**, so an API key is required. + +## Secrets + +```bash +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.services.ai.azure.com/" +dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" # chat model (keyless) +dotnet user-secrets set "AzureOpenAI:ApiKey" "" # GPT-Image-2 (key auth) +``` + +Optional (defaults shown): + +```bash +dotnet user-secrets set "AzureOpenAI:ImageDeployment" "gpt-image-2" +dotnet user-secrets set "AzureOpenAI:ImageModelName" "GPT-Image-2" +``` + +Then sign in for the keyless chat client: + +```bash +az login +``` + +> The endpoint can be the bare Foundry resource URL or include an `/openai...` suffix — +> the sample normalizes it to the bare URL that `GptImage2Generator` expects. + +## Run + +```bash +# Uses the built-in default prompt +dotnet run + +# Or pass your own prompt +dotnet run -- "a corgi astronaut floating over Mars, comic style" +``` + +Generated PNGs are saved to the `images/` folder next to the built executable. The agent +prints the saved file path when it finishes. + +## How it works + +1. A `GptImage2Generator` is created against your Foundry endpoint + `gpt-image-2` deployment. +2. A local function `GenerateImage(prompt)` is wrapped as an `AIFunction` and handed to the + agent via `chatClient.AsAIAgent(..., tools: [AIFunctionFactory.Create(GenerateImage)])`. +3. `agent.RunAsync(request)` lets the model infer a vivid prompt and call the tool, which + generates the image and writes the PNG. diff --git a/samples/MAF/MAF-MCP-01/MAF-MCP-01.csproj b/samples/MAF/MAF-MCP-01/MAF-MCP-01.csproj new file mode 100644 index 00000000..55836fb2 --- /dev/null +++ b/samples/MAF/MAF-MCP-01/MAF-MCP-01.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + MAF_MCP_01 + enable + enable + genai-beginners-dotnet + + + + + + + + + + + + + + diff --git a/samples/MAF/MAF-MCP-01/Program.cs b/samples/MAF/MAF-MCP-01/Program.cs new file mode 100644 index 00000000..a3dd9956 --- /dev/null +++ b/samples/MAF/MAF-MCP-01/Program.cs @@ -0,0 +1,75 @@ +// MAF-MCP-01 — a Microsoft Agent Framework agent that uses MCP tools. +// +// This sample wires a Microsoft Agent Framework (MAF) AIAgent to the public +// Microsoft Learn MCP Server (https://learn.microsoft.com/api/mcp). The agent +// receives the Learn documentation tools (microsoft_docs_search, +// microsoft_docs_fetch, microsoft_code_sample_search) and calls them on its own +// to ground its answers in the live Microsoft Learn docs. +// +// The Microsoft Learn MCP Server is public and keyless, so no API key or token is +// required to connect to it. +// +// To run the sample, set the following user secrets (Azure OpenAI, keyless via Azure CLI): +// dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +// dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" +// Then sign in with: az login + +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using ModelContextProtocol.Client; + +var config = new ConfigurationBuilder().AddUserSecrets().Build(); +var endpoint = config["AzureOpenAI:Endpoint"] + ?? throw new InvalidOperationException("Set AzureOpenAI:Endpoint in User Secrets."); +var deploymentName = config["AzureOpenAI:Deployment"] ?? "gpt-5-mini"; + +// 1. Connect to the public Microsoft Learn MCP Server (no auth needed). +var clientTransport = new HttpClientTransport(new HttpClientTransportOptions +{ + Name = "Microsoft Learn MCP", + Endpoint = new Uri("https://learn.microsoft.com/api/mcp") +}); +await using var mcpClient = await McpClient.CreateAsync(clientTransport); + +// 2. Discover the tools the server exposes. +var tools = await mcpClient.ListToolsAsync(); +Console.WriteLine("Connected to the Microsoft Learn MCP Server. Available tools:"); +foreach (var tool in tools) +{ + Console.WriteLine($" - {tool.Name}: {tool.Description}"); +} +Console.WriteLine(); + +// 3. Create the chat client and build a MAF agent that owns the MCP tools. +// The agent invokes the tools automatically while answering. +IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); + +AIAgent docsAgent = chatClient.AsAIAgent( + name: "LearnDocsAgent", + instructions: "You are a .NET documentation assistant. " + + "Use the Microsoft Learn tools to answer questions about .NET and AI. " + + "Always ground your answer in the docs and include a Microsoft Learn link.", + tools: [.. tools]); + +// 4. Ask the agent a question — it decides when to call the Learn MCP tools. +const string question = + "What is the latest version of Microsoft Agent Framework for C#? " + + "Answer with the version number and a Microsoft Learn docs link."; + +Console.WriteLine($"Question: {question}"); +Console.WriteLine(); +Console.WriteLine("Agent is thinking (it will call the MCP tools as needed)..."); +Console.WriteLine(); + +// Stream the grounded answer so it appears token-by-token in the console. Tool calls +// happen automatically behind the scenes; the final text streams in live. +await foreach (var update in docsAgent.RunStreamingAsync(question)) +{ + Console.Write(update.Text); +} +Console.WriteLine(); diff --git a/samples/MAF/MAF-MCP-01/README.md b/samples/MAF/MAF-MCP-01/README.md new file mode 100644 index 00000000..1e89e91e --- /dev/null +++ b/samples/MAF/MAF-MCP-01/README.md @@ -0,0 +1,40 @@ +# MAF-MCP-01 — A MAF agent that uses MCP tools + +This sample shows a **Microsoft Agent Framework (MAF)** `AIAgent` that uses **MCP tools** +from the public **[Microsoft Learn MCP Server](https://learn.microsoft.com/training/support/mcp)**. + +The Microsoft Learn MCP Server is a public, **keyless**, streamable-HTTP MCP server at +`https://learn.microsoft.com/api/mcp`. It exposes documentation tools such as: + +- `microsoft_docs_search` — search official Microsoft/Azure documentation +- `microsoft_docs_fetch` — fetch a full docs page as Markdown +- `microsoft_code_sample_search` — find official code samples + +## What it shows + +1. Connect to the Learn MCP Server with `McpClient` over an HTTP transport. +2. Discover the tools with `ListToolsAsync()`. +3. Hand those tools to a MAF agent: `chatClient.AsAIAgent(name, instructions, tools: [.. tools])`. +4. Call `agent.RunAsync(...)` — the agent decides when to invoke the MCP tools and + grounds its answer in the live Microsoft Learn docs (with a docs link). + +Unlike a raw chat + tools call, the **agent owns the tools** and runs the tool loop itself, +which is the building block for tool-using and multi-agent scenarios. + +## Run it + +Set the Azure OpenAI endpoint and deployment (keyless auth via Azure CLI): + +```bash +dotnet user-secrets set "AzureOpenAI:Endpoint" "https://.openai.azure.com/" +dotnet user-secrets set "AzureOpenAI:Deployment" "gpt-5-mini" +az login +dotnet run +``` + +> No API key or token is needed for the Microsoft Learn MCP Server itself. + +## Learn more + +- [Microsoft Agent Framework](https://learn.microsoft.com/agent-framework/) +- [Model Context Protocol in .NET](https://learn.microsoft.com/dotnet/ai/get-started-mcp) diff --git a/samples/MAF/MAF01/Program.cs b/samples/MAF/MAF01/Program.cs index cfbad95a..783b1a96 100644 --- a/samples/MAF/MAF01/Program.cs +++ b/samples/MAF/MAF01/Program.cs @@ -18,6 +18,11 @@ name: "Writer", instructions: "Write stories that are engaging and creative."); -AgentResponse response = await writer.RunAsync("Write a short story about a haunted house with a character named Lucia."); - -Console.WriteLine(response.Text); \ No newline at end of file +// Stream the response so the story appears token-by-token in the console — a livelier +// demo experience than waiting for the full response. +await foreach (var update in writer.RunStreamingAsync( + "Write a short story about a haunted house with a character named Lucia.")) +{ + Console.Write(update.Text); +} +Console.WriteLine(); \ No newline at end of file diff --git a/samples/MAF/MAF02/Program.cs b/samples/MAF/MAF02/Program.cs index 4ced8ea2..707c6afc 100644 --- a/samples/MAF/MAF02/Program.cs +++ b/samples/MAF/MAF02/Program.cs @@ -31,7 +31,11 @@ AIAgent workflowAgent = workflow.AsAIAgent(); -AgentResponse workflowResponse = - await workflowAgent.RunAsync("Write a short story about a haunted house."); - -Console.WriteLine(workflowResponse.Text); \ No newline at end of file +// Stream the workflow response so the edited story appears token-by-token in the +// console — a livelier demo experience than waiting for the full response. +await foreach (var update in workflowAgent.RunStreamingAsync( + "Write a short story about a haunted house.")) +{ + Console.Write(update.Text); +} +Console.WriteLine(); \ No newline at end of file