Back to Blog
6 min read

Azure Bot Service Updates: Building Intelligent Conversational AI

Azure Bot Service continues to evolve, making it easier to build sophisticated conversational experiences. With Ignite 2022 updates and improved integration with Azure OpenAI, building intelligent bots is more accessible than ever.

Bot Framework Composer Updates

Bot Framework Composer now supports more advanced dialog patterns:

// dialog.schema
{
  "$kind": "Microsoft.AdaptiveDialog",
  "autoEndDialog": true,
  "defaultResultProperty": "dialog.result",
  "triggers": [
    {
      "$kind": "Microsoft.OnConversationUpdateActivity",
      "actions": [
        {
          "$kind": "Microsoft.SendActivity",
          "activity": "${WelcomeMessage()}"
        }
      ]
    },
    {
      "$kind": "Microsoft.OnIntent",
      "intent": "OrderStatus",
      "actions": [
        {
          "$kind": "Microsoft.BeginDialog",
          "dialog": "OrderStatusDialog"
        }
      ]
    }
  ]
}

Building with Bot Framework SDK

Basic Bot with OpenAI Integration

using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;
using Azure.AI.OpenAI;

public class OpenAIBot : ActivityHandler
{
    private readonly OpenAIClient _openAIClient;
    private readonly ConversationState _conversationState;

    public OpenAIBot(OpenAIClient openAIClient, ConversationState conversationState)
    {
        _openAIClient = openAIClient;
        _conversationState = conversationState;
    }

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext,
        CancellationToken cancellationToken)
    {
        var conversationData = await GetConversationDataAsync(turnContext, cancellationToken);

        // Add user message to history
        conversationData.History.Add(new ChatMessage(ChatRole.User, turnContext.Activity.Text));

        // Generate response using OpenAI
        var chatCompletionsOptions = new ChatCompletionsOptions
        {
            DeploymentName = "gpt-35-turbo",
            Messages = {
                new ChatMessage(ChatRole.System, GetSystemPrompt())
            }
        };

        foreach (var message in conversationData.History.TakeLast(10))
        {
            chatCompletionsOptions.Messages.Add(message);
        }

        var response = await _openAIClient.GetChatCompletionsAsync(chatCompletionsOptions, cancellationToken);
        var assistantMessage = response.Value.Choices[0].Message.Content;

        // Add assistant response to history
        conversationData.History.Add(new ChatMessage(ChatRole.Assistant, assistantMessage));

        await turnContext.SendActivityAsync(
            MessageFactory.Text(assistantMessage),
            cancellationToken);

        await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
    }

    private string GetSystemPrompt() => """
        You are a helpful customer service assistant for Contoso.
        Be polite, concise, and helpful.
        If you don't know something, say so rather than making things up.
        """;

    private async Task<ConversationData> GetConversationDataAsync(
        ITurnContext turnContext,
        CancellationToken cancellationToken)
    {
        var accessor = _conversationState.CreateProperty<ConversationData>("ConversationData");
        return await accessor.GetAsync(turnContext, () => new ConversationData(), cancellationToken);
    }
}

public class ConversationData
{
    public List<ChatMessage> History { get; set; } = new();
}

Skill Integration

// SkillBot.cs - Child skill
public class OrderSkillBot : ActivityHandler
{
    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext,
        CancellationToken cancellationToken)
    {
        var text = turnContext.Activity.Text.ToLower();

        if (text.Contains("order status"))
        {
            var orderId = ExtractOrderId(text);
            var status = await GetOrderStatusAsync(orderId);

            await turnContext.SendActivityAsync(
                MessageFactory.Text($"Order {orderId}: {status}"),
                cancellationToken);

            // End skill
            await turnContext.SendActivityAsync(
                new Activity { Type = ActivityTypes.EndOfConversation },
                cancellationToken);
        }
    }
}

// ParentBot.cs - Root bot that uses skills
public class ParentBot : ActivityHandler
{
    private readonly SkillHttpClient _skillClient;
    private readonly SkillsConfiguration _skillsConfig;

    protected override async Task OnMessageActivityAsync(
        ITurnContext<IMessageActivity> turnContext,
        CancellationToken cancellationToken)
    {
        var intent = await ClassifyIntentAsync(turnContext.Activity.Text);

        if (intent == "order")
        {
            // Forward to order skill
            var skill = _skillsConfig.Skills["OrderSkill"];

            await _skillClient.PostActivityAsync(
                _skillsConfig.SkillHostEndpoint,
                skill,
                turnContext.Activity.ServiceUrl,
                turnContext.Activity,
                cancellationToken);
        }
        else
        {
            // Handle locally
            await HandleLocallyAsync(turnContext, cancellationToken);
        }
    }
}

Adaptive Cards with Dynamic Content

public class CardFactory
{
    public static Attachment CreateOrderStatusCard(Order order)
    {
        var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 4))
        {
            Body = new List<AdaptiveElement>
            {
                new AdaptiveTextBlock
                {
                    Text = $"Order #{order.OrderId}",
                    Size = AdaptiveTextSize.Large,
                    Weight = AdaptiveTextWeight.Bolder
                },
                new AdaptiveFactSet
                {
                    Facts = new List<AdaptiveFact>
                    {
                        new AdaptiveFact("Status", order.Status),
                        new AdaptiveFact("Date", order.OrderDate.ToShortDateString()),
                        new AdaptiveFact("Total", order.Total.ToString("C"))
                    }
                },
                new AdaptiveContainer
                {
                    Items = order.Items.Select(item => new AdaptiveColumnSet
                    {
                        Columns = new List<AdaptiveColumn>
                        {
                            new AdaptiveColumn
                            {
                                Width = "stretch",
                                Items = new List<AdaptiveElement>
                                {
                                    new AdaptiveTextBlock { Text = item.Name }
                                }
                            },
                            new AdaptiveColumn
                            {
                                Width = "auto",
                                Items = new List<AdaptiveElement>
                                {
                                    new AdaptiveTextBlock { Text = item.Price.ToString("C") }
                                }
                            }
                        }
                    }).ToList<AdaptiveElement>()
                }
            },
            Actions = new List<AdaptiveAction>
            {
                new AdaptiveSubmitAction
                {
                    Title = "Track Package",
                    Data = new { action = "track", orderId = order.OrderId }
                },
                new AdaptiveOpenUrlAction
                {
                    Title = "View Details",
                    Url = new Uri($"https://contoso.com/orders/{order.OrderId}")
                }
            }
        };

        return new Attachment
        {
            ContentType = AdaptiveCard.ContentType,
            Content = card
        };
    }
}

Multi-channel Deployment

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Bot configuration
builder.Services.AddSingleton<IBotFrameworkHttpAdapter, AdapterWithErrorHandler>();
builder.Services.AddSingleton<IBot, OpenAIBot>();

// Channel-specific configuration
builder.Services.Configure<TeamsChannelOptions>(options =>
{
    options.AllowedTenants = new[] { "tenant-id" };
});

builder.Services.Configure<SlackChannelOptions>(options =>
{
    options.VerificationToken = builder.Configuration["Slack:VerificationToken"];
});

var app = builder.Build();

// Bot endpoints
app.MapPost("/api/messages", async (IBotFrameworkHttpAdapter adapter, IBot bot, HttpContext context) =>
{
    await adapter.ProcessAsync(context.Request, context.Response, bot);
});

// Teams-specific endpoint
app.MapPost("/api/teams", async (TeamsAdapter adapter, IBot bot, HttpContext context) =>
{
    await adapter.ProcessAsync(context.Request, context.Response, bot);
});

app.Run();

Proactive Messaging

public class ProactiveMessagingService
{
    private readonly IBotFrameworkHttpAdapter _adapter;
    private readonly IConversationReferenceStore _referenceStore;

    public ProactiveMessagingService(
        IBotFrameworkHttpAdapter adapter,
        IConversationReferenceStore referenceStore)
    {
        _adapter = adapter;
        _referenceStore = referenceStore;
    }

    public async Task SendOrderUpdateAsync(string userId, Order order)
    {
        var reference = await _referenceStore.GetReferenceAsync(userId);

        if (reference == null)
        {
            return; // User hasn't talked to bot
        }

        await ((BotAdapter)_adapter).ContinueConversationAsync(
            reference.BotId,
            reference,
            async (turnContext, cancellationToken) =>
            {
                var card = CardFactory.CreateOrderStatusCard(order);

                await turnContext.SendActivityAsync(
                    MessageFactory.Attachment(card),
                    cancellationToken);
            },
            CancellationToken.None);
    }

    public async Task SendBroadcastAsync(string message)
    {
        var references = await _referenceStore.GetAllReferencesAsync();

        var tasks = references.Select(reference =>
            ((BotAdapter)_adapter).ContinueConversationAsync(
                reference.BotId,
                reference,
                async (turnContext, cancellationToken) =>
                {
                    await turnContext.SendActivityAsync(
                        MessageFactory.Text(message),
                        cancellationToken);
                },
                CancellationToken.None));

        await Task.WhenAll(tasks);
    }
}

QnA Maker Integration

public class QnADialog : ComponentDialog
{
    private readonly QnAMakerClient _qnaClient;

    public QnADialog(QnAMakerClient qnaClient) : base(nameof(QnADialog))
    {
        _qnaClient = qnaClient;

        var waterfallSteps = new WaterfallStep[]
        {
            GetAnswerAsync,
            ProcessFeedbackAsync
        };

        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
        AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
    }

    private async Task<DialogTurnResult> GetAnswerAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken)
    {
        var question = stepContext.Context.Activity.Text;

        var response = await _qnaClient.GetAnswersAsync(
            stepContext.Context,
            new QnAMakerOptions
            {
                Top = 1,
                ScoreThreshold = 0.5f
            });

        if (response?.FirstOrDefault() != null)
        {
            var answer = response.First();

            await stepContext.Context.SendActivityAsync(
                MessageFactory.Text(answer.Answer),
                cancellationToken);

            return await stepContext.PromptAsync(
                nameof(ConfirmPrompt),
                new PromptOptions
                {
                    Prompt = MessageFactory.Text("Was this helpful?")
                },
                cancellationToken);
        }

        await stepContext.Context.SendActivityAsync(
            MessageFactory.Text("I don't have information about that. Let me connect you with a human agent."),
            cancellationToken);

        return await stepContext.EndDialogAsync(null, cancellationToken);
    }

    private async Task<DialogTurnResult> ProcessFeedbackAsync(
        WaterfallStepContext stepContext,
        CancellationToken cancellationToken)
    {
        var helpful = (bool)stepContext.Result;

        if (!helpful)
        {
            // Log for improvement
            await LogNegativeFeedbackAsync(stepContext.Context.Activity.Text);
        }

        return await stepContext.EndDialogAsync(null, cancellationToken);
    }
}

Testing Bots

using Microsoft.Bot.Builder.Testing;
using Xunit;

public class BotTests
{
    [Fact]
    public async Task Welcome_Message_On_Conversation_Start()
    {
        var adapter = new TestAdapter();
        var bot = new OpenAIBot(CreateMockOpenAIClient(), new ConversationState(new MemoryStorage()));

        await adapter
            .Send(new Activity { Type = ActivityTypes.ConversationUpdate, MembersAdded = new[] { new ChannelAccount("user") } })
            .AssertReply(activity =>
            {
                Assert.Contains("Welcome", activity.AsMessageActivity().Text);
            })
            .StartTestAsync();
    }

    [Fact]
    public async Task Handles_Order_Status_Intent()
    {
        var adapter = new TestAdapter();
        var bot = CreateBot();

        await adapter
            .Send("What's the status of my order 12345?")
            .AssertReply(activity =>
            {
                var text = activity.AsMessageActivity().Text;
                Assert.Contains("12345", text);
                Assert.Contains("status", text.ToLower());
            })
            .StartTestAsync();
    }
}

Conclusion

Azure Bot Service provides a comprehensive platform for building conversational AI. With OpenAI integration, multi-channel support, and robust SDK features, you can create intelligent bots that provide real value to users. The key is combining structured dialog flows with AI-powered natural language understanding.

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.