Building AI-Powered Chatbots with .NET and Azure Bot Framework

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:

  1. Conversation Interface: The channels where users interact with your bot (web, Teams, Slack, etc.)
  2. Bot Framework: Handles message routing, conversation state, and integration with channels
  3. Natural Language Understanding (NLU): Interprets user intent and extracts entities
  4. Dialog Management: Controls the flow of conversation
  5. Knowledge Base: Provides information for the bot to reference
  6. Large Language Model: Generates natural language responses
  7. 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"
  }
}