Building an AI Agent With Semantic Kernel, C#, OpenAI, and MongoDB Atlas
Avalie esse Tutorial
Gen AI continues to take off and AI agents are no different. More and more developers are being asked to develop solutions that integrate AI agents into the user experience. This helps businesses reduce costs by attempting to solve many user questions or requests without the need for human interaction.
With winter on its way, there is no better time to think about comforting food on those cold nights. For this reason, in this tutorial, we are going to create an AI agent using Microsoft’s Semantic Kernel for C#, OpenAI, and MongoDB Atlas to help you decide whether you have the ingredients to cook, or whether you should just head to a cozy restaurant that is recommended and let someone else do the cooking!
Nosso agente será composto por algumas partes:
- A plugin for retrieving a list of available ingredients from a text file
- A prompt that uses AI to return what the cuisine of the given meal is
- Another plugin for searching a collection in our MongoDB Atlas cluster
Para explicar brevemente,plugin um plugin -in é um código personalizado marcado como disponível para AI a IA e que pode ser chamado para obter alguma funcionalidade a partir do código, como API chamar uma API ou interagir com outros serviços.
Um prompt também é personalizado, mas cria texto, geralmente com entradas dinâmicas, que é então usado como entrada para AI que a IA execute a tarefa com base na entrada.
If you would like to learn more about integrating retrieval-augmented generation (RAG) with Semantic Kernel, you can learn to build a movie recommendation bot that uses MongoDB Atlas to store the data and vector embeddings and uses Atlas Vector Search under the hood via a Semantic Kernel connector to search the data.
Mas antes de nos distraírmos pela faminta, vamos começar, para que tenhamos nosso agente pronto a tempo do restaurante!
Você precisará de algumas coisas para acompanhar este tutorial:
- Atlas M0 cluster deployed with the full sample dataset loaded
- OpenAI account and API key
- .NET 9 SDK
- The starter code that can be found on the “start” branch on GitHub
To save time with some of the code and files, the starter repo comes with some things already available out of the box.
- Inside of
Data/cupboardinventory.txt
is a list of ingredients that you might find in a cupboard or fridge. You can always make changes to this if you wish to add or remove ingredients, and these can be case-insensitive. We will use this to simulate what ingredients are available. Restaurants.cs
within Models has properties we care about for a restaurant. As part of the MongoDB connector available for Semantic Kernel (which is already added to the project), we have access to the MongoDB C# Driver under the hood. This means we can take advantage of being able to represent our documents as simple classes.- Program.cs has a method already implemented inside it called
GenerateEmbeddingsForCuisine()
. This is because we want to generate embeddings for the cuisine field for documents available from the sample dataset so they are available to the application later on. We don’t need to create embeddings for every document, though. We just need a good sample size so it is set to fetch 1000 documents.
If you want to understand more about how this method works, the section on adding documents to the memory store in the Semantic Kernel and RAG article goes into it in more detail.
O código inicial que estamos usando é um .NET aplicação de console .NET tradicional usando .NET 9.NET. Embora possamos Go ir em frente e adicionarappsettings arquivos “” e configurá-los em nossa
Program.cs
classe , isso é excessivo para uma demonstração simples. Então, vamos aproveitar as variáveis de ambiente.Antes de começarmos a usar nossas variáveis de ambiente no código, vamos salvá-las. Execute cada um dos seguintes comandos (dependendo do seu sistema operacional (sistema operacional)), um a um, na linha de comando de sua escolha:
1 export OPENAI_API_KEY=”<REPLACE WITH YOUR OPEN AI API KEY>” # MacOS/Linux 2 set OPENAI_API_KEY=”<REPLACE WITH YOUR OPEN AI API KEY>” # Windows” 3 4 export OPENAI_MODEL_NAME=”<REPLACE WITH YOUR MODEL OF CHOICE>” 5 set OPENAI_MODEL_NAME_”<REPLACE WITH YOUR MODEL OF CHOICE>” 6 7 export MONGODB_ATLAS_CONNECTION_STRING=”<YOUR MONGODB ATLAS CONNECTION STRING>” 8 set MONGODB_ATLAS_CONNECTION_STRING=”<YOUR MONGODB ATLAS CONNECTION STRING>”
Now, we can add the following code within
Program.cs
, below the using statements but before the method definition, to fetch these environment variables:1 string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new ArgumentNullException("Environment.GetEnvironmentVariable(\"OPENAI_API_KEY\")"); 2 string modelName = Environment.GetEnvironmentVariable("OPENAI_MODEL_NAME") ?? "gpt-4o-mini";
Isto verifica a presença desses valores e lança uma exceção ou define um padrão. Gpt-4o-mini é um modelo perfeitamente aceitável para o nosso caso de uso.
We also want to call the existing method to generate the embeddings for our sample data. Before we can do that, however, we need to update it to use the API key we now have available as a variable.
Inside the method, update the call to use Open AI for text embedding generation.
1 memoryBuilder.WithOpenAITextEmbeddingGeneration( 2 "text-embedding-3-small", 3 "<YOUR OPENAI APIKEY>" 4 );
Replace the string for your OpenAI API key with the variable
apiKey
.Now, we can add a call to the method earlier in the file, after we set up the variables from environment variables:
1 await GenerateEmbeddingsForCuisine();
Pode levar alguns minutos para gerar as incorporações, então agora é um bom momento para executar o aplicação pela primeira vez:
1 dotnet run
You can always continue on with the tutorial while you wait, or make a coffee. Once the new
embedded_cuisines
collection has between 900 and 1000 documents (or around the number you selected if you chose to change the limit in the method), you can stop the application and delete or comment out the call to the method.Agora que temos nossos dados de amostra com cozinhas incorporadas disponíveis, é hora de começar a configurar o Semantic Kernel para que possamos começar a disponibilizar as ferramentas para atingir nosso objetivo relacionado à alimentação ao AI agente de IA nas seções posteriores.
1 var builder = Kernel.CreateBuilder(); 2 3 builder.Services.AddOpenAIChatCompletion( 4 modelName, 5 apiKey); 6 7 var kernel = builder.Build();
Now, the kernel instance is configured and ready to go. We can start to build the first tool for our AI to opt to use: the ingredients plugin.
As mentioned earlier in this tutorial, we have a list of ingredients available in a
.txt
file inside the data folder that we can use to simulate fetching the ingredients from an API (if you have a techy smart fridge, for example).Portanto, o primeiro plugin que vamos escrever é um que obtém todos os componentes desse arquivo. O agente pode então usar isso para obter todos os componentes disponíveis antes de decidir se estão faltando algum componente necessário para preparar a comida escolhida.
- Na raiz do projeto, adicione uma nova pasta chamada
Plugins
. - Crie uma nova classe dentro dessa pasta chamada
IngredientsPlugin
. - Cole o seguinte código dentro da classe:
1 [ ]2 public static string GetIngredientsFromCupboard() 3 { 4 // Ensures that the file path functions across all operating systems. 5 string baseDir = AppContext.BaseDirectory; 6 string projectRoot = Path.Combine(baseDir, "..", "..", ".."); 7 string filePath = Path.Combine(projectRoot, "Data", "cupboardinventory.txt"); 8 return File.ReadAllText(filePath).ToLower(); 9 }
Observação: se o seu editor de texto não adicionar automaticamente as declarações de uso, adicione o seguinte no topo do arquivo:
1 using System.ComponentModel; 2 using Microsoft.SemanticKernel;
Here we have a simple method defined called GetIngredientsFromCupboard.
It is annotated with this KernelFunction definition with a Description property. This tells the AI that this method is available and also what it is for. This is used to help it decide when and if to call this method to achieve a task.
O código dentro do método é um código C# bastante comum para ler um arquivo. O resultado de
Directory.GetCurrentDirectory()
é diferente dependendo do sistema operacional e de onde o aplicação está sendo executado. Portanto, a maior parte desse código é apenas para obter o caminho do arquivo de forma independente do sistema operacional.O que considero inteligentes é que o método retorna o conteúdo do arquivo (em letras minúsculas para consistência) e é assim que a IA AI tem acesso a ele, combinando C# código C# básico com o conhecimento de que a função existe!
Agora precisamos disponibilizar o plugin -in para usarmos mais tarde, quando criarmos um prompt para o que queremos alcançar.
Portanto, após a última chamada para
var kernel = builder.Build();
, adicione o seguinte para importar o plugin-in:1 kernel.ImportPluginFromType<IngredientsPlugin>();
Agora é hora de fazer o prompt GetCuisine. Precisamos de uma maneira de fazer AI com que a IA nos informe qual é a preparação da comida, então é aqui que entra a criação de uma solicitação.
There are two ways of creating a prompt: via two files (a json config file and a prompt txt file) or with a YAML file. I find YAML easy to get wrong with its indenting approach. So we are going to use the former approach.
- Crie uma
Prompts
pasta chamada na raiz do projeto. - Crie uma pasta dentro dessa pasta chamada
GetCuisine
. - Crie um novo arquivo chamado
config.json
e insira o seguinte JSON:
1 { 2 "schema": 1, 3 "type": "completion", 4 "description": "Identify the cuisine of the user's requested meal", 5 "execution_settings": { 6 "default": { 7 "max_tokens": 800, 8 "temperature": 0 9 } 10 }, 11 "input_variables": [ 12 { 13 "name": "cuisine", 14 "description": "Text from the user that contains their requested meal", 15 "required": true 16 } 17 ] 18 }
This specifies the configuration for our prompt and specifies what it does—that is for chat completion, it should have 0 creativity (
temperature: 0
), aka be specific, and there will be an input variable available called cuisine which will contain the requested meal. Because input_variables
is an array, you can specify multiple input variables if needed here as well.Crie outro arquivo na pasta chamado,
skprompt.txt
que é o que criará dinamicamente o texto para que a IA AI entenda o que está sendo solicitado dela. Em seguida, adicione o seguinte:1 Return a single word that represents the cuisine of the requested meal that is sent: {{$cuisine}}. 2 3 For example, if the meal is mac and cheese, return American. Or for pizza it would be Italian. 4 Do not return any extra words, just return the single name of the cuisine.
This is an example of generating a prompt statement and making use of good prompt engineering to shape how well the AI understands and responds. The
{{$cuisine}}
is how you dynamically populate the prompt with values. This has to start with a $ sign, be inside the double curly brackets, and match the name of an input variable declared in the array inside the config.json
file.A maneira de disponibilizar prompts para a IA AI como um plugin plugin -in é ligeiramente diferente em comparação com os plug-ins.
Após a chamada para importar o ResultsPlugin, adicione o seguinte:
1 string baseDir = AppContext.BaseDirectory; 2 string projectRoot = Path.Combine(baseDir, "..", "..", ".."); 3 var plugins = kernel.CreatePluginFromPromptDirectory(projectRoot + "/Prompts");
Semantic Kernel is clever and can find all prompts available within the prompts directory. It will then be available later in an array of plugin names (named after the folder, in our case GetCuisine).
Por último, queremos criar outro plugin, desta vez para restaurantes e interagir com nosso cluster MongoDB Atlas .
- Crie uma classe dentro da pasta
Plugins
chamadaRestaurantsPlugin
. - Adicione as seguintes declarações de uso e a declaração de namespace :
1 using FoodAgentDotNet.Models; 2 using Microsoft.SemanticKernel; 3 using Microsoft.SemanticKernel.Connectors.MongoDB; 4 using Microsoft.SemanticKernel.Connectors.OpenAI; 5 using Microsoft.SemanticKernel.Memory; 6 using MongoDB.Driver; 7 using System; 8 using System.Collections.Generic; 9 using System.ComponentModel; 10 using System.Linq; 11 using System.Text; 12 using System.Threading.Tasks; 13 14 namespace FoodAgentDotNet.Plugins;
Em seguida, substitua o restante da classe pelo seguinte:
1 2 public class RestaurantsPlugin 3 { 4 static readonly string mongoDBConnectionString = Environment.GetEnvironmentVariable("MONGODB_ATLAS_CONNECTION_STRING"); 5 static readonly string openAIApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"); 6 7 [ ]8 public static async Task<List<Restaurant>> GetRecommendedRestaurant( 9 [string cuisine) ] 10 { 11 var mongoDBMemoryStore = new MongoDBMemoryStore(mongoDBConnectionString, "sample_restaurants", "restaurants_index"); 12 var memoryBuilder = new MemoryBuilder(); 13 memoryBuilder.WithOpenAITextEmbeddingGeneration( 14 "text-embedding-3-small", 15 openAIApiKey ); 16 memoryBuilder.WithMemoryStore(mongoDBMemoryStore); 17 var memory = memoryBuilder.Build(); 18 19 var restaurants = memory.SearchAsync( 20 "embedded_cuisines", 21 cuisine, 22 limit: 5, 23 minRelevanceScore: 0.5 24 ); 25 26 List<Restaurant> recommendedRestaurants = new(); 27 28 await foreach(var restaurant in restaurants) 29 { 30 recommendedRestaurants.Add(new Restaurant 31 { 32 Name = restaurant.Metadata.Description, 33 // We include the cuisine so that the AI has this information available to it 34 Cuisine = restaurant.Metadata.AdditionalMetadata, 35 }); 36 } 37 return recommendedRestaurants; 38 } 39 }
Há muito aqui, então vamos dar uma olhada.
Some of these features are still considered experimental so warnings are disabled.
Just like with the ingredients plugin from earlier, we add the attribute to the method to mark it as a KernelFunction. However, this time, we also pass in an argument for the cuisine so we add an additional description attribute to describe to the AI what the argument is there for.
Next, we build up the memory store and configure MongoDB as our memory store. We also set up the OpenAI text embedding again as it will need to generate embeddings for the cuisine passed in to use in the vector search.
We then bring those pieces together to explicitly search our
embedded_cuisines
collection for up to five restaurants that might suit the requested cuisine.We build up a list of recommended restaurants, assigning the values we care about before returning that list so the AI has it available.
Agora precisamos retornar a
Program.cs
brevemente para adicionar nosso novo plugin-in. Após a chamada anterior para adicionar o ClusterPlugin, adicione o seguinte para adicionar também o RestaurantsPlugin:1 kernel.ImportPluginFromType<RestaurantsPlugin>();
When creating the
MongoDBMemoryStore
object, we passed it an index called restaurants_index
, but that doesn’t exist yet. So let’s change that!It’s coming very soon (version 3.1 of the C# driver) but for now, there is no neat and readable way to programmatically create a vector search index in C#.
The easiest way to create one is from within the Atlas UI in a browser or via MongoDB Compass.
I won’t go into detail here as we already have lots of content on how to do it. Check out our documentation that shows how to do it, if you need help.
Você pode utilizar o seguinte JSON para definir o índice de pesquisa vetorial:
1 { 2 "fields": [ 3 { 4 "type": "vector", 5 "path": "embedding", 6 "numDimensions": 1536, 7 "similarity": "cosine" 8 } 9 ] 10 }
Recomendamos chamar o índice
restaurants_index
para corresponder ao código. Se você optar por usar outra coisa, certifique-se de atualizar o código colado anteriormente dentro de RestaurantsPlugin
.Agora que temos todas as peças definidas, é hora de unir tudo. Vamos solicitar a entrada do usuário e usar essa resposta para construir o que queremos que AI a IA faça.
Primeiro, vamos dizer à IA AI que queremos que ela atenda às chamadas automaticamente por conta própria. Adicione o seguinte após a chamada para criar a variável plugins:
1 OpenAIPromptExecutionSettings settings = new() 2 { 3 ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions 4 };
Next, let’s add the user interaction:
1 Console.WriteLine("What would you like to make for dinner?"); 2 var input = Console.ReadLine();
Agora queremos criar um prompt que especifique o que queremos alcançar e comece a adicionar nosso primeiro plugin-in:
1 string ingredientsPrompt = @"This is a list of ingredients available to the user: 2 {{IngredientsPlugin.GetIngredientsFromCupboard}} 3 4 Based on their requested dish " + input + ", list what ingredients they are missing from their cupboard to make that meal and return just the list of missing ingredients. If they have similar items such as pasta instead of a specific type of pasta, don't consider it missing";
Você pode ver aqui como o prompt é construído. Informamos que temos componentes disponíveis que eles podem obter chamando o método que passamos entre double colchetes duplos. Ele está no formato PluginName.Method, então pode parecer familiar.
Em seguida, damos a ele acesso à resposta do usuário sobre o que ele quer Comer e usamos isso para solicitar que ele veja quais componentes estão faltando para fazer a refação. Mais uma vez, há um pouco de engenharia rápida ocorrendo no final, para evitar que seja muito complicado e ignore componentes perfeitamente válidos.
Podemos então invocar esse prompt:
1 var ingredientsResult = await kernel.InvokePromptAsync(ingredientsPrompt, new(settings)); 2 3 var missing = ingredientsResult.ToString().ToLower() 4 .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) 5 .Where(line => line.StartsWith("- ")) 6 .ToList();
The AI has a tendency to return quite a lot of extra explainer text alongside the list of ingredients so the missing variable just does some basic formatting to grab only the list of missing ingredients as we need to be very specific.
We now want to have some fun with the user and decide whether they have enough ingredients to make their meal or something similar, or should just not bother and go to a restaurant instead! But as well as suggesting they go to a restaurant, we will use our custom plugin to recommend some restaurants too!
1 var cuisineResult = await kernel.InvokeAsync( 2 plugins["GetCuisine"], 3 new() { { "cuisine", input } } 4 ); 5 6 if (missing.Count >= 5) 7 { 8 string restaurantPrompt = @"This is the cuisine that the user requested: " + cuisineResult + @". Based on this cuisine, recommend a restaurant for the user to eat at. Include the name and address 9 {{RestaurantsPlugin.GetRecommendedRestaurant}}"; 10 11 var kernelArguments = new KernelArguments(settings) 12 { 13 { "cuisine", cuisineResult } 14 }; 15 16 var restaurantResult = await kernel.InvokePromptAsync(restaurantPrompt, kernelArguments); 17 18 Console.WriteLine($"You have so many missing ingredients ({missing.Count}!), why bother? {restaurantResult}"); 19 } 20 else if(missing.Count < 5 && missing.Count > 0) 21 { 22 Console.WriteLine($"You have most of the ingredients to make {input}. You are missing: "); 23 foreach (var ingredient in missing) 24 { 25 Console.WriteLine(ingredient); 26 } 27 string similarPrompt = @"The user requested to make " + input + @" but is missing some ingredients. Based on what they want to eat, suggest another meal that is similar from the " + cuisineResult + " cuisine they can make and tell them the name of it but do not return a full recipe"; 28 var similarResult = await kernel.InvokePromptAsync(similarPrompt, new(settings)); 29 30 Console.WriteLine(similarResult); 31 } 32 else { 33 Console.WriteLine("You have all the ingredients to make " + input + "!"); 34 string recipePrompt = @"Find a recipe for making " + input; 35 var recipeResult = await kernel.InvokePromptAsync(recipePrompt, new(settings)); 36 Console.WriteLine(recipeResult); 37 }
Como buscamos todos os prompts disponíveis no diretório de prompts anteriormente, agora os temos disponíveis na array de plugins que mencionei. Então, começando por descobrir qual é a cozinha.
A chamada usa um objeto KernelArguments que contém valores que queremos disponibilizar, portanto, criamos um novo in-line para a chamada, passando o nome da variável de entrada, correspondendo
cuisine
ao que definimos anteriormente e ao valor que queremos atribuir a isso.Em seguida, usamos algumas declarações básicas if/else para lidar com as várias condições, que vão desde a falta de muitos componentes, a falta de apenas alguns e a falta de nenhum!
Dentro de cada um, um prompt ligeiramente diferente para a AI IA é criado e usado, com a resposta então enviada ao usuário.
Agora que temos todo o código, vamos testá-lo!
Debug from within your IDE or using the .NET CLI.
dotnet run
Em seguida, você verá o prompt perguntando o que quer para o restaurante. Tente inserir sua comida favorita e veja como ela funciona!
Dependendo do modelo, pode levar alguns segundos para ser executado, portanto, se você não obter uma resposta instantânea ao nome da sua comida, não se desespere!
Woo! In this tutorial, we have put together the power of Microsoft’s Semantic Kernel, OpenAI, and MongoDB Atlas to build a powerful AI agent that helps users find out whether to visit the grocery store or go out for dinner!
Why don’t you try playing around with different meals or ingredients in the included text file and see what recommendations you get?
Se você tiver uma cozinha inteligentes que tenha uma API para lhe informar quais componentes você tem, pode até tentar combiná-los para que alguns dos dados sejam genuinamente seus!
Boa tarde!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.