Bot Framework Adapter: sendConversationHistory Fails in Web Chat, Succeeds in Emulator.

Hessel Wellema 256 Reputation points
2024-11-20T17:38:33.2066667+00:00

This is a follow up question on this question about reopening a historic conversation

In line with the chatinterface of ChatGPT I would like to allow users of a webchat interface to reopen historic conversation.

What is working

  • Conversation metadata is stored in table storage
  • The transcripts are stored via middleware in blob storage
  • Users can start a new conversation in the web chat agent or select an old conversation
  • In the last case, a new conversation is started and the activities of the selected old conversationId are retrieved from transcriptblogstorage

-> These activities are send to the conversation via conversations.sendConversationHistory

The last step only works in the emulator. Not when the webchat agent is used.

What is not working In webchat, the activities sent via sendConversationHistory are not visible

When I add logging:

const result = await connector.conversations.sendConversationHistory(turnContext.activity.conversation.id, { activities: validActivities });
        debuggingMode && console.log('Conversation history sent successfully.');
        console.log(result);

When using the emulator I see something like this and the old the historic conversation is visible in the emulator chatwindow

Conversation history sent successfully.
{ id: 'Processed 7 of 7 activities successfully.' }

When using webchat I see this and nothing is visible in the webchat chatwindow

Conversation history sent successfully.
{}

I understand that webchat (or DIrectLine) will be more strict and the emulator is more forgiving. I am unable however to find any documentation about what is required and what not.

What did I already try

.1 Based on what I found on SO I tried multiple ways to structure the activities. Not with good result. .2 I tried to send the activities in Postman to try and figure out what the best way was.

Sending the activities to https://europe.directline.botframework.com/v3/directline/conversations/<conversationId/activities/history/ results in The requested resource does not support http method 'POST' Sending the activities to https://europe.directline.botframework.com/v3/directline/conversations/<conversationId/activities results in

{
    "error": {
        "code": "MissingProperty",
        "message": "Missing 'Activity.From' field"
    }
}

whatever I try with the from fields.

What do I need help with

I need to understand what the structure of the activities is that will be accepted and processed. When using sendConversationHistory in combination with webchat


I am using "botbuilder": "^4.21.0",

the connector is created like this:

const { BotFrameworkAdapter, ConversationState, MemoryStorage } = require('botbuilder');
const adapter = new BotFrameworkAdapter({
    appId: process.env.MicrosoftAppId,
    appPassword: process.env.MicrosoftAppPassword
});


This is a sample of the current set of activities that is sent. What is important (eg ids that are unique and incremental) is correct in my opinion


{
    "activities": [
        {
            "text": "Hello! Select a button.",
            "inputHint": "acceptingInput",
            "channelId": "directline",
            "locale": "en-US",
            "serviceUrl": "https://europe.directline.botframework.com/",
            "conversation": {
                "id": "<conversationid>"
            },
            "from": {
                "id": "<botid>",
                "name": "Bot",
                "role": "bot"
            },
            "recipient": {
                "id": "<userd>",
                "name": "User",
                "role": "user"
            },
            "type": "message",
            "id": "<conversationid>|0000001",
            "timestamp": "2024-11-20T16:40:25.396Z"
        },
        {
            "text": "Selected Button Text",
            "textFormat": "plain",
            "type": "message",
            "channelId": "directline",
            "from": {
                "id": "<userd>",
                "name": "User",
                "role": "user"
            },
            "locale": "en-US",
            "localTimestamp": "2024-11-20T14:05:02.000Z",
            "localTimezone": "Europe/Amsterdam",
            "attachments": [],
            "entities": [
                {
                    "requiresBotState": true,
                    "supportsListening": true,
                    "supportsTts": true,
                    "type": "ClientCapabilities"
                }
            ],
            "conversation": {
                "id": "<conversationid>"
            },
            "id": "<conversationid>|0000002",
            "recipient": {
                "id": "<botid>",
                "name": "Bot",
                "role": "bot"
            },
            "timestamp": "2024-11-20T16:40:25.396Z",
            "serviceUrl": "https://europe.directline.botframework.com/",
            "rawTimestamp": "2024-11-20T14:05:02.362Z",
            "rawLocalTimestamp": "2024-11-20T15:05:02+01:00",
            "callerId": "urn:botframework:azure"
        }
    ]
}

The code that retrieves the historic activities from transcript log storage and sends in to directline


async function loadAndSendConversationHistory(turnContext, transcriptStore, conversationId, conversationHistoryAccessor, userId) {

    // Retrieve saved conversation reference
    const savedConversationReference = await getConversationReference(userId, conversationId);

    if (!savedConversationReference) {
        console.error('No saved conversation reference found.');
        return;
    }

    // Retrieve transcript activities
    const rawActivities = [];
    let continuationToken;

    do {
        const page = await transcriptStore.getTranscriptActivities(turnContext.activity.channelId, conversationId, continuationToken);
        continuationToken = page.continuationToken;
        rawActivities.push(...page.items);
    } while (continuationToken);

    // Validate and filter activities
    const validActivities = rawActivities.filter(activity => {
        if (!validateActivity(activity)) {
            debuggingMode && console.log(`Invalid activity filtered out: ${JSON.stringify(activity)}`);
            return false;
        }
        // Remove suggestedActions for activities without text
        if (!activity.text && activity.suggestedActions) {
            delete activity.suggestedActions;
        }
        return true;
    });

    // Assign sequential IDs only to valid activities
    validActivities.forEach((activity, index) => {
        assignActivityDefaults(activity, conversationId, index + 1);
    });

    debuggingMode && console.log(`Activities after assigning IDs: ${JSON.stringify(validActivities, null, 2)}`);

    try {
        const connector = turnContext.adapter.createConnectorClient(turnContext.activity.serviceUrl);
        const conversations = await connector.conversations.getConversations();

        const result = await connector.conversations.sendConversationHistory(turnContext.activity.conversation.id, { activities: validActivities });
        debuggingMode && console.log('Conversation history sent successfully.');
        console.log(result);
    } catch (error) {
        console.error('Error sending activities:', error);
    }
}

// Helper function to validate activity structure
function validateActivity(activity) {
    const requiredFields = ["type", "id", "timestamp", "from", "recipient", "conversation", "serviceUrl", "text"];
    const hasRequiredFields = requiredFields.every(field => field in activity);

    if (!hasRequiredFields) {
        debuggingMode && console.log('Activity validation failed:', activity);
        return false;
    }

    // Additional checks for fields
    if (!activity.from.id || !activity.recipient.id || !activity.conversation.id) {
        debuggingMode && console.log('Activity has missing identifiers:', activity);
        return false;
    }

    return true;
}

// Helper function to assign default values and remove irrelevant fields
function assignActivityDefaults(activity, conversationId, sequenceNumber) {
    activity.id = `${conversationId}|${sequenceNumber.toString().padStart(7, '0')}`;
    activity.timestamp = new Date().toISOString(); // Use current timestamp
    activity.serviceUrl = "https://europe.directline.botframework.com/"; // Set service URL
    activity.channelId = "directline"; // Set channel ID

    // Default values for missing fields
    if (!activity.from.name) {
        activity.from.name = "User"; // Default to empty string
    }
    if (!activity.recipient.name) {
        activity.recipient.name = "Bot"; // Default recipient name
    }

    // Remove irrelevant fields
    delete activity.channelData;
    delete activity.replyToId;
}


Azure AI Bot Service
Azure AI Bot Service
An Azure service that provides an integrated environment for bot development.
863 questions
{count} votes

2 answers

Sort by: Most helpful
  1. Hessel Wellema 256 Reputation points
    2024-11-21T12:32:12.0266667+00:00

    Figured it out by using postman and the api

    The Bot framework adapter is just fine for sending historic activities.
    My silly mistake was that I was using the conversationId of the old conversation while sending activties to a new conversation with a new conversationId.

    1 person found this answer helpful.

  2. VasaviLankipalle-MSFT 18,151 Reputation points
    2024-11-21T16:09:26.11+00:00

    Hello @Hessel Wellema , I'm glad that you were able to resolve your issue and thank you for posting your solution so that others experiencing the same thing can easily reference this! Since the Microsoft Q&A community has a policy that "The question author cannot accept their own answer. They can only accept answers by others ", I'll repost your solution in case you'd like to "Accept " the answer.

    Issue: Bot Framework Adapter: sendConversationHistory Fails in Web Chat, Succeeds in Emulator.

    Solution:

    The Bot framework adapter is just fine for sending historic activities. Got the adapter to work. The initial code was correct except for one, used the conversationId of the old conversation while sending activities to a new conversation with a new conversationId.

    Thank you again for your time and patience throughout this issue.

    I hope this helps.

    Regards,

    Vasavi


    Please remember to "Accept Answer" if any answer/reply helped, so that others in the community facing similar issues can easily find the solution.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.