Summary: This post explores how to build sophisticated AI-powered chatbots using .NET and Azure Bot Framework. Learn how to integrate Azure OpenAI Service, implement natural language understanding, manage conversation state, and deploy your chatbot across multiple channels.
Introduction
Chatbots have evolved from simple rule-based systems to sophisticated AI-powered assistants capable of natural conversations. With the advancements in large language models (LLMs) like GPT-4, developers can now create chatbots that understand context, maintain coherent conversations, and provide helpful responses to a wide range of queries.
In this post, we’ll explore how to build AI-powered chatbots using .NET and Azure Bot Framework, with integration to Azure OpenAI Service. We’ll cover everything from setting up the development environment to implementing natural language understanding, managing conversation state, and deploying your chatbot across multiple channels. By the end of this post, you’ll have the knowledge to create a sophisticated chatbot that leverages the power of modern AI.
Understanding Modern Chatbot Architecture
Before diving into implementation, let’s understand the architecture of a modern AI-powered chatbot.
Key Components
A sophisticated chatbot typically consists of these components:
- Conversation Interface: The channels where users interact with your bot (web, Teams, Slack, etc.)
- Bot Framework: Handles message routing, conversation state, and integration with channels
- Natural Language Understanding (NLU): Interprets user intent and extracts entities
- Dialog Management: Controls the flow of conversation
- Knowledge Base: Provides information for the bot to reference
- Large Language Model: Generates natural language responses
- Integration Services: Connects to external systems and APIs
Azure Bot Framework Architecture
Azure Bot Framework provides a comprehensive platform for building chatbots:
- Bot Framework SDK: Libraries for building bots in .NET or JavaScript
- Bot Framework Service: Cloud service that connects your bot to channels
- Language Understanding: NLU capabilities through Azure Cognitive Services
- Azure Bot Service: Hosting and management of your bot
- Azure OpenAI Service: Integration with GPT models for advanced language capabilities
Setting Up Your Development Environment
Let’s start by setting up your development environment.
Prerequisites
To follow along with this tutorial, you’ll need:
- Visual Studio 2022 or Visual Studio Code
- .NET 6 SDK or later
- An Azure subscription
- Access to Azure OpenAI Service
- Bot Framework Emulator for local testing
Creating a New Bot Project
Let’s create a new bot project using the Bot Framework SDK:
bash
# Install the Bot Framework templates
dotnet new -i Microsoft.Bot.Framework.CSharp.EchoBot
# Create a new bot project
dotnet new echobot -n AiChatbot
cd AiChatbot
Installing Required Packages
Add the necessary packages to your project:
bash
dotnet add package Microsoft.Bot.Builder.Integration.AspNet.Core
dotnet add package Microsoft.Bot.Builder.AI.Luis
dotnet add package Microsoft.Bot.Builder.Dialogs
dotnet add package Azure.AI.OpenAI
dotnet add package Microsoft.Extensions.Http
Building the Bot Foundation
Let’s start by implementing the basic structure of our bot.
Bot Implementation
First, let’s create a basic bot class:
csharp
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public class AiChatbot : ActivityHandler
{
private readonly BotState _conversationState;
private readonly BotState _userState;
private readonly IOpenAIService _openAIService;
public AiChatbot(
ConversationState conversationState,
UserState userState,
IOpenAIService openAIService)
{
_conversationState = conversationState;
_userState = userState;
_openAIService = openAIService;
}
protected override async Task OnMessageActivityAsync(
ITurnContext<IMessageActivity> turnContext,
CancellationToken cancellationToken)
{
// Get conversation state
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData(), cancellationToken);
// Get user state
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile(), cancellationToken);
// Update conversation history
conversationData.ConversationHistory.Add(new ConversationTurn
{
Role = "user",
Content = turnContext.Activity.Text
});
// Generate response using OpenAI
string response = await _openAIService.GenerateResponseAsync(
conversationData.ConversationHistory,
userProfile);
// Add bot response to history
conversationData.ConversationHistory.Add(new ConversationTurn
{
Role = "assistant",
Content = response
});
// Trim history if it gets too long
if (conversationData.ConversationHistory.Count > 20)
{
// Keep system message if present, plus last 10 turns
if (conversationData.ConversationHistory[0].Role == "system")
{
var systemMessage = conversationData.ConversationHistory[0];
conversationData.ConversationHistory =
new List<ConversationTurn> { systemMessage }
.Concat(conversationData.ConversationHistory
.Skip(conversationData.ConversationHistory.Count - 10))
.ToList();
}
else
{
conversationData.ConversationHistory = conversationData.ConversationHistory
.Skip(conversationData.ConversationHistory.Count - 10)
.ToList();
}
}
// Send response
await turnContext.SendActivityAsync(MessageFactory.Text(response), cancellationToken);
// Save state
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
}
protected override async Task OnMembersAddedAsync(
IList<ChannelAccount> membersAdded,
ITurnContext<IConversationUpdateActivity> turnContext,
CancellationToken cancellationToken)
{
foreach (var member in membersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
// Get conversation state
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData(), cancellationToken);
// Add system message to history
conversationData.ConversationHistory.Add(new ConversationTurn
{
Role = "system",
Content = "You are a helpful AI assistant that provides accurate and concise information. You are friendly and conversational. If you don't know the answer to a question, you should say so rather than making up information."
});
// Save state
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
// Send welcome message
await turnContext.SendActivityAsync(MessageFactory.Text("Hello! I'm your AI assistant. How can I help you today?"), cancellationToken);
}
}
}
}
public class ConversationData
{
public List<ConversationTurn> ConversationHistory { get; set; } = new List<ConversationTurn>();
}
public class ConversationTurn
{
public string Role { get; set; }
public string Content { get; set; }
}
public class UserProfile
{
public string Name { get; set; }
public string PreferredLanguage { get; set; }
public Dictionary<string, string> Preferences { get; set; } = new Dictionary<string, string>();
}
OpenAI Service Implementation
Next, let’s implement the OpenAI service:
csharp
using Azure;
using Azure.AI.OpenAI;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public interface IOpenAIService
{
Task<string> GenerateResponseAsync(
List<ConversationTurn> conversationHistory,
UserProfile userProfile);
}
public class OpenAIService : IOpenAIService
{
private readonly OpenAIClient _openAIClient;
private readonly string _deploymentName;
public OpenAIService(IConfiguration configuration)
{
string endpoint = configuration["OpenAI:Endpoint"];
string apiKey = configuration["OpenAI:ApiKey"];
_deploymentName = configuration["OpenAI:DeploymentName"];
_openAIClient = new OpenAIClient(
new Uri(endpoint),
new AzureKeyCredential(apiKey));
}
public async Task<string> GenerateResponseAsync(
List<ConversationTurn> conversationHistory,
UserProfile userProfile)
{
// Create chat messages from conversation history
var chatMessages = conversationHistory.Select(turn =>
new ChatMessage(
turn.Role == "user" ? ChatRole.User :
turn.Role == "assistant" ? ChatRole.Assistant :
ChatRole.System,
turn.Content
)).ToList();
// If no system message is present, add one
if (!chatMessages.Any(m => m.Role == ChatRole.System))
{
chatMessages.Insert(0, new ChatMessage(
ChatRole.System,
"You are a helpful AI assistant that provides accurate and concise information. You are friendly and conversational. If you don't know the answer to a question, you should say so rather than making up information."
));
}
// Add user profile information to system message if available
if (!string.IsNullOrEmpty(userProfile.Name) || userProfile.Preferences.Any())
{
var systemMessage = chatMessages.First(m => m.Role == ChatRole.System);
string additionalContext = "\n\nUser information:";
if (!string.IsNullOrEmpty(userProfile.Name))
{
additionalContext += $"\nName: {userProfile.Name}";
}
if (!string.IsNullOrEmpty(userProfile.PreferredLanguage))
{
additionalContext += $"\nPreferred language: {userProfile.PreferredLanguage}";
}
if (userProfile.Preferences.Any())
{
additionalContext += "\nPreferences:";
foreach (var preference in userProfile.Preferences)
{
additionalContext += $"\n- {preference.Key}: {preference.Value}";
}
}
// Update the system message
int systemIndex = chatMessages.IndexOf(systemMessage);
chatMessages[systemIndex] = new ChatMessage(
ChatRole.System,
systemMessage.Content + additionalContext
);
}
// Create chat completions options
var chatCompletionsOptions = new ChatCompletionsOptions
{
Temperature = 0.7f,
MaxTokens = 800,
NucleusSamplingFactor = 0.95f,
FrequencyPenalty = 0,
PresencePenalty = 0
};
// Add messages to options
foreach (var message in chatMessages)
{
chatCompletionsOptions.Messages.Add(message);
}
// Generate response
var response = await _openAIClient.GetChatCompletionsAsync(
_deploymentName,
chatCompletionsOptions);
return response.Value.Choices[0].Message.Content;
}
}
Configuring Dependency Injection
Now, let’s configure dependency injection in Startup.cs
:
csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Create the Bot Framework Authentication to be used with the Bot Adapter
services.AddSingleton<BotFrameworkAuthentication, ConfigurationBotFrameworkAuthentication>();
// Create the Bot Adapter with error handling enabled
services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
// Create the storage we'll be using for User and Conversation state
services.AddSingleton<IStorage, MemoryStorage>();
// Create the User state
services.AddSingleton<UserState>();
// Create the Conversation state
services.AddSingleton<ConversationState>();
// Register the OpenAI service
services.AddSingleton<IOpenAIService, OpenAIService>();
// Create the bot as a transient
services.AddTransient<IBot, AiChatbot>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseWebSockets();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Configuring appsettings.json
Add your Azure OpenAI Service configuration to appsettings.json
:
json
{
"MicrosoftAppType": "",
"MicrosoftAppId": "",
"MicrosoftAppPassword": "",
"MicrosoftAppTenantId": "",
"OpenAI": {
"Endpoint": "https://your-resource-name.openai.azure.com/",
"ApiKey": "your-api-key",
"DeploymentName": "gpt-4"
}
}