Botframework 聊天机器人中的空响应和跨会话问题

Botframework 聊天机器人中的空响应和跨会话问题,botframework,Botframework,我正在使用V4的bot框架。使用API调用,我试图获取数据并将其显示给用户,我定义了一个自定义方法从API中捕获数据并对其进行预处理,然后通过瀑布式对话框将其发送给用户,我使该方法异步,并使用wait where调用它 我面临的问题有两种情况- 当两个用户向一个实例发送问题时,其中一个响应捕获为null,而不是从API获取的值 我们使用提示卡来显示结果,当用户点击按钮时,会偶尔观察到交叉会话。 非常感谢您在这方面的帮助 定义用于进行API调用并获取数据的自定义方法: 公共静态异步任务 Ge

我正在使用V4的bot框架。使用API调用,我试图获取数据并将其显示给用户,我定义了一个自定义方法从API中捕获数据并对其进行预处理,然后通过瀑布式对话框将其发送给用户,我使该方法异步,并使用wait where调用它

我面临的问题有两种情况-

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

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

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


公共静态异步任务
GetEPCallsDoneSync(ConversationData ConversationData)
{
LogWriter.LogWrite(“信息:访问端点”);
字符串responseMessage=null;
尝试
{
conversationData.fulFillmentMap=等待
AnchorUtil.GetFulfillmentAsync(xxxxx);//从API调用获取数据的响应
if(conversationData.fulFillmentMap==null | | |(conversationData.fulFillmentMap.ContainsKey(“状态”)&&conversationData.fulFillmentMap[“状态”].ToString()!=“200”))
{
responseMessage=“抱歉,出现问题。请稍后再试!”;
}
其他的
{
conversationData.NLGresultMap=wait
AnchorUtil.GetNLGAsync(conversationData.fulFillmentMap,xxxx);//API调用以获取要显示的响应
if(conversationData.errorCaptureDict.ContainsKey(“实现中间错误”)| | conversationData.NLGresultMap.ContainsKey(“NLGError”))
{
responseMessage=“抱歉,出现问题:(请稍后再试!!!”;
}
其他的
{
responseMessage=FormatDataResponse(conversationData.NLGresultMap[“REPLY”].ToString());//响应消息
}
}
返回响应消息;
}
捕获(HttpRequestException e)
{
LogWriter.LogWrite(“错误:+e.Message”);
System.Console.WriteLine(“错误:+e.Message”);
返回null;
}
}
以及调用上述函数的dialog类的瀑布步骤:


专用异步任务DoProcessInvocationStep(WaterCallStepContext stepContext,CancellationToken CancellationToken)
{
conversationData.index=0;//用于其他用途的变量
conversationData.result=等待
GetEPcallsDoneAsync(conversationData);
Wait_conversationStateAccessor.SetAsync(stepContext.Context、conversationData、cancellationToken);
返回Wait-stepContext.NextAsync(cancellationToken);
}
ConversationData包含通过瀑布式对话框处理数据所需的变量,对象的值已在每个步骤中通过访问器设置和访问,如下所示:

在对话类中

公共类TopLevelDialog:ComponentDialog
{
专用只读IStatePropertyAccessor\u会话状态访问器;
会话数据会话数据;
公共顶级对话(会话状态会话状态)
:base(名称(TopLevelDialog))
{
_conversationStateAccessor=conversationState.CreateProperty(nameof(ConversationData));
AddDialog(新建文本提示(名称)(文本提示));
AddDialog(新建ChoicePrompt(名称)(ChoicePrompt));
AddDialog(新建ReviewSelectionDialog(会话状态));
AddDialog(新建EsSelectDialog());
AddDialog(新建WaterWallDialog)(名称(WaterWallDialog),新建WaterWallStep[]
{
StartSelectionStepAsync,
GetESResultStep,
doprocess步骤,
ResultStepAsync,
迭代步骤异步
}));
InitialDialogId=nameof(WaterWallDialog);
}
专用异步任务StartSelectStepAsync(WaterWallStepContext stepContext,CancellationToken CancellationToken)
{
conversationData=Wait_conversationStateAccessor.GetAsync(stepContext.Context,()=>new conversationData());
//功能代码
Wait_conversationStateAccessor.SetAsync(stepContext.Context、conversationData、cancellationToken);
返回Wait-stepContext.NextAsync(null,cancellationToken);
}
//其他对话框步骤
}

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

比如说,

User A
启动瀑布式对话框并完成一半。
conversationData
在这一点上是正确的,并且准确地表示了它应该做的事情

现在,
User B
启动一个对话框。在
startSelectsStepAsync
上,由于
conversationData=wait\u conversationStateAccessor.GetAsync(stepContext.Context,()=>new conversationData()),他们只需为每个人重置
conversationData
和所有用户共享相同的
conversationData
,因为
conversationData conversationData;

所以现在,当
用户A
继续他们的对话时,
对话数据将为null/空


状态应该如何Sav
// 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;