How to pass data from promptContext to Luis result? - node.js

I want to validate the data by checking if it is identified as an entity(lets say randomEntity) by Luis or not. If the data entered is recognized as randomEntity, then move ahead, otherwise use retry prompt.
But this does not work using promptContext-
const luisResult = await this.luisRecognizer.recognize(promptContext.context);
This is sample code-
class MainDialog extends ComponentDialog {
constructor(userState) {
super(MAIN_DIALOG);
this.userState = userState;
this.addDialog(new TextPrompt('firstPrompt', this.firstStepValidator));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.firstStep.bind(this),
]));
this.initialDialogId = WATERFALL_DIALOG;
let luisConfig = {
applicationId: 'myid',
endpointKey: 'myendpointkey',
endpoint: 'myendpoint',
};
this.luisRecognizer = new LuisRecognizer(
luisConfig,
{
includeAllIntents: true,
log: true,
staging: false
},
true
);
}
/**
* 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 firstStep(stepContext) {
const promptOptions = {
prompt: 'Please enter xyz id',
retryPrompt: 'Please enter a valid xyz id' };
return await stepContext.prompt('firstPrompt', promptOptions);
}
async firstStepValidator(promptContext) {
const luisResult = await this.luisRecognizer.recognize(promptContext.context); //THROWS ERROR: TypeError: Cannot read property 'recognize' of undefined
//do something with result
}

Related

Get value of choiceSet within adaptive card in Dialog

I have a step in my dialog where I send an Adaptive Card with a choice set. When the user selects a choice on the card and presses submit, how would I capture that value and continue with my dialog?
If there isn't an official way to do this, any suggestions about some hacks would be nice. Thanks ahead of time!
Here is my dialog class:
import { AdaptiveCards } from "#microsoft/adaptivecards-tools";
import {
ActivityPrompt,
ChoicePrompt,
ComponentDialog,
Dialog,
DialogSet,
DialogTurnStatus,
TextPrompt,
WaterfallDialog,
WaterfallStepContext,
} from "botbuilder-dialogs";
import { TicketProfile } from "../TicketProfile";
import rawCategoryListCard from "../adaptiveCards/categoryList.json";
import { ActivityTypes, CardFactory, MessageFactory } from "botbuilder";
const CHOICE_PROMPT = "CHOICE_PROMPT";
const HELP_PROMPT = "HELP_PROMPT";
const SUBJECT_PROMPT = "SUBJECT_PROMPT";
const CATEGORY_PROMPT = "CATEGORY_PROMPT";
const AREA_PATH_PROMPT = "AREA_PATH_PROMPT";
const REPORTED_PROBLEM_PROMPT = "REPORTED_PROBLEM_PROMPT";
const WATERFALL_DIALOG = "WATERFALL_DIALOG";
const CONFIRM_PROMPT = "CONFIRM_PROMPT";
export class TicketingPreferences extends ComponentDialog {
ticketProfile: any;
constructor(userState?: any) {
super("TicketingPreferences");
this.ticketProfile = userState.createProperty("TicketProfile");
this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
this.addDialog(new TextPrompt(SUBJECT_PROMPT));
this.addDialog(new ActivityPrompt(CATEGORY_PROMPT, this.inputValidator));
this.addDialog(new TextPrompt(AREA_PATH_PROMPT));
this.addDialog(new TextPrompt(REPORTED_PROBLEM_PROMPT));
this.addDialog(
new WaterfallDialog(WATERFALL_DIALOG, [
this.choiceStep.bind(this),
this.subjectStep.bind(this),
this.categoryStep.bind(this),
this.areaPathStep.bind(this),
this.reportedProblemStep.bind(this),
this.summaryStep.bind(this),
])
);
this.initialDialogId = WATERFALL_DIALOG;
}
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 choiceStep(stepContext: WaterfallStepContext) {
return await stepContext.prompt(CHOICE_PROMPT, {
prompt: "How can I help you?",
choices: ["Create Ticket", "Update Ticket", "Delete Ticket"],
});
}
async subjectStep(stepContext: any) {
stepContext.values.choice = stepContext.result.value;
return await stepContext.prompt(
SUBJECT_PROMPT,
"What is the ticket title?"
);
}
async categoryStep(stepContext: any) {
stepContext.values.subject = stepContext.result;
const exampleCatgory = {
category: [
{
title: "Category 1",
},
{
title: "Category 2",
},
{
title: "Category 3",
},
],
};
const card =
AdaptiveCards.declare(rawCategoryListCard).render(exampleCatgory);
const form = MessageFactory.attachment(CardFactory.adaptiveCard(card));
return await stepContext.prompt(CATEGORY_PROMPT, { prompt: form });
}
async areaPathStep(stepContext: any) {
if (stepContext.result) {
stepContext.values.category = stepContext.result;
return await stepContext.prompt(
AREA_PATH_PROMPT,
"What is the area path of your ticket?"
);
}
}
async inputValidator(promptContext) {
const userInputObject = promptContext.recognized.value.value;
return true;
}
}

createClickwrap returns 404 Not Found

When I call createClickwrap method, I got 404 error Not Found. If I run this method through Quickstart generated demo project, I don't get this error. However, if I run it through my project I get the error. If I debug the demo app and my app, the functions parameters are the same.
This is the code in my app:
docusign controller:
const docuSignService = require('./docusign_esign_service');
const demoDocumentsPath = path.resolve(__dirname, '../demo_documents');
const { createClickwrap } = require('./createClickWrap');
async getDocusignRecieptService() {
const authResponse = await docuSignService.authenticate();
if (authResponse) {
const docTermsPdf = 'Term_Of_Service.pdf';
const docFile = path.resolve(demoDocumentsPath, docTermsPdf);
const { basePath, accessToken, apiAccountId } = authResponse;
const { clickwrapId } = await createClickwrap({ docFile, basePath, accessToken, accountId: apiAccountId });
const res = await activateClickwrap({ clickwrapId, basePath, accessToken, accountId: apiAccountId });
console.log({ res });
}
}
docuSignService.js
const SCOPES = ['signature', 'impersonation', 'openid', 'click.manage', 'click.send'];
const fs = require('fs');
const docusign = require('docusign-esign');
class DocusingService {
async authenticate() {
const jwtLifeSec = 10 * 60, // requested lifetime for the JWT is 10 min
dsApi = new docusign.ApiClient();
dsApi.setOAuthBasePath(process.env.dsOauthServer.replace('https://', '')); // it should be domain only.
let rsaKey = fs.readFileSync(process.env.privateKeyLocation);
try {
const results = await dsApi.requestJWTUserToken(
process.env.dsJWTClientId,
process.env.impersonatedUserGuid,
SCOPES,
rsaKey,
jwtLifeSec
);
const accessToken = results.body.access_token;
// get user info
const userInfoResults = await dsApi.getUserInfo(accessToken);
// use the default account
let userInfo = userInfoResults.accounts.find((account) => account.isDefault === 'true');
return {
accessToken: results.body.access_token,
apiAccountId: userInfo.accountId,
basePath: `${userInfo.baseUri}/restapi`
};
} catch (e) {
let body = e.response && e.response.body;
// Determine the source of the error
if (body) {
// The user needs to grant consent
if (body.error && body.error === 'consent_required') {
if (this.getConsent()) {
return this.authenticate();
}
} else {
// Consent has been granted. Show status code for DocuSign API error
this._debug_log(`\nAPI problem: Status code ${e.response.status}, message body:
${JSON.stringify(body, null, 4)}\n\n`);
}
}
}
}
getConsent() {
var urlScopes = SCOPES.join('+');
// Construct consent URL
var redirectUri = 'https://developers.docusign.com/platform/auth/consent';
var consentUrl =
`${process.env.dsOauthServer}/oauth/auth?response_type=code&` +
`scope=${urlScopes}&client_id=${process.env.dsJWTClientId}&` +
`redirect_uri=${redirectUri}`;
throw new Error(`Open the following URL in your browser to grant consent to the application: ${consentUrl}`);
}
getArgs(apiAccountId, accessToken, basePath, signerEmail, signerName, id, agreementData, redirect_uri) {
const envelopeArgs = {
signerEmail: signerEmail,
signerName: signerName,
status: 'sent',
signerClientId: id,
dsReturnUrl: redirect_uri,
agreement: agreementData
};
const args = {
accessToken: accessToken,
basePath: basePath,
accountId: apiAccountId,
envelopeArgs: envelopeArgs
};
return args;
}
}
module.exports = new DocusingService();
createClickWrap.js
const createClickwrap = async ({ docFile, clickwrapName = 'clickwrapName', basePath, accessToken, accountId }) => {
// Step 3. Construct the request Body
// Create display settings model
const displaySettings = docusignClick.DisplaySettings.constructFromObject({
consentButtonText: 'I Agree',
displayName: 'Terms of Service',
downloadable: true,
format: 'modal',
hasAccept: true,
mustRead: true,
requireAccept: true,
documentDisplay: 'document'
});
// Create document model
// Read and encode file. Put encoded value to Document entity.
// The reads could raise an exception if the file is not available!
const documentPdfExample = fs.readFileSync(docFile);
const encodedExampleDocument = Buffer.from(documentPdfExample).toString('base64');
const document = docusignClick.Document.constructFromObject({
documentBase64: encodedExampleDocument,
documentName: 'Terms of Service',
fileExtension: 'pdf',
order: 0
});
// Create clickwrapRequest model
const clickwrapRequest = docusignClick.ClickwrapRequest.constructFromObject({
displaySettings,
documents: [document],
name: clickwrapName,
requireReacceptance: true
});
// Step 4. Call the Click API
const dsApiClient = new docusignClick.ApiClient();
dsApiClient.setBasePath(basePath);
dsApiClient.addDefaultHeader('Authorization', 'Bearer ' + accessToken);
const accountApi = new docusignClick.AccountsApi(dsApiClient);
// Create a clickwrap
let result = null;
try {
result = await accountApi.createClickwrap(accountId, {
clickwrapRequest
});
} catch (e) {
debugger;
console.log(e);
}
debugger;
console.log(`Clickwrap was created. ClickwrapId ${result.clickwrapId}`);
return result;
};
module.exports = { createClickwrap };
Parameters look like this in the demo app and it works:
and these are the parameters in my app:
The first parameter accountId is the same. Why I am getting this issue in my app if function gets the same parameters?
"Error: Not Found
at Request.callback (/Users/and/test/node_modules/docusign-click/node_modules/superagent/lib/node/index.js:696:15)
at IncomingMessage.<anonymous> (/Users/and/test/node_modules/docusign-click/node_modules/superagent/lib/node/index.js:906:18)
at IncomingMessage.emit (node:events:539:35)
at IncomingMessage.emit (node:domain:475:12)
at endReadableNT (node:internal/streams/readable:1345:12)
at processTicksAndRejections (node:internal/process/task_queues:83:21)"
Thanks to pointing out in the comments, when I changed basePath ${userInfo.baseUri}/restapi to ${userInfo.baseUri}/clickapi, it works now.

Get a list of Azure Function Apps in Node JS?

With the az cli you can use the az functionapp list method to get an array of all your Function Apps and their associated meta data (i.e. appServicePlanId, defaultHostName, lastModifiedTimeUtc...etc)
I've spent the last hour looking everywhere, including in the azure-sdk-for-js src but I can't seem to find the right NodeJS SDK to simply list all of my Azure function apps. I would like to use a Node SDK for this, NOT manually construct an HTTP request. Any ideas where this functionality lives?
To be super clear, I'm looking to do something like this:
await new AzureFunctionClient(credentials,subscriptionId).functionApps.listAll()
You can use the REST API to list functions
Listing functions
get
/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{functionapp}/functions?api-version=2015-08-01
Super annoying but it looks like for some reason there is not a NodeJS SDK for basic CRUD actions with Azure Functions (emphasis on the R in CRUD) to do things like simply list all of your function apps or functions. I had to write this by hand:
import { URLSearchParams } from 'url'
import * as Sentry from '#sentry/node'
import fetch from 'node-fetch'
import * as head from 'lodash/head'
import { ResourceManagementClient } from '#azure/arm-resources'
const functionapp = 'functionapp'
const getAadToken = async ({ clientId, clientSecret, tenantId }) => {
const body = new URLSearchParams()
body.append('grant_type', 'client_credentials')
body.append('resource', 'https://management.azure.com/')
body.append('client_id', clientId)
body.append('client_secret', clientSecret)
return fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
})
}
const listResourceGroupsForSubscription = async ({
returnIds = false,
credentials,
subscriptionId,
}) => {
const resourceGroups = []
const client = new ResourceManagementClient(credentials, subscriptionId)
const getAllResourceGroupsData = async (nextLinkToken = '') => {
let rsgs
let nextLink
/**
* Then this is the nth time this has been called
*/
try {
if (nextLinkToken) {
rsgs = await client.resourceGroups.listNext(nextLinkToken)
nextLink = rsgs.nextLink
} else {
/**
* Then this is the first time this has been called
*/
rsgs = await client.resourceGroups.list()
nextLink = rsgs.nextLink
}
} catch (e) {
logger.error(e)
Sentry.captureException(e)
}
/**
* Add the rsgs to our resourceGroups array so we can aggregate as we go
*/
resourceGroups.push(...rsgs)
if (nextLink) {
/**
* If there is another page of data, get it
*/
await getAllResourceGroupsData(nextLink)
}
return rsgs
}
await Promise.all(await getAllResourceGroupsData())
return returnIds ? resourceGroups.map(({ id }) => id) : resourceGroups
}
const createUriForMsRestApiRequest = ({ id }) =>
[`https://management.azure.com`, id, '?api-version=2020-06-01'].join('')
const getDataFromMsRestApi = async ({
authToken,
initialUrl,
kindSelector = '',
}) => {
const allData = []
const getAllData = async (nextLinkToken = '') => {
try {
const url = nextLinkToken || initialUrl
const response = await fetch(url, {
headers: { Authorization: `Bearer ${authToken}` },
})
const { value: data, nextLink } = await response.json()
allData.push(
...(kindSelector
? data.filter(({ kind }) => kind.includes(functionapp))
: data)
)
if (nextLink) {
logger.info(lt.fetchingMoreRestApiData)
await getAllData(nextLink)
}
logger.info(lt.fetchedDataFromRestApi(url))
return data
} catch (e) {
logger.error(e)
Sentry.captureException(e)
}
}
await Promise.all(await getAllData())
return allData
}
const getAzureFunctionAppsData = async ({
credentials,
subscriptionId,
clientId,
clientSecret,
tenantId,
}) => {
/**
* In order to get a list of all the function apps and functions we need to create a uri that looks like this:
* /subscriptions/${subscriptionId}/resourceGroups/${resourceGroupId}/providers/Microsoft.Web/sites/${functionAppId}/functions
* And since Azure does not provide this functionaliy in any of their SDKS, we have to fetch a lot of data ourselves...
*/
try {
/**
* Step 1, get all the resource groups (we just need the ids)
*/
const resourceGroupsIds = await listResourceGroupsForSubscription({
returnIds: true,
credentials,
subscriptionId,
})
/**
* Step 2, Grab the bearer token for auth with the REST API
*/
const response = await getAadToken({
clientId,
clientSecret,
tenantId,
})
const { access_token: authToken } = await response.json()
/**
* Step 3, for each resource group get all the sites grab the Function Apps
*/
const functionApps = (
await Promise.all(
(resourceGroupsIds || []).map(
async resourceGroupId =>
await getDataFromMsRestApi({
authToken,
kindSelector: functionapp,
initialUrl: createUriForMsRestApiRequest({
id: `${resourceGroupId}/providers/Microsoft.Web/sites`,
}),
})
)
)
).flat()
/**
* Step 4, now we can finally grab the actual functions for each function app
*/
const azureFunctions = (
await Promise.all(
(functionApps || []).map(
async ({ id: functionAppId }) =>
await getDataFromMsRestApi({
authToken,
initialUrl: createUriForMsRestApiRequest({
id: `${functionAppId}/functions`,
}),
})
)
)
).flat()
return (functionApps || []).map(({ name, ...data }) => ({
name,
...data,
functions: azureFunctions.filter(
({ name: funcName }) => head(funcName.split('/')) === name
),
}))
} catch (e) {
logger.error(e)
Sentry.captureException(e)
}
}

Azure Bot Framework V4 (NodeJS) - LUIS recognizer returns error?

Using Azure Bot Framework and LUIS.ai to recognize user intent. Performing a get request to the endpoint with the text returns the json object I am expecting, however using the built-in Luis Recognizer I receive the following error: 'Cannot read property 'get' of undefined'. From the documentation here: https://learn.microsoft.com/en-us/azure/cognitive-services/luis/luis-nodejs-tutorial-bf-v4 this appears to be the proper configuration so I am not sure what is going awry. Any ideas?
const { ComponentDialog, DialogSet, DialogTurnStatus, WaterfallDialog, ChoicePrompt, TextPrompt } = require('botbuilder-dialogs');
const { TopLevelDialog, TOP_LEVEL_DIALOG } = require('./topLevelDialog');
const { LuisRecognizer, QnAMaker } = require('botbuilder-ai');
const axios = require('axios');
const MAIN_DIALOG = 'MAIN_DIALOG';
const WATERFALL_DIALOG = 'WATERFALL_DIALOG';
const USER_PROFILE_PROPERTY = 'USER_PROFILE_PROPERTY';
const CHOICE_PROMPT = 'CHOICE_PROMPT';
const TEXT_PROMPT = 'TEXT_PROMPT';
class MainDialog extends ComponentDialog {
constructor(userState) {
super(MAIN_DIALOG);
this.userState = userState;
this.userProfileAccessor = userState.createProperty(USER_PROFILE_PROPERTY);
this.addDialog(new TextPrompt(TEXT_PROMPT));
this.addDialog(new TopLevelDialog());
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.initialStep.bind(this),
this.askIfFinishedStep.bind(this),
this.finalStep.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
let luisConfig = {
applicationId: '',
endpointKey: '',
endpoint: '',
};
this.Luis = new LuisRecognizer(
luisConfig,
{
includeAllIntents: true,
log: true,
staging: false
},
true
);
}
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 initialStep(stepContext) {
let luisAnalysis = await this.Luis.recognize(stepContext);
let queryString = encodeURIComponent(stepContext.context._activity.text);
/*
Ignore this if statement - only in use with the get request
*/
if(luisResponse.data.topScoringIntent.intent === 'TrainingExpiry' && luisResponse.data.topScoringIntent.score > .75)
{
return await stepContext.beginDialog(TOP_LEVEL_DIALOG);
}
else
{
await stepContext.context.sendActivity("I'm sorry, that is not supported at this time or a high enough intent was not acknowledged.");
await stepContext.context.sendActivity("Top intent: " + luisResponse.data.topScoringIntent.intent + " Score: " + luisResponse.data.topScoringIntent.score);
return await stepContext.next();
}
}
async askIfFinishedStep(stepContext) {
const promptOptions = { prompt: 'Is there anything else I can assist you with?' };
return await stepContext.prompt(TEXT_PROMPT, promptOptions);
}
async finalStep(stepContext) {
if(stepContext.context._activity.text.toLowerCase() === 'no')
{
await stepContext.context.sendActivity("Good bye");
return await stepContext.endDialog();
}
else
{
return await stepContext.beginDialog(MAIN_DIALOG);
}
}
}
module.exports.MainDialog = MainDialog;
module.exports.MAIN_DIALOG = MAIN_DIALOG;
Note: The issue was in my parameter being passed to the recognizer, as #billoverton pointed out. The solution is to pass stepContext.context.
Looking at luisRecognizer.js from the botbuilder-ai module, the error is because the recognizer is expecting a turnContext (with turnState property) and you are sending a stepContext. turnState doesn't exist on stepContext, thus the get property is failing and causing your error. If you send stepContext.context instead, that will fix the issue, i.e. let luisAnalysis = await this.Luis.recognize(stepContext.context);

Bot framework continueDialog not popping off the stack

I create chatbot by Bot Framework and use LINE API Developers on node.js
Problem on my work was continueDialog not continue waterfallDialog next step.
and the stack didn't change when it's done with continueDialog
before continueDialog
stack1 [ { id: 'xxxDialog',
state: { options: {}, values: [Object], stepIndex: 0 } } ]
after continueDialog
stack2 [ { id: 'xxxDialog',
state: { options: {}, values: [Object], stepIndex: 0 } } ]
on index.js
server.post('/api/line',jsonParser, async (req, res)=> {
const conversationReference = {
type: "message",
text: "Hello world" ,
channelData: { clientActivityID: "id-xxxx" },
channelId: 'api/line/id-xxxx',
recipient:
{ id: lineID`,
name: 'line',
role: 'botLine' },
serviceUrl: 'https://localhost:3978' ,
from:
{ id: lineId`,
name: 'User',
role: 'user' },
conversation: { id: lineId },
};
const context = await adapter.createContext(conversationReference);
await bot.onTurn(context);
});
on bot.js
class Bot extends ActivityHandler {
/**
*
* #param {ConversationState} conversationState
* #param {UserState} userState
* #param {Dialog} dialog
*/
constructor(conversationState, userState) {
super();
this.conversationState = conversationState;
this.userState = userState;
this.dialogStateAccessor = conversationState.createProperty('dialogStateAccessor');
this.dialogAccessor= conversationState.createProperty('testAccessor');
this.dialog = new DialogSet(this.dialogStateAccessor);
this.dialog.add(new WaterfallDialog('testDialog', [
this.step1.bind(this),
this.step2.bind(this)
]));
}
async step1(stepContext){
linesent("step 1") ;
return {status : DialogTurnStatus.waiting} ;
}
async step2(stepContext){
linesent("step 2") ;
return await stepContext.endDialog();
}
async onTurn(turnContext) {
const reservation = await this.dialogAccessor.get(turnContext, null);
// Generate a dialog context for our dialog set.
const dc = await this.dialog.createContext(turnContext);
if (!dc.activeDialog){
// If there is no active dialog, check whether we have a reservation yet.
if (!reservation) {
// If not, start the dialog.
await dc.beginDialog(`testDialog`);
}
}
else {
//Continue the dialog.
const dialogTurnResult = await dc.continueDialog();
}
return await this.conversationState.saveChanges(turnContext, false);
}
but it did not show any error.
Any help would be appreciated.
found the continueDialog method be
async continueDialog(dc) {
// Don't do anything for non-message activities
if (dc.context.activity.type !== botbuilder_core_1.ActivityTypes.Message) {
return dialog_1.Dialog.EndOfTurn;
}
// Run next step with the message text as the result.
return await this.resumeDialog(dc, dialog_1.DialogReason.continueCalled, dc.context.activity.text);
}
the bot always do if condition.
Changed continueDialog to resumeDialog on your activeDialog id the waterfall will working on next step.

Resources