Edit

Build an Azure AI agent

This article walks you through creating a custom Azure AI agent and integrating it with Dynamics 365 Contact Center.

You learn how to:

  1. Build a basic agent that sends welcome messages

  2. Install and enable the Dynamics middleware

  3. Integrate your agent with Dynamics 365 Contact Center

  4. Implement escalation and end-conversation workflows

Build a basic agent

Start by creating a simple agent that can receive messages and send a welcome message when a conversation starts.

Create a project

Run the following commands:

dotnet new web -n MyDynamicsAgent
cd MyDynamicsAgent

Add packages

Install the required packages:

dotnet add package Microsoft.Agents.Builder
dotnet add package Microsoft.Agents.Core
dotnet add package Microsoft.Agents.Connector
dotnet add package Microsoft.Agents.Authentication
dotnet add package Microsoft.Agents.Hosting.AspNetCore

Create agent class

Create MyAgent.cs:

using Microsoft.Agents.Builder;
using Microsoft.Agents.Builder.App;
using Microsoft.Agents.Core.Models;

namespace MyDynamicsAgent;

public class MyAgent : AgentApplication
{
        public MyAgent(AgentApplicationOptions options) : base(options)
        {
                // Handle when users join the conversation.
                OnConversationUpdate(ConversationUpdateEvents.MembersAdded, OnMembersAddedAsync);

                // Handle incoming messages.
                OnActivity(ActivityTypes.Message, OnMessageActivityAsync);
        }

        private async Task OnMembersAddedAsync(
                ITurnContext turnContext,
                ITurnState turnState,
                CancellationToken cancellationToken)
        {
                var membersAdded = turnContext.Activity.MembersAdded;
                foreach (var member in membersAdded)
                {
                        // Don't respond to the agent joining.
                        if (member.Id != turnContext.Activity.Recipient.Id)
                        {
                                var welcomeMessage = Activity.CreateMessageActivity();
                                welcomeMessage.Text = "Welcome! How can I help you today?";
                                await turnContext.SendActivityAsync(welcomeMessage, cancellationToken);
                        }
                }
        }

        private async Task OnMessageActivityAsync(
                ITurnContext turnContext,
                ITurnState turnState,
                CancellationToken cancellationToken)
        {
                var userMessage = turnContext.Activity.Text;
                var reply = Activity.CreateMessageActivity();
                reply.Text = $"You said: {userMessage}";

                await turnContext.SendActivityAsync(reply, cancellationToken);
        }
}

Configure Program.cs

Update Program.cs:

using Microsoft.Agents.Hosting.AspNetCore;
using MyDynamicsAgent;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
        options.ListenLocalhost(3979); // HTTPS
        options.ListenLocalhost(3978); // HTTP
});

// Add Agent application.
builder.AddAgentApplicationOptions();
builder.AddAgent<MyAgent, CloudAdapter>();

// Add authentication.
builder.Services.AddControllers();
builder.Services.AddAgentAspNetAuthentication(builder.Configuration);

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseDefaultFiles()
     .UseStaticFiles()
     .UseRouting()
     .UseEndpoints(endpoints => endpoints.MapControllers());

app.Run();

Configure appsettings.json

Add the following settings:

{
    "TokenValidation": {
        "Enabled": false,
        "Audiences": [
            "{{ClientId}}"
        ],
        "TenantId": "{{TenantId}}"
    },
    "AgentApplication": {
        "StartTypingTimer": false,
        "RemoveRecipientMention": false,
        "NormalizeMentions": false
    },
    "Connections": {
        "ServiceConnection": {
            "Settings": {
                "AuthType": ""
            }
        }
    },
    "ConnectionsMap": [
        {
            "ServiceUrl": "*",
            "Connection": "ServiceConnection"
        }
    ],
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}

Run the following command: dotnet run

dotnet run

The agent runs on https://localhost:3979 and http://localhost:3978.

More samples

Install Dynamics 365 middleware

To enable your agent to work with Dynamics 365 Contact Center, you must install and configure the Dynamics 365 middleware.

Install the middleware package

dotnet add package Microsoft.Dynamics.AgentsSDK.Middleware

Create a custom adapter

Create AdapterWithErrorHandler.cs:

using Microsoft.Agents.Builder;
using Microsoft.Agents.Core.Models;
using Microsoft.Dynamics.AgentsSDK.Middleware.Core;
using Microsoft.Extensions.Logging;

namespace MyDynamicsAgent;

public class AdapterWithErrorHandler : CloudAdapter
{
    public AdapterWithErrorHandler(
        IConfiguration configuration,
        ILogger<CloudAdapter> logger,
        IServiceProvider serviceProvider)
        : base(configuration, logger, serviceProvider)
    {
        // Add Dynamics middleware.
        Use(new OmnichannelMiddleware());

        // Add error handling.
        OnTurnError = async (turnContext, exception) =>
        {
            logger.LogError(exception, "Exception caught in adapter");
            var errorMessage = Activity.CreateMessageActivity();
            errorMessage.Text = "Sorry, something went wrong. Please try again.";
            await turnContext.SendActivityAsync(errorMessage);
        };
    }
}

Update Program.cs to use custom adapter

Replace:

builder.AddAgent<MyAgent, CloudAdapter>();

With:

builder.AddAgent<MyAgent, AdapterWithErrorHandler>();

Handle Dynamics 365 events

Update MyAgent.cs to handle Dynamics-specific events:

public MyAgent(AgentApplicationOptions options) : base(options)
{
    OnConversationUpdate(ConversationUpdateEvents.MembersAdded, OnMembersAddedAsync);
    OnActivity(ActivityTypes.Message, OnMessageActivityAsync);

    // Add Dynamics event handling.
    OnActivity(ActivityTypes.Event, OnEventActivityAsync);
}

private async Task OnEventActivityAsync(
    ITurnContext turnContext,
    ITurnState turnState,
    CancellationToken cancellationToken)
{
    // Handle omnichannelSetContext event.
    if (turnContext.Activity.Name == "omnichannelSetContext")
    {
        var welcomeMessage = Activity.CreateMessageActivity();
        welcomeMessage.Text = "Welcome! How can I help you today?";

        // Bridge message so it is visible to both customer and agent.
        BridgeAgentMessage(welcomeMessage);
        await turnContext.SendActivityAsync(welcomeMessage, cancellationToken);
    }
}

private void BridgeAgentMessage(IActivity activity)
{
    SetChannelData(activity, "deliveryMode", "bridged");
}

Enable Agent Context for Azure Agents

Integrate with Dynamics 365

Now register your agent in Azure and connect it to the channel in Dynamics 365 Contact Center.

Learn more in Integrate Azure Agents.

Implement escalation and end conversation

Add the ability to transfer conversations to service representatives and gracefully end conversations.

Create helper class for Dynamics 365 operations

Create DynamicsAgentClient.cs:

using Microsoft.Agents.Builder;
using Microsoft.Agents.Core.Models;

namespace MyDynamicsAgent;

public static class DynamicsAgentClient
{
    public static async Task BridgeAndSendActivityAsync(
        ITurnContext turnContext,
        IActivity messageActivity,
        CancellationToken cancellationToken)
    {
        BridgeAgentMessage(messageActivity);
        await turnContext.SendActivityAsync(messageActivity, cancellationToken);
    }

    public static async Task EscalateConversationAsync(
        ITurnContext turnContext,
        CancellationToken cancellationToken)
    {
        // Customer-facing message.
        var customerMessage = Activity.CreateMessageActivity();
        customerMessage.Text = "Let me transfer you to a live agent. One moment please.";
        BridgeAgentMessage(customerMessage);

        // Agent-facing message with context.
        var agentMessage = Activity.CreateMessageActivity();
        agentMessage.Text = "Customer requested to speak with an agent.";
        AddEscalationContext(agentMessage);

        // Send both messages.
        await turnContext.SendActivitiesAsync(
            new[] { customerMessage, agentMessage },
            cancellationToken);
    }

    public static async Task EndConversationAsync(
        ITurnContext turnContext,
        CancellationToken cancellationToken)
    {
        // Customer-facing message.
        var customerMessage = Activity.CreateMessageActivity();
        customerMessage.Text = "Thank you for contacting us. Have a great day!";
        BridgeAgentMessage(customerMessage);

        // Agent-facing message.
        var agentMessage = Activity.CreateMessageActivity();
        agentMessage.Text = "Conversation ended by customer.";
        AddEndConversationContext(agentMessage);

        await turnContext.SendActivitiesAsync(
            new[] { customerMessage, agentMessage },
            cancellationToken);
    }

    private static void BridgeAgentMessage(IActivity activity)
    {
        SetChannelData(activity, "deliveryMode", "bridged");
    }

    private static void AddEscalationContext(IActivity activity)
    {
        var command = new
        {
                type = "Escalate",
                context = new Dictionary<string, object>()
        };

        SetChannelData(activity, "tags", JsonSerializer.Serialize(command));
    }

    private static void AddEndConversationContext(IActivity activity)
    {
        var context = new
        {
            tags = new { deliveryMode = "bridged" },
            command = new
            {
                type = "EndConversation",
                value = new { }
            }
        };

        activity.ChannelData = System.Text.Json.JsonSerializer.Serialize(context);
    }
}

Update your agent to use escalation and end conversation

Update MyAgent.cs:

private async Task OnMessageActivityAsync(
    ITurnContext turnContext,
    ITurnState turnState,
    CancellationToken cancellationToken)
{
    var userMessage = turnContext.Activity.Text?.ToLower() ?? "";

    // Check for escalation request.
    if (userMessage.Contains("agent") || userMessage.Contains("human"))
    {
        await DynamicsAgentClient.EscalateConversationAsync(turnContext, cancellationToken);
        return;
    }

    // Check for end conversation request.
    if (userMessage.Contains("bye") || userMessage.Contains("end chat"))
    {
        await DynamicsAgentClient.EndConversationAsync(turnContext, cancellationToken);
        return;
    }

    // Normal message handling.
    var reply = Activity.CreateMessageActivity();
    reply.Text = $"You said: {userMessage}";

    await DynamicsAgentClient.BridgeAndSendActivityAsync(turnContext, reply, cancellationToken);
}

Pass context variables to the representative

You can pass information that your agent collects during a conversation, such as a customer name, an order number, or a case ID, to the representative, notification templates, or routing by putting it in the context object of the escalation command. The system creates a context variable for each value. The context variables configured to be displayed appear in the representative's context panel when they accept the conversation. To use this capability, ensure that the corresponding context variables are configured on the workstream.

The system includes the context object in the same command that initiates the transfer or escalation. No separate call is required to set context. Each property in the context object is converted into a context variable. An example payload is as follows.

{
  "type": "Escalate",
  "context": {
    "visitorName": { "value": "John Doe", "isDisplayable": true },
    "orderId":     { "value": 12121,      "isDisplayable": true },
    "tier":        "gold"
  }
}

The isDisplayable field determines if the context value is displayed to the service rep in their desktop. It's an optional field. If you don't set it, the field doesn't display.

This code example captures a name during the conversation and includes it in escalation:

// Wrapper that makes a context variable visible to the representative.
public sealed class ContextVariable
{
    [JsonPropertyName("value")]         public object? Value { get; set; }
    [JsonPropertyName("isDisplayable")] public bool IsDisplayable { get; set; }

    public static ContextVariable Displayable(object? value) =>
        new() { Value = value, IsDisplayable = true };
}

public static async Task EscalateConversationAsync(
    ITurnContext turnContext,
    IDictionary<string, object>? contextVars,
    CancellationToken cancellationToken)
{
    // Customer-facing message.
    var customerMessage = Activity.CreateMessageActivity();
    customerMessage.Text = "Let me transfer you to a live agent. One moment please.";
    BridgeAgentMessage(customerMessage);

    // Representative-facing message carrying the context. Must be a message activity.
    var agentMessage = Activity.CreateMessageActivity();
    agentMessage.Text = "Customer requested an agent.";
    AddEscalationContext(agentMessage, contextVars);

    await turnContext.SendActivitiesAsync(
        new[] { customerMessage, agentMessage }, cancellationToken);
}

// Collect a value earlier in the conversation, for example "name: John Doe".
if (userMessage.StartsWith("name:", StringComparison.OrdinalIgnoreCase))
{
    // Read the ORIGINAL activity text so the customer's capitalization is preserved —
    // a lowercased copy would store "john doe".
    var rawText = turnContext.Activity.Text ?? string.Empty;
    var separator = rawText.IndexOf(':', StringComparison.Ordinal);
    var visitorName = separator >= 0 ? rawText[(separator + 1)..].Trim() : string.Empty;

    if (!string.IsNullOrWhiteSpace(visitorName))
    {
        // Store per conversation — use persisted storage in production.
        _contextVariables[turnContext.Activity.Conversation.Id] =
            new Dictionary<string, object>
            {
                ["visitorName"] = ContextVariable.Displayable(visitorName)
            };

        var reply = Activity.CreateMessageActivity();
        reply.Text = $"Thanks {visitorName}! I'll pass your name to the agent if you're transferred.";
        await DynamicsAgentClient.BridgeAndSendActivityAsync(turnContext, reply, cancellationToken);
        return;
    }
}

// Later, on escalation, hand the collected values over.
if (userMessage.Contains("agent"))
{
    _contextVariables.TryGetValue(turnContext.Activity.Conversation.Id, out var vars);
    await DynamicsAgentClient.EscalateConversationAsync(turnContext, vars, cancellationToken);
    return;
}

Test escalation

  1. Start a chat via live chat widget.
  2. Type "I need an agent".
  3. Verify the conversation transfers to a service representative queue.

Test end conversation

  1. Start a chat.
  2. Type "Bye".
  3. Verify the conversation closes gracefully.

Configure escalation and handoff