I've been trying to figure out an issue with an amazon lex chatbot I've been building all day. The node.js lambda keeps giving the same errors and for the life of me I can't figure out why. The chatbot is called BestiaryProject, the Intent is MonsterSearch, and the two slots are monsterType and monsterName.
The error I get is as follows:
"a9fba2d2-ec22-4092-b332-53ae16acb345 ERROR Invoke Error {"errorType":"Error","errorMessage":"Intent with name undefined not supported","stack":["Error: Intent with name undefined not supported"," at dispatch (/var/task/index.js:168:11)"," at Runtime.exports.handler (/var/task/index.js:189:9)"," at Runtime.handleOnce (file:///var/runtime/index.mjs:548:29)"]}"
here is the code:
function elicitSlot(sessionAttributes, intentName, slots, slotToElicit, message) {
return {
sessionAttributes,
dialogAction: {
type: 'ElicitSlot',
intentName,
slots,
slotToElicit,
message,
},
};
}
function close(sessionAttributes, fulfillmentState, message) {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState,
message,
},
};
}
function delegate(sessionAttributes, slots) {
return {
sessionAttributes,
dialogAction: {
type: 'Delegate',
slots,
},
};
}
// ---------------- Helper Functions --------------------------------------------------
function buildValidationResult(isValid, violatedSlot, messageContent) {
if (messageContent == null) {
return {
isValid,
violatedSlot,
};
}
return {
isValid,
violatedSlot,
message: { contentType: 'PlainText', content: messageContent },
};
}
let page = 0;
//function to validate user inputs and return page number
function validateMonsters(monsterType, monsterName, time) {
const monsterTypes = ['dragon', 'fiend', 'celestial', 'giant', 'magical beast', 'fey', 'undead', 'elemental'];
if (monsterType && monsterTypes.indexOf(monsterType) === -1) {
return buildValidationResult(false, 'monsterType', `I do not know what ${monsterType} is, would you like to try a different one?`);
}
const monsterNames = ['vampire', 'troll', 'fire giant', 'wyvern', 'true dragon', 'angel', 'azata', 'chimera', 'manticore', 'unicorn', 'dryad', 'ghoul', 'fire elemental', 'water elemental', 'balor', 'succubus'];
if (monsterName && monsterNames.indexOf(monsterName) === -1) {
return buildValidationResult(false, 'monsterName', `I've never heard of ${monsterName}, would you like to try a different one?`);
}
const pages = [9, 23, 44, 58, 68, 90, 116, 124, 125, 146, 148, 199, 268, 269, 270, 282];
if (monsterName == 'angel') {
page = pages[0];
}
if (monsterName == 'azata') {
page = pages[1];
}
if (monsterName == 'chimera') {
page = pages[2];
}
if (monsterName == 'balor') {
page = pages[3];
}
if (monsterName == 'succubus') {
page = pages[4];
}
if (monsterName == 'true dragon') {
page = pages[5];
}
if (monsterName == 'dryad') {
page = pages[6];
}
if (monsterName == 'fire elemental') {
page = pages[7];
}
if (monsterName == 'water elemental') {
page = pages[8];
}
if (monsterName == 'ghoul') {
page = pages[9];
}
if (monsterName == 'fire giant') {
page = pages[10];
}
if (monsterName == 'manticore') {
page = pages[11];
}
if (monsterName == 'troll') {
page = pages[12];
}
if (monsterName == 'unicorn') {
page = pages[13];
}
if (monsterName == 'vampire') {
page = pages[14];
}
if (monsterName == 'wyvern') {
page = pages[15];
}
return buildValidationResult(true, null, null);
}
// --------------- Functions that control the bot's behavior -----------------------
/**
* Performs dialog management and fulfillment for finding your monster.
*
* Beyond fulfillment, the implementation of this intent demonstrates the use of the elicitSlot dialog action
* in slot validation and re-prompting.
*
*/
function searchMonsters(intentRequest, callback) {
const monsterType = intentRequest.currentIntent.slots.monsterType;
const monsterName = intentRequest.currentIntent.slots.monsterName;
const source = intentRequest.invocationSource;
if (source === 'DialogCodeHook') {
// Perform basic validation on the supplied input slots. Use the elicitSlot dialog action to re-prompt for the first violation detected.
const slots = intentRequest.currentIntent.slots;
const validationResult = validateMonsters(monsterType, monsterName, page);
if (!validationResult.isValid) {
slots[`${validationResult.violatedSlot}`] = null;
callback(elicitSlot(intentRequest.sessionAttributes, intentRequest.currentIntent.name, slots, validationResult.violatedSlot, validationResult.message));
return;
}
}
// give user info on the monster, and rely on the goodbye message of the bot to define the message to the end user. In a real bot, this would likely involve a call to a backend service.
callback(close(intentRequest.sessionAttributes, 'Fulfilled',
{ contentType: 'PlainText', content: `Thanks, the monster ${monsterName} of type ${monsterType} can be found on page ${page}` }));
}
// --------------- Intents -----------------------
/**
* Called when the user specifies an intent for this skill.
*/
function dispatch(intentRequest, callback) {
console.log(`dispatch userId=${intentRequest.userId}, intentName=${intentRequest.currentIntent.name}`);
const intentName = intentRequest.currentIntent.name;
// Dispatch to your skill's intent handlers
if (intentName === 'MonsterSearch') {
return searchMonsters(intentRequest, callback);
}
throw new Error(`Intent with name ${intentName} not supported`);
}
// --------------- Main handler -----------------------
// Route the incoming request based on intent.
// The JSON body of the request is provided in the event slot.
exports.handler = (event, context, callback) => {
try {
console.log(`event.bot.name=${event.bot.name}`);
/**
* Uncomment this if statement and populate with your Lex bot name and / or version as
* a sanity check to prevent invoking this Lambda function from an undesired Lex bot or
* bot version.
*/
if (event.bot.name !== 'BestiaryProject') {
callback('Invalid Bot Name');
}
dispatch(event, (response) => callback(null, response));
} catch (err) {
callback(err);
}
};```
Quite simply what the error is telling you is that intentRequest.currentIntent.name is not resulting in a valid value; in fact you're getting back a null.
Try logging the entire intentRequest object at the start of your dispatch method to view the data that's passed through to your method and ensure you're looking in the right place for the intent name.
Related
Hello I'm having a hard time dealing with UserStates in MSBF
Here's the setup of the dialogBot.ts
export class DialogBot extends ActivityHandler {
private conversationState: BotState;
private userState: BotState;
private dialog: Dialog;
private dialogState: StatePropertyAccessor<DialogState>;
/**
*
* #param {BotState} conversationState
* #param {BotState} userState
* #param {Dialog} dialog
*/
constructor(
conversationState: BotState,
userState: BotState,
dialog: Dialog
) {
super();
if (!conversationState) {
throw new Error(
'[DialogBot]: Missing parameter. conversationState is required'
);
}
if (!userState) {
throw new Error('[DialogBot]: Missing parameter. userState is required');
}
if (!dialog) {
throw new Error('[DialogBot]: Missing parameter. dialog is required');
}
this.conversationState = conversationState as ConversationState;
this.userState = userState as UserState;
this.dialog = dialog;
this.dialogState =
this.conversationState.createProperty<DialogState>('DialogState');
this.onMessage(async (context, next) => {
console.log('Running dialog with Message Activity.');
// Run the Dialog with the new message Activity.
await (this.dialog as MainDialog).run(context, this.dialogState);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
this.onDialog(async (context, next) => {
// Save any state changes. The load happened during the execution of the Dialog.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);
// By calling next() you ensure that the next BotHandler is run.
await next();
});
}
}
In the MainDialog.ts I'm fetching a user from the database based on the userID passed on and if it fetches anything it should be saved in the UserState.
mainDialog.ts
export class MainDialog extends CancelAndHelpDialog {
private userProfileAccessor: StatePropertyAccessor<any>;
userState: UserState;
constructor(
bookingDialog: BookingDialog,
userState: UserState,
conversationState: ConversationState
) {
super('MainDialog');
// DECLARE DIALOGS HERE
const createJobOrderDialog = new CreateJobOrderDialog(
'createJobOrderDialog'
);
const checkJobOrderStatusDialog = new CheckJobOrderStatusDialog(
'checkJobOrderStatusDialog'
);
const accountSetupDialog = new AccountSetupDialog(
'accountSetupDialog',
userState
);
this.userProfileAccessor = userState.createProperty('userProfile');
this.userState = userState;
// Define the main dialog and its related components.
// This is a sample "book a flight" dialog.
this.addDialog(new TextPrompt('TextPrompt'));
this.addDialog(bookingDialog);
this.addDialog(createJobOrderDialog);
this.addDialog(checkJobOrderStatusDialog);
this.addDialog(accountSetupDialog);
this.addDialog(
new WaterfallDialog(MAIN_WATERFALL_DIALOG, [
this.accountSetupStep.bind(this),
this.introStep.bind(this),
this.actStep.bind(this),
this.finalStep.bind(this)
])
);
this.initialDialogId = MAIN_WATERFALL_DIALOG;
}
/**
* The run method handles the incoming activity (in the form of a DialogContext) and passes it through the dialog system.
* If no dialog is active, it will start the default dialog.
* #param {TurnContext} context
*/
public async run(
context: TurnContext,
accessor: StatePropertyAccessor<DialogState>
) {
const dialogSet = new DialogSet(accessor);
dialogSet.add(this);
const dialogContext = await dialogSet.createContext(context);
const results = await dialogContext.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(this.id);
}
}
private async accountSetupStep(
stepContext: WaterfallStepContext
): Promise<DialogTurnResult> {
const userProfile = await this.userProfileAccessor.get(
stepContext.context,
{}
);
stepContext.context.activity.from.id = '*******************';
userProfile.isHandover = false;
await this.userProfileAccessor.set(stepContext.context, userProfile);
// await this.userState.saveChanges(stepContext.context, true);
const result = await userService.getUser(
stepContext.context.activity.from.id
);
console.log(result);
if (Object.keys(result).length === 0) {
return await stepContext.beginDialog('accountSetupDialog');
} else {
userProfile.user = result;
await this.userProfileAccessor.set(stepContext.context, userProfile);
// await this.userState.saveChanges(stepContext.context, true);
return await stepContext.next();
}
}
private async introStep(
stepContext: WaterfallStepContext
): Promise<DialogTurnResult> {
const userProfile = await this.userProfileAccessor.get(
stepContext.context,
{}
);
console.log('INTRO STEP USERPROFILE', userProfile);
await stepContext.context.sendActivities([
{
type: 'message',
text: `Hi ${userProfile.user.first_name}, welcome to Podmachine. Let us take care of the dirty stuff so you can sound like a Pro!`
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 },
{
type: 'message',
text: 'To start, you need to submit a job order.'
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 },
{
type: 'message',
text: `So what's a job order? It's basically sending a request to edit (1) one raw episode audio file to Podmachine team. We'll handle the rest. `
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 },
{
type: 'message',
text: `Since you're part of the early access users (Yay!), you're entitled to (1) one free job order / edit. Go ahead and click "Create New Job order."`
},
{
type: 'typing'
},
{ type: 'delay', value: 1000 }
]);
const messageText = (stepContext.options as any).restartMsg
? (stepContext.options as any).restartMsg
: `Please take note that once you submit your job order, Podmachine team will review it first. Make sure all the details you put in your job order are correct. It will be our basis when we do the edits. Thank you!`;
const promptMessage = MessageFactory.suggestedActions(
[
'Create New Job Order',
'Check Status',
'Chat with Team',
'Subscribe Now'
],
messageText
);
return await stepContext.prompt('TextPrompt', {
prompt: promptMessage
});
}
/**
* Second step in the waterall. This will use LUIS to attempt to extract the origin, destination and travel dates.
* Then, it hands off to the bookingDialog child dialog to collect any remaining details.
*/
private async actStep(
stepContext: WaterfallStepContext
): Promise<DialogTurnResult> {
// const bookingDetails = new BookingDetails();
const userProfile = await this.userProfileAccessor.get(stepContext.context, {});
console.log('USER PROFILE ACT STEP', userProfile);
switch (stepContext.result) {
case 'Create New Job Order':
return await stepContext.beginDialog('createJobOrderDialog');
break;
case 'Check Status':
return await stepContext.beginDialog('checkJobOrderStatusDialog');
break;
case 'Chat with Team':
userProfile.isHandover = true;
await stepContext.context.sendActivity(
`Hi ${userProfile.user.first_name}, we're glad to assist you. Please type your concern below. A Podmachine associate will getback to you within 3-5 minutes. Thank you for your patience.`
);
await this.userProfileAccessor.set(stepContext.context, userProfile);
return await stepContext.endDialog();
break;
case 'Upgrade Now':
await stepContext.context.sendActivity(
`Redirecting to Upgrade Now page...`
);
return await stepContext.endDialog();
break;
case 'Schedule a Checkpoint Meeting':
await stepContext.context.sendActivity(`Feature in progress...`);
return await stepContext.endDialog();
break;
default:
break;
}
return await stepContext.next();
// return await stepContext.beginDialog('bookingDialog', bookingDetails);
}
I can see the saved user details in the introStep but when it comes to the actStep I no longer see the value and it comes out undefined. Can you help me with implementing UserState because I'm not sure if I'm doing it correctly by loading it, the samples from github is not as clear.
USER PROFILE ACT STEP {}
[onTurnError] unhandled error: DialogContextError: Cannot read properties of undefined (reading 'first_name')
Looks like your bot aren't storing the state, so it can't recover it on the next turn.
Are you setting somewhere the storage your bot are using?
Check this doc on how to use storages:
https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=javascript
I'm trying to use react-admin to provide a user CRUD from my API (express)
I followed the steps from react-admin documentation.
Creating my own DataProvider.
Inserting Admin component tells me it is properly setup.
Adding a child Ressource component with users as the ressource name and ListGuesser as the list.
At this point I get a toast saying response in undefined and a console error saying Warning: Missing translation for key: "response is undefined"
I can see in the network tabs that the request is properly sent and receives a 200 response with the data I expected
I cannot understand it and where it comes from
Here is my adminComponent
import React from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin'
import myDataProvider from './myDataProvider'
import './adminHomepage.css'
let myProvider = myDataProvider('http://localhost:8666')
function AdminHomepage(props) {
return (
<Admin dataProvider={myProvider}>
<Resource name="users" list={ListGuesser} />
</Admin>
);
}
export default AdminHomepage;
Here is my dataProvider
import useAuth from "../../hooks/useAuth";
import { stringify } from 'query-string';
import {
fetchUtils,
GET_LIST,
GET_ONE,
CREATE,
UPDATE,
DELETE,
GET_MANY_REFERENCE
} from 'ra-core';
const { getToken } = useAuth();
export default (apiUrl, httpClient = fetchUtils.fetchJson) => {
const convertDataRequestToHttp = (type, resource, params) => {
let url = "";
const options = {};
options.headers = new Headers({ Authorization : getToken(), Accept: "application/json" })
switch (type) {
case GET_LIST: {
url = `${apiUrl}/${resource}/`;
break;
}
case GET_ONE: {
url = `${apiUrl}/${resource}/${params.id}`;
break;
}
case CREATE: {
url = `${apiUrl}/${resource}/${params.id}`;
options.method = "POST";
options.body = JSON.stringify(params.data);
break;
}
case UPDATE: {
url = `${apiUrl}/${resource}/${params.id}`;
options.method = "PUT";
options.body = JSON.stringify(params.data);
break;
}
case DELETE: {
url = `${apiUrl}/${resource}/${params.id}`;
options.method = "DEL";
break;
}
default: {
throw new Error(`Unsupported request type ${type}`);
}
}
return { url, options };
};
const convertHttpResponse = (response, type, resource, params) => {
const { headers, json } = response;
switch (type) {
case GET_LIST:
case GET_MANY_REFERENCE: {
if (!headers.has("content-range")) {
throw new Error(
"Content-Range is missing from header, see react-admin data provider documentation"
);
}
let ret = {
data: json.users,
total: parseInt(
headers
.get("Content-Range")
.split(" ")
.pop()
)
};
console.log("RETURN", ret)
return ret
}
case CREATE: {
return { data: { ...params.data, id: json.id } };
}
default: {
return { data: json };
}
}
};
return (type, resource, params) => {
const { url, options } = convertDataRequestToHttp(type, resource, params);
return httpClient(url, options).then(response => {
console.log(response)
convertHttpResponse(response, type, resource, params);
});
};
};
Screenshot of my error
Warning: Missing translation for key: "response is undefined"
in Notification (created by Connect(Notification))
in Connect(Notification) (created by WithStyles(Connect(Notification)))
in WithStyles(Connect(Notification)) (created by Context.Consumer)
in Context.Consumer (created by translate(WithStyles(Connect(Notification))))
in translate(WithStyles(Connect(Notification))) (created by Layout)
in Layout (created by WithStyles(Layout))
in WithStyles(Layout) (created by Route)
in Route (created by withRouter(WithStyles(Layout)))
in withRouter(WithStyles(Layout)) (created by Connect(withRouter(WithStyles(Layout))))
in Connect(withRouter(WithStyles(Layout))) (created by LayoutWithTheme)
in LayoutWithTheme (created by Route)
in Route (created by CoreAdminRouter)
in CoreAdminRouter (created by Connect(CoreAdminRouter))
in Connect(CoreAdminRouter) (created by getContext(Connect(CoreAdminRouter)))
in getContext(Connect(CoreAdminRouter)) (created by Route)
in Route (created by CoreAdminBase)
in CoreAdminBase (created by withContext(CoreAdminBase))
in withContext(CoreAdminBase) (at adminHomepage.js:11)
in AdminHomepage (created by Router.Consumer)
in Router.Consumer (created by Route)
in Route (at App.js:17)
in App (at src/index.js:7)
I think you're forgetting to return the actual result back:
Change your code to:
return (type, resource, params) => {
const { url, options } = convertDataRequestToHttp(type, resource, params);
return httpClient(url, options).then(response => {
console.log(response)
return convertHttpResponse(response, type, resource, params);
});
}
This bit is probably just to pad the answer.
A Promise in JavaScript is just an object which will at some point get resolved to a value. When you do Promise.then you're basically returning a new promise which will have a callback triggered when resolved and that callback will receive the resolved property. The new promise your make will have the return value based on the result of the callback given. In your case the final resolved promise would have been undefined because nothing was returned in the callback
I just found this issue because I was trying to solve the duplicate post request issue when I am using workbox-background-sync. There is a function of my web app to upload the photos. But every time I did uploaded twice to the database. Here is the code I have:
const bgSyncQueue = new workbox.backgroundSync.Queue(
'photoSubmissions',
{
maxRetentionTime: 48 * 60,//48 hours
callbacks: {
queueDidReplay: function (requests) {
if (requests.length === 0) {
removeAllPhotoSubmissions();
}
else {
for(let request of requests) {
if (request.error === undefined && (request.response && request.response.status === 200)) {
removePhotoSubmission();
}
}
}
}
}
});
workbox.routing.registerRoute(
new RegExp('.*\/Home\/Submit'),
args => {
const promiseChain = fetch(args.event.request.clone())
.catch(err => {
bgSyncQueue.addRequest(args.event.request);
addPhotoSubmission();
changePhoto();
});
event.waitUntil(promiseChain);
},
'POST'
);
It may because the fetch(args.event.request.clone()). If I remove it, then there is no duplication anymore. I am using workbox 3.6.1 .
Finally I found the solution. Below is my code:
const photoQueue = new workbox.backgroundSync.Plugin('photoSubmissions', {
maxRetentionTime: 48 * 60, // Retry for max of 48 Hours
callbacks: {
queueDidReplay: function (requests) {
if (requests.length === 0) {
removeAllPhotoSubmissions();
}
else {
for(let request of requests) {
if (request.error === undefined && (request.response && request.response.status === 200)) {
removePhotoSubmission();
}
}
}
}
}
});
const myPhotoPlugin = {
fetchDidFail: async ({originalRequest, request, error, event}) => {
addPhotoSubmission();
changePhoto();
}
};
workbox.routing.registerRoute(
new RegExp('.*\/Home\/Submit'),
workbox.strategies.networkOnly({
plugins: [
photoQueue,
myPhotoPlugin
]
}),
'POST'
);
I removed fetch. If we still want to controll by ourselves, we need to use respondWith(). I have tested it, it is working. But I would like to use more workbox way to solve the problem. I am using workbox 3.6.3 and I created my own plugin to include a callback function fetchDidFail to update my views. Here are the references I found:
one and two. There are no duplicate posts anymore.
I added the IceCandidate after completing the offer and answer, but it still calls errors: 'RTCPeerConnection': the icecandidate cannot be added. This is part of my code:
else if (data.user_type == 'signaling') {
if (!rtcPeerConn)
startSignaling();
var message = JSON.parse(data.user_data);
if (message.sdp) {
rtcPeerConn.setRemoteDescription(new RTCSessionDescription(message.sdp),
function () { // if we received an offer, we need to answer
if (rtcPeerConn.remoteDescription.type == 'offer' && myUserType=="doctor") {
rtcPeerConn.createAnswer(sendLocalDesc, logError);
}
}, logError);
} else {
rtcPeerConn.addIceCandidate(new RTCIceCandidate(message.candidate));
}
}
I want to override the existing magento2 JS Component in my theme for some more customization.
Magento_Checkout/js/view/minicart.js
Above JS component i want to override and i want to add some more operation on the remove button event.
You can try "map" of require js. I used this and working for me. following is the requirejs-config.js inside my theme.
var config = {
map: {
'*': {
'Magento_Checkout/js/view/minicart':'js/custom/minicart'
}
}
};
Modified minicart.js file is placed inside "web/js/custom" folder inside my theme.
Just Go to your theme Override Magento_Checkout there, then under web folder make path as same as core module then add your js file & do required changes. It will reflect on frontend.
You can also extend an existing Magento JS without overwriting the whole file in your module add the require-config.js
app/code/MyVendor/MyModule/view/frontend/requirejs-config.js
var config = {
config: {
mixins: {
'Magento_Checkout/js/view/minicart': {
'MyVendor_MyModule/js/minicart': true
}
}
}
};
Then add the minicart.js
app/code/MyVendor/MyModule/view/frontend/web/js/minicart.js
define([], function () {
'use strict';
return function (Component) {
return Component.extend({
/**
* #override
*/
initialize: function () {
var self = this;
return this._super();
},
MyCustomFunction: function () {
return "my function";
}
});
}
});
define(['jquery'],function ($) {
'use strict';
var mixin = {
/**
*
* #param {Column} elem
*/
initSidebar: function () {
var sidebarInitialized = false, miniCart;
miniCart = $('[data-block=\'minicart\']');
if (miniCart.data('mageSidebar')) {
miniCart.sidebar('update');
}
if (!$('[data-role=product-item]').length) {
return false;
}
miniCart.trigger('contentUpdated');
if (sidebarInitialized) {
return false;
}
sidebarInitialized = true;
miniCart.sidebar({
'targetElement': 'div.block.block-minicart',
'url': {
'checkout': window.checkout.checkoutUrl,
'update': window.checkout.updateItemQtyUrl,
'remove': window.checkout.removeItemUrl,
'loginUrl': window.checkout.customerLoginUrl,
'isRedirectRequired': window.checkout.isRedirectRequired
},
'button': {
'checkout': '#top-cart-btn-checkout',
'remove': '#mini-cart a.action.delete',
'increacseqty':'#mini-cart a.action.increase-qty',
'decreaseqty':'#mini-cart a.action.decrease-qty',
'close': '#btn-minicart-close'
},
'showcart': {
'parent': 'span.counter',
'qty': 'span.counter-number',
'label': 'span.counter-label'
},
'minicart': {
'list': '#mini-cart',
'content': '#minicart-content-wrapper',
'qty': 'div.items-total',
'subtotal': 'div.subtotal span.price',
'maxItemsVisible': window.checkout.minicartMaxItemsVisible
},
'item': {
'qty': ':input.cart-item-qty',
'button': ':button.update-cart-item'
},
'confirmMessage': $.mage.__('Are you sure you would like to remove this item from the shopping cart??')
});
return this._super();
}
};
return function (minicart) { // target == Result that Magento_Ui/.../columns returns.
return minicart.extend(mixin); // new result that all other modules receive
};
});