Lab 10 -Build a Chat Bot Using Azure OpenAI, Azure Cosmos DB for NoSQL, And Blazor (Optional))
Lab 10 -Build a Chat Bot Using Azure OpenAI, Azure Cosmos DB for NoSQL, And Blazor (Optional))
The .NET SDK for Azure Cosmos DB for NoSQL is useful for business applications that
need to manage various account resources including databases, containers, and items.
The SDK can perform queries that return multiple items, operations on specific items,
and even transactions that batch operations together over multiple items.
The .NET SDK for Azure OpenAI provides a streamlined interface for interacting with the
various models available for deployment. Specifically, the SDK includes classes to
interface directly with the model, send prompts, fine tune the requests, and parse the
completion responses.
Exercise 1: Setup
To complete this project, you need an Azure Cosmos DB for NoSQL account and an Azure
OpenAI account. To streamline this process, deploy a Bicep template to Azure with both of these
accounts.
3. In You have no storage mounted dialog box, click on the Create storage.
4. Ensure the type of shell indicated on the top left of the Cloud Shell pane is switched
to Bash. If it’s PowerShell, switch to Bash by using the drop-down menu.
5. Once the terminal starts, enter the following command to download the sample
application.
6. Create a new shell variable named resourceGroupName with the name of the Azure
resource group that you create (mslearn-cosmos-openai).
Copy
resourceGroupName="mslearn-cosmos-openai"
7. Create a resource group using az group create. Then, execute the following command
Copy
8. Deploy the azuredeploy.json template file to the resource group using az group
deployment create. Then, execute the following command.
Copy
9. Wait for the deployment to complete before proceeding with this project. This
deployment can take approximately 5-10 minutes.
1. Open your browser, navigate to the address bar, and type or paste the following
URL: https://portal.azure.com/, then press the Enter button.
2. Click on the Portal Menu, then select Resource group.
5. Now, select the Azure Cosmos DB account to navigate to the resource's page.
6. Select the Keys option in the Settings section of the resource navigation menu.
Record the value of the URI and PRIMARY KEY fields. You use these values later.
7. Return to the Resource Groups page. Select the Azure OpenAI account.
8. In your Azure Open AI window, navigate to the Resource Management section,
and click on Keys and Endpoints.
9. In Keys and Endpoints page, copy KEY1, (You can use either KEY1 or KEY2) and
Endpoint and then Save the notepad to use the information in the upcoming tasks.
Task 3: Run the Docker
1. In your Windows search box, type Docker , then click on Docker Desktop.
2. Run the Docker Desktop.
Task 4: Install Dev Containers extension
1. In your Windows search box, type Visual Studio, then click on Visual Studio Code.
2. Open your browser, navigate to the address bar, type or paste the following URL:
https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-
containers then press the Enter button.
BashCopy
git checkout start
1. Open the Command Palette, search for the >Dev Containers commands, and then
select >Dev Containers: Open Folder in Container.
2. Navigate and select cosmosdb-chatgpt folder from C:\LabFiles and click on the
Select Folder button.
3. Visual Studio Code a notification stating Folder contains a Dev Container
configuration file. Reopen folder to develop in a container appers, then click on the
Reopen in Container button.
1. In case, Not all host requirements in devcontainer.json are met by the Docker
daemon. dialog box appears, then click on the Continue button.
4. Build the .NET project. Run the bellow command on the terminal.
BashCopy
dotnet build
• Open the CodeTour within the application and walk through the entire
tour.
• Successfully build the application.
• Run the application using the Hot Reload feature of .NET
After you complete this exercise, you'll have a general understanding of the project and
its components.
14. Finally, review the final step of the guided tour and finish the tour.
Task 2: Build and run the application
Now it's time to make sure the application works as expected. In this step, build the
application to verify that there's no issues before you start and run the application using
the stubbed out implementations of the service methods.
1. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
2. Start the application with hot reloads enabled using dotnet watch. Run the below
command
BashCopy
dotnet watch run --non-interactive
3. Visual Studio Code launches an in-tool simple browser with the web application
running.
4. In the web application, create a New Chat session with at least one message.
5. The AI assistant responds with the prebaked string values that you observed during
the guided tour of the project's code.
Important: Closing the terminal releases the port so you can rebuild and run this
application again later in this project. If you forget to close the terminal, you may run
into issues with the application's port already being in use when debugging later in the
project.
The Azure.AI.OpenAI package on NuGet provides a typed SDK to access various model
deployments from your account endpoint.
1. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
2. Use dotnet add package to import the Azure.AI.OpenAI package from NuGet specifying
a prerelease version.
BashCopy
dotnet add package Azure.AI.OpenAI --prerelease
3. Build the .NET project again.
BashCopy
dotnet build
4. Close the terminal.
In a .NET application, it's common to use the configuration providers to inject new
settings into your application. For this application, use the appsettings.Development.json file
to provide the most current values for the Azure OpenAI endpoint and key.
On Linux, files are case-sensitive. The .NET environment for this project is
named Development and the filename must match that environment name to work.
2. Within the file, create a new JSON object with a placeholder property
for OpenAi settings.
JSONCopy
{
"OpenAi": {
}
}
3. Within the OpenAi property, create two new properties for the Endpoint and Key. Use
the Azure OpenAI endpoint and key settings you recorded earlier in this lab(Exercise
1>Task 2).
JSONCopy
{
"OpenAi": {
"Endpoint": "<your-azure-openai-endpoint>",
"Key": "<your-azure-openai-key>"
}
}
Finally, implement the class variables required to use the Azure OpenAI client. At this
step, implement a few static prompts and create a new instance of the OpenAIClient class.
3. Within the OpenAiService class, add a new variable named _client that's of
type OpenAIClient.
C#Copy
private readonly OpenAIClient _client;
4. Create a new string variable named _systemPromptText with a static block of text to
send to the AI assistant before each prompt.
C#Copy
private readonly string _systemPrompt = @"
You are an AI assistant that helps people find information.
Provide concise answers that are polite and professional." +
Environment.NewLine;
5. Create another new string variable named _summarizePrompt with a static block of
text to send to the AI assistant with instructions on how to summarize a
conversation.
C#Copy
private readonly string _summarizePrompt = @"
Summarize this prompt in one or two words to use as a label in a button
on a web page.
Do not use any punctuation." + Environment.NewLine;
6. Within the constructor of the class, add two extra lines of code to check if the
endpoint or key is null. Use ArgumentNullException.ThrowIfNullOrEmpty to throw an
error early if either of these values are null.
C#Copy
ArgumentNullException.ThrowIfNullOrEmpty(endpoint);
ArgumentNullException.ThrowIfNullOrEmpty(key);
Note:When you run the application, this will throw an error right away if either of these
settings don't have a valid value provided through
the appsettings.Development.json file.
7. Next, take the model name that is a parameter of the constructor and save it to
the _modelName variable.
C#Copy
_modelName = modelName;
8. Finally, create a new instance of the OpenAIClient class using the endpoint to build
a Uri and the key to build an AzureKeyCredential.
C#Copy
Uri uri = new(endpoint);
AzureKeyCredential credential = new(key);
_client = new(
endpoint: uri,
keyCredential: credential
);
Reference code
using Cosmos.Chat.GPT.Models;
using Azure;
using Azure.AI.OpenAI;
namespace Cosmos.Chat.GPT.Services;
ArgumentNullException.ThrowIfNullOrEmpty(modelName);
ArgumentNullException.ThrowIfNullOrEmpty(endpoint);
ArgumentNullException.ThrowIfNullOrEmpty(key);
_modelName = modelName;
Uri uri = new(endpoint);
AzureKeyCredential credential = new(key);
_client = new(
endpoint: uri,
keyCredential: credential
);
At this point, your constructor should include enough logic to create a client instance.
Since the class doesn't do anything with the client yet, there's no point in running the
web application, but there's value in building the application to make sure your code
doesn't have any omissions or errors.
1. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
BashCopy
dotnet build
3. Observe the build output and check to make sure there aren't any build errors.
• Send a question from the user to the AI assistant and ask for a response.
• Send a series of prompts to the AI assistant and ask for a summarization of the
conversation.
C#Copy
public async Task<(string completionText, int completionTokens)>
GetChatCompletionAsync(string sessionId, string userPrompt)
{
}
3. Create a new variable named options of type ChatCompletionsOptions. Add the two
message variables to the Messages list, set the value of User to
the sessionId constructor parameter, set MaxTokens to 4000, and set the remaining
properties to the recommended values here.
C#Copy
ChatCompletionsOptions options = new()
{
DeploymentName = "chatmodel",
Messages = {
new ChatRequestSystemMessage(_systemPrompt),
new ChatRequestUserMessage(userPrompt)
},
User = sessionId,
MaxTokens = 4000,
Temperature = 0.3f,
NucleusSamplingFactor = 0.5f,
FrequencyPenalty = 0,
PresencePenalty = 0
};
Note: 4096 is the maximum number of tokens for the gpt-35-turbo model. We're just
rounding down here to simplify things.
C#Copy
Response<ChatCompletions> completionsResponse =
await_client.GetChatCompletionsAsync(options);
ChatCompletions completions = completionsResponse.Value;
5. Finally, return a tuple as the result of the GetChatCompletionAsync method with the
content of the completion as a string, the number of tokens associated with the
prompt, and the number of tokens for the response.
C#Copy
return (
completionText: completions.Choices[0].Message.Content,
completionTokens: completions.Usage.CompletionTokens
);
Task 2: Ask the AI model to summarize a conversation
Now, send the AI model a different system prompt, your current conversation, and
session ID so the AI model can summarize the conversation in a couple of words.
C#Copy
2. Create a ChatCompletionsOptions variable named options with the two message variables
in the Messages list, User set to the sessionId constructor parameter, MaxTokens set
to 200, and the remaining properties to the recommended values here:
C#Copy
DeploymentName = "chatmodel",
Messages = {
new ChatRequestSystemMessage(_systemPrompt),
new ChatRequestUserMessage(conversationText)
},
User = sessionId,
MaxTokens = 200,
Temperature = 0.0f,
NucleusSamplingFactor = 1.0f,
FrequencyPenalty = 0,
PresencePenalty = 0
};
C#Copy
C#Copy
string completionText = completions.Choices[0].Message.Content;
return completionText;
// ChatRequestSystemMessage systemMessage =
new(ChatRole.System, _systemPrompt);
// ChatRequestUserMessage userMessage = new(ChatRole.User,
userPrompt);
ChatCompletionsOptions options = new()
{
DeploymentName = "chatmodel",
Messages = {
new ChatRequestSystemMessage(_systemPrompt),
new ChatRequestUserMessage(userPrompt)
},
User = sessionId,
MaxTokens = 4000,
Temperature = 0.3f,
NucleusSamplingFactor = 0.5f,
FrequencyPenalty = 0,
PresencePenalty = 0
};
return (
completionText: completions.Choices[0].Message.Content,
completionTokens: completions.Usage.CompletionTokens
);
}
return completionText;
}
}
At this point, your application should have a thorough enough implementation of the
Azure OpenAI service that you can test the application. Remember, you haven't
implemented a data store yet, so your conversations aren't persisted between
debugging sessions.
1. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
BashCopy
dotnet build
3. Start the application with hot reloads enabled using dotnet watch.
BashCopy
Note: The Hot Reload feature is enabled here if you need to make a small correction to
the application's code. For more information, see .NET Hot Reload support for
ASP.NET Core.
The Microsoft.Azure.Cosmos NuGet package is a typed library that simplifies the process of
accessing Azure Cosmos DB for NoSQL from a .NET application.
1. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
2. Import the Microsoft.Azure.Cosmos package from NuGet with dotnet add package.
BashCopy
BashCopy
dotnet build
4. Close the terminal.
Use the appsettings.Development.json file again to provide current values for the Azure
Cosmos DB for NoSQL endpoint and key.
Finally, implement the class variables and client required to access Azure Cosmos DB for
NoSQL using the client. For this step, use the SDK's client classes to implement an
instance of type Container in the CosmosDbService class.
• Microsoft.Azure.Cosmos
• Microsoft.Azure.Cosmos.Fluent
C#Copy
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Fluent;
3. Within the CosmosDbService class, add a new Container-typed variable named _container.
C#Copy
C#Copy
ArgumentNullException.ThrowIfNullOrEmpty(endpoint);
ArgumentNullException.ThrowIfNullOrEmpty(key);
C#Copy
Note: Setting this property will ensure that the JSON produced by the SDK is both
serialized and deserialized in camel case regardless of how it's corresponding property is
cased in the .NET class.
C#Copy
C#Copy
C#Copy
9. Finally, assign the constructor's container variable to the class' _container variable only if
it's not null. If it's null, throw an ArgumentException.
C#Copy
_container = container ??
throw new ArgumentException("Unable to connect to existing Azure Cosmos DB
container or database.");
At this point, your constructor should include enough logic to create a container
instance that the rest of the service uses. Since the class doesn't do anything with the
container yet, there's no point in running the web application, but there's value in
building the application to make sure your code doesn't have any omissions or errors.
1. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
2. Build the .NET project.
BashCopy
dotnet build
3. Observe the build output and check to make sure there aren't any build errors.
4. Close the terminal.
Azure Cosmos DB for NoSQL stores data in JSON format allowing us to store many
types of data in a single container. This application stores both a chat "session" with the
AI assistant and the individual "messages" within each session. With the API for NoSQL,
the application can store both types of data in the same container and then differentiate
between these types using a simple type field.
C#Copy
3. Create a new variable named partitionKey of type PartitionKey using the current
session's SessionId property as the parameter.
C#Copy
4. Invoke the CreateItemAsync method of the container passing in the session parameter
and partitionKey variable. Return the response as the result of
the InsertSessionAsync method.
C#Copy
6. Create a PartitionKey variable using session.SessionId as the value of the partition key.
C#Copy
7. Create a new message variable named newMessage with the Timestamp property
updated to the current UTC timestamp.
C#Copy
8. Invoke CreateItemAsync passing in both the new message and partition key variables.
Return the response as the result of InsertMessageAsync.
C#Copy
There are two main use cases where the application needs to retrieve multiple items
from our container. First, the application retrieves all sessions for the current user by
filtering the items to ones where type = Session. Second, the application retrieves all
messages for a session by performing a similar filter where type = Session & sessionId =
<current-session-id>. Implement both queries here using the .NET SDK and a feed iterator.
C#Copy
C#Copy
C#Copy
C#Copy
while (response.HasMoreResults)
{
}
Note: Using a while loop here will effectively loop through all pages of your response
until there are no pages left.
6. Within the while loop, asynchronously get the next page of results by
invoking ReadNextAsync on the response variable and then add those results to the list
variable named output.
C#Copy
7. Outside the while loop, return the output variable with a list of sessions as the result
of the GetSessionsAsync method.
C#Copy
return output;
C#Copy
9. Create a query variable of type QueryDefinition. Use the SQL query SELECT * FROM c WHERE
c.sessionId = @sessionId AND c.type = @type. Use the fluent WithParameter method to assign
the @sessionId parameter to the session identifier passed in as a parameter, and
the @type parameter to the name of the Message class.
C#Copy
C#Copy
11. Use a while loop to iterate through all pages of results and store the results in a
single List<Message> variable named output.
C#Copy
12. Return the output variable as the result of the GetSessionMessagesAsync method.
C#Copy
return output;
There are scenarios when either a single session requires an update or more than one
message requires an update. For the first scenario, use the ReplaceItemAsync method of
the SDK to replace an existing item with a modified version. For the second scenario, use
the transactional batch capability of the SDK to modify multiple messages in a single
batch.
2. Create a PartitionKey variable using session.SessionId as the value of the partition key.
C#Copy
3. Invoke ReplaceItemAsync passing in the new message, message's unique identifier and
partition key. Return the response as the result of UpdateSessionAsync.
C#Copy
5. Use language-integrated query (LINQ) to validate that all messages contain a single
session identifier (SessionId). If any of the messages contain a different value, throw
an ArgumentException.
C#Copy
6. Create a new PartitionKey variable using the SessionId property of the first message.
C#Copy
Note: Remember, you can safely assume that all messages have the same session
identifier if the application has moved to this point in the method's code.
C#Copy
TransactionalBatch batch = _container.CreateTransactionalBatch(partitionKey);
8. Iterate over each message in the messages array using a foreach loop.
C#Copy
9. Within the foreach loop, add each message as an upsert operation to the batch.
C#Copy
batch.UpsertItem(
item: message
);
Note: Upsert tells Azure Cosmos DB to determine, server-side, whether an item should
be replaced or updated. Azure Cosmos DB will make this determination with the id and
partition key of each item.
10. Outside of the foreach loop, asynchronously invoke the ExecuteAsync method of the
batch to execute all operations within the batch.
C#Copy
await batch.ExecuteAsync();
11. Save the Services/CosmosDbService.cs file.
Finally, combine the query and transactional batch functionality to remove multiple
items. In this example, get the session item and all related messages by querying for all
items with a specific session identifier regardless of type. Then, create a transactional
batch to delete all matched items as a single transaction.
2. Create a variable named partitionKey of type PartitionKey using the sesionId string value
passed in as a parameter to this method.
C#Copy
3. Using the same sessionId method parameter, build a QueryDefinition object that finds all
items that match the session identifier. Use a query parameter for the sessionId and
ensure that you don't filter the query on the type of item.
C#Copy
4. Create a new FeedIterator<string> using GetItemQueryIterator and the query you built.
C#Copy
C#Copy
2. Create a while loop to iterate through all pages of results. Within the
while loop, get the next page of results and use a foreach loop to
iterate through all item identifiers per page. Within the foreach loop,
add a batch operation to delete the item using batch.DeleteItem.
C#Copy
while (response.HasMoreResults)
{
FeedResponse<string> results = await response.ReadNextAsync();
foreach (var itemId in results)
{
batch.DeleteItem(
id: itemId
);
}
}
C#Copy
await batch.ExecuteAsync();
namespace Cosmos.Chat.GPT.Services;
_container = container ??
throw new ArgumentException("Unable to connect to existing Azure Cosmos DB container or
database.");
}
while (response.HasMoreResults)
{
FeedResponse<Session> results = await response.ReadNextAsync();
output.AddRange(results);
}
return output;
}
return output;
}
Now your application has a full implementation of Azure OpenAI and Azure Cosmos DB.
You can test the application end-to-end by debugging the solution.
5. In the Visual Studio Code editor, click on Terminal, open a New Terminal.
BashCopy
dotnet build
7. Start the application with hot reloads enabled using dotnet watch.
BashCopy
BashCopy
4. In the Resource group page, navigate to command bar and click on Delete resource
group.
5. In the Delete Resource group pane that appears on the right side, enter the
resource group name and click on Delete button.