聊天机器人中无响应和跨会话问题

时间:2020-02-19 15:39:07

标签: botframework

我正在使用V4的机器人框架。使用API​​调用,我试图获取数据并将其显示给用户,并定义了一个自定义方法以从API捕获数据并对其进行预处理,然后再通过瀑布对话框将其发送给用户,并且使该方法异步在等待调用的地方使用。

在2种情况下,我面临的问题-

  • 当两个用户在一个实例上发送问题时,其中一个响应被捕获为null而不是从API获取的值。

  • 我们使用提示卡来显示结果,并且当用户单击按钮时,会偶尔观察到交叉会话。 非常感谢您在此方面的帮助。

定义为进行API调用并获取数据的自定义方法:

$config['base_url'] = 'http://localhost/helloby/';

以及调用上述函数的对话框类的瀑布步骤:


    public static async Task<string> 
    GetEPcallsDoneAsync(ConversationData conversationData)
        {
            LogWriter.LogWrite("Info: Acessing end-points");
            string responseMessage = null;
            try
            {
                conversationData.fulFillmentMap = await 
                AnchorUtil.GetFulfillmentAsync(xxxxx);//response from API call to get data

                if (conversationData.fulFillmentMap == null || (conversationData.fulFillmentMap.ContainsKey("status") && conversationData.fulFillmentMap["status"].ToString() != "200"))
                {
                    responseMessage = "Sorry, something went wrong. Please try again later!";
                }
                else
                {
                    conversationData.NLGresultMap = await 
            AnchorUtil.GetNLGAsync(conversationData.fulFillmentMap ,xxxx);//API call to get response to be displayed


                    if (conversationData.errorCaptureDict.ContainsKey("fulfillmentError") || conversationData.NLGresultMap.ContainsKey("NLGError"))
                    {
                        responseMessage = "Sorry, something went wrong:( Please try again later!!!";
                    }
                    else
                    {
                        responseMessage = FormatDataResponse(conversationData.NLGresultMap["REPLY"].ToString()); //response message
                    }
                }
                return responseMessage;
            }
            catch (HttpRequestException e)
            {
                LogWriter.LogWrite("Error: " + e.Message);
                System.Console.WriteLine("Error: " + e.Message);
                return null;
            }
        }

ConversationData包含通过瀑布对话框处理数据所需的变量,已在每个步骤中按如下所述通过访问器设置了对象的值并对其进行了访问:

在对话框类中,


     private async Task<DialogTurnResult> DoProcessInvocationStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
                conversationData.index = 0; //var for some other purpose
                conversationData.result = await 
    AnchorUtil.GetEPcallsDoneAsync(conversationData);
                await _conversationStateAccessor.SetAsync(stepContext.Context, conversationData, cancellationToken);

                return await stepContext.NextAsync(cancellationToken);
     }

1 个答案:

答案 0 :(得分:0)

您的两个问题都可能源于同一件事。您不能将conversationData声明为类级属性。您会遇到这样的并发问题,因为每个用户都会覆盖其他每个用户的conversationData。您必须在每个步骤函数中重新声明conversationData

例如,

User A启动瀑布对话框,并完成一半。 conversationData在这一点上是正确的,并且恰好表示它应该是什么。

现在User B将启动一个对话框。在StartSelectionStepAsync,由于conversationData,他们只是为所有人重置了conversationData = await _conversationStateAccessor.GetAsync(stepContext.Context, () => new ConversationData());,由于conversationData,所有用户都共享相同的ConversationData conversationData;

因此,现在User A继续其对话时,conversationData将为空/空。


如何保存状态

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const { Channels, MessageFactory } = require('botbuilder');
const {
    AttachmentPrompt,
    ChoiceFactory,
    ChoicePrompt,
    ComponentDialog,
    ConfirmPrompt,
    DialogSet,
    DialogTurnStatus,
    NumberPrompt,
    TextPrompt,
    WaterfallDialog
} = require('botbuilder-dialogs');
const { UserProfile } = require('../userProfile');

const ATTACHMENT_PROMPT = 'ATTACHMENT_PROMPT';
const CHOICE_PROMPT = 'CHOICE_PROMPT';
const CONFIRM_PROMPT = 'CONFIRM_PROMPT';
const NAME_PROMPT = 'NAME_PROMPT';
const NUMBER_PROMPT = 'NUMBER_PROMPT';
const USER_PROFILE = 'USER_PROFILE';
const WATERFALL_DIALOG = 'WATERFALL_DIALOG';

/**
 * This is a "normal" dialog, where userState is stored properly using the accessor, this.userProfile.
 * In this dialog example, we create the userProfile using the accessor in the first step, transportStep.
 * We then pass prompt results through the remaining steps using step.values.
 * In the final step, summaryStep, we save the userProfile using the accessor.
 */
class UserProfileDialogNormal extends ComponentDialog {
    constructor(userState) {
        super('userProfileDialogNormal');

        this.userProfileAccessor = userState.createProperty(USER_PROFILE);

        this.addDialog(new TextPrompt(NAME_PROMPT));
        this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
        this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
        this.addDialog(new NumberPrompt(NUMBER_PROMPT, this.agePromptValidator));
        this.addDialog(new AttachmentPrompt(ATTACHMENT_PROMPT, this.picturePromptValidator));

        this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
            this.transportStep.bind(this),
            this.nameStep.bind(this),
            this.nameConfirmStep.bind(this),
            this.ageStep.bind(this),
            this.pictureStep.bind(this),
            this.confirmStep.bind(this),
            this.saveStep.bind(this)
        ]));

        this.initialDialogId = WATERFALL_DIALOG;
    }

    /**
     * The run method handles the incoming activity (in the form of a TurnContext) and passes it through the dialog system.
     * If no dialog is active, it will start the default dialog.
     * @param {*} turnContext
     * @param {*} accessor
     */
    async run(turnContext, accessor) {
        const dialogSet = new DialogSet(accessor);
        dialogSet.add(this);

        const dialogContext = await dialogSet.createContext(turnContext);
        const results = await dialogContext.continueDialog();
        if (results.status === DialogTurnStatus.empty) {
            await dialogContext.beginDialog(this.id);
        }
    }

    async transportStep(step) {
        // Get the userProfile if it exists, or create a new one if it doesn't.
        const userProfile = await this.userProfileAccessor.get(step.context, new UserProfile());

        // Pass the userProfile through step.values.
        // This makes it so we don't have to call this.userProfileAccessor.get() in every step.
        step.values.userProfile = userProfile;

        // Skip this step if we already have the user's transport.
        if (userProfile.transport) {
            // ChoicePrompt results will show in the next step with step.result.value.
            // Since we don't need to prompt, we can pass the ChoicePrompt result manually.
            return await step.next({ value: userProfile.transport });
        }

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        // Running a prompt here means the next WaterfallStep will be run when the user's response is received.
        return await step.prompt(CHOICE_PROMPT, {
            prompt: 'Please enter your mode of transport.',
            choices: ChoiceFactory.toChoices(['Car', 'Bus', 'Bicycle'])
        });
    }

    async nameStep(step) {
        // Retrieve the userProfile from step.values.
        const userProfile = step.values.userProfile;
        // Set the transport property of the userProfile.
        userProfile.transport = step.result.value;

        // Pass the userProfile through step.values.
        // This makes it so we don't have to call this.userProfileAccessor.get() in every step.
        step.values.userProfile = userProfile;

        // Skip the prompt if we already have the user's name.
        if (userProfile.name) {
            // We pass in a skipped bool so we know whether or not to send messages in the next step.
            return await step.next({ value: userProfile.name, skipped: true });
        }

        return await step.prompt(NAME_PROMPT, 'Please enter your name.');
    }

    async nameConfirmStep(step) {
        // Retrieve the userProfile from step.values and set the name property
        const userProfile = step.values.userProfile;

        // If userState is working correctly, we'll have userProfile.transport from the previous step.
        if (!userProfile || !userProfile.transport) {
            throw new Error(`transport property does not exist in userProfile.\nuserProfile:\n ${ JSON.stringify(userProfile) }`);
        }
        // Text prompt results normally end up in step.result, but if we skipped the prompt, it will be in step.result.value.
        userProfile.name = step.result.value || step.result;
        // step.values.userProfile.name is already set by reference, so there's no need to set it again to pass it to the next step.

        // We can send messages to the user at any point in the WaterfallStep. Only do this if we didn't skip the prompt.
        if (!step.result.skipped) {
            await step.context.sendActivity(`Thanks ${ step.result }.`);
        }

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        // Skip the prompt if we already have the user's age.
        if (userProfile.age) {
            return await step.next('yes');
        }
        return await step.prompt(CONFIRM_PROMPT, 'Do you want to give your age?', ['yes', 'no']);
    }

    async ageStep(step) {
        // Retrieve the userProfile from step.values
        const userProfile = step.values.userProfile;

        // If userState is working correctly, we'll have userProfile.name from the previous step.
        if (!userProfile || !userProfile.name) {
            throw new Error(`name property does not exist in userProfile.\nuserProfile:\n ${ JSON.stringify(userProfile) }`);
        }

        // Skip the prompt if we already have the user's age.
        if (userProfile.age) {
            // We pass in a skipped bool so we know whether or not to send messages in the next step.
            return await step.next({ value: userProfile.age, skipped: true });
        }

        if (step.result) {
            // User said "yes" so we will be prompting for the age.
            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
            const promptOptions = { prompt: 'Please enter your age.', retryPrompt: 'The value entered must be greater than 0 and less than 150.' };

            return await step.prompt(NUMBER_PROMPT, promptOptions);
        } else {
            // User said "no" so we will skip the next step. Give -1 as the age.
            return await step.next(-1);
        }
    }

    async pictureStep(step) {
        // Retrieve the userProfile from step.values and set the age property
        const userProfile = step.values.userProfile;
        // We didn't set any additional properties on userProfile in the previous step, so no need to check for them here.

        // Confirm prompt results normally end up in step.result, but if we skipped the prompt, it will be in step.result.value.
        userProfile.age = step.result.value || step.result;
        // step.values.userProfile.age is already set by reference, so there's no need to set it again to pass it to the next step.

        if (!step.result.skipped) {
            const msg = userProfile.age === -1 ? 'No age given.' : `I have your age as ${ userProfile.age }.`;

            // We can send messages to the user at any point in the WaterfallStep. Only send it if we didn't skip the prompt.
            await step.context.sendActivity(msg);
        }

        // Skip the prompt if we already have the user's picture.
        if (userProfile.picture) {
            return await step.next(userProfile.picture);
        }

        if (step.context.activity.channelId === Channels.msteams) {
            // This attachment prompt example is not designed to work for Teams attachments, so skip it in this case
            await step.context.sendActivity('Skipping attachment prompt in Teams channel...');
            return await step.next(undefined);
        } else {
            // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
            var promptOptions = {
                prompt: 'Please attach a profile picture (or type any message to skip).',
                retryPrompt: 'The attachment must be a jpeg/png image file.'
            };

            return await step.prompt(ATTACHMENT_PROMPT, promptOptions);
        }
    }

    async confirmStep(step) {
        // Retrieve the userProfile from step.values and set the picture property
        const userProfile = step.values.userProfile;
        // If userState is working correctly, we'll have userProfile.age from the previous step.
        if (!userProfile || !userProfile.age) {
            throw new Error(`age property does not exist in userProfile.\nuserProfile:\n ${ JSON.stringify(userProfile) }`);
        }
        userProfile.picture = (step.result && typeof step.result === 'object' && step.result[0]) || 'no picture provided';
        // step.values.userProfile.picture is already set by reference, so there's no need to set it again to pass it to the next step.

        let msg = `I have your mode of transport as ${ userProfile.transport } and your name as ${ userProfile.name }`;
        if (userProfile.age !== -1) {
            msg += ` and your age as ${ userProfile.age }`;
        }

        msg += '.';
        await step.context.sendActivity(msg);
        if (userProfile.picture && userProfile.picture !== 'no picture provided') {
            try {
                await step.context.sendActivity(MessageFactory.attachment(userProfile.picture, 'This is your profile picture.'));
            } catch (err) {
                await step.context.sendActivity('A profile picture was saved but could not be displayed here.');
            }
        }

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        return await step.prompt(CONFIRM_PROMPT, { prompt: 'Would you like me to save this information?' });
    }

    async saveStep(step) {
        if (step.result) {
            // Get the current profile object from user state.
            const userProfile = step.values.userProfile;

            // Save the userProfile to userState.
            await this.userProfileAccessor.set(step.context, userProfile);

            await step.context.sendActivity('User Profile Saved.');
        } else {
            // Ensure the userProfile is cleared
            await this.userProfileAccessor.set(step.context, {});
            await step.context.sendActivity('Thanks. Your profile will not be kept.');
        }

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is the end.
        return await step.endDialog();
    }

    async agePromptValidator(promptContext) {
        // This condition is our validation rule. You can also change the value at this point.
        return promptContext.recognized.succeeded && promptContext.recognized.value > 0 && promptContext.recognized.value < 150;
    }

    async picturePromptValidator(promptContext) {
        if (promptContext.recognized.succeeded) {
            var attachments = promptContext.recognized.value;
            var validImages = [];

            attachments.forEach(attachment => {
                if (attachment.contentType === 'image/jpeg' || attachment.contentType === 'image/png') {
                    validImages.push(attachment);
                }
            });

            promptContext.recognized.value = validImages;

            // If none of the attachments are valid images, the retry prompt should be sent.
            return !!validImages.length;
        } else {
            await promptContext.context.sendActivity('No attachments received. Proceeding without a profile picture...');

            // We can return true from a validator function even if Recognized.Succeeded is false.
            return true;
        }
    }
}

module.exports.UserProfileDialogNormal = UserProfileDialogNormal;