Executing nested WaterfallDialogs - nodejs - node.js

I'm trying to build a requirement system for order dialogs in our bot, so that we can reuse the main structure for different procedures.
enum DialogIds {
// Necessary Ids
oauthPrompt = "oauthPrompt",
// Requirement dialogs
itemWaterfallDialog = "itemWaterfallDialog",
// Reset Dialogs
summaryWaterfallDialog = "summaryWaterfallDialog",
// All other prompts
unrecognizedItemPrompt = "unrecognizedItemPrompt",
beneficiaryConfirmPrompt = "beneficiaryConfirmPrompt",
askBeneficiaryPrompt = "askBeneficiaryPrompt",
reasonPrompt = "reasonPrompt",
orderConfirm = "orderConfirm"
}
export class OrderDialog extends ComponentDialog {
private responseManager: ResponseManager;
private requirementManager: RequirementManager;
private luisResult: RecognizerResult | undefined = undefined;
// TODO: get userState and ConversationState
constructor(
private service: BotServices,
telemetryClient: BotTelemetryClient
) {
super(OrderDialog.name);
this.initialDialogId = OrderDialog.name;
// Response manager serving OrderResponses.json
this.responseManager = new ResponseManager(["fr-fr"], [OrderResponses]);
const routeWaterfallDialog: ((
sc: WaterfallStepContext
) => Promise<DialogTurnResult>)[] = [
this.route.bind(this)
];
this.telemetryClient = telemetryClient;
this.addDialog(
new WaterfallDialog(this.initialDialogId, routeWaterfallDialog)
);
/**
* Order specific dialogs and requirements
*/
const itemWaterfallDialog: WaterfallDialog = new WaterfallDialog(
DialogIds.itemWaterfallDialog,
[this.itemStep.bind(this), this.itemEndStep.bind(this)]
);
this.addDialog(itemWaterfallDialog);
const reqs = [
new Requirement<string>("claimant", false, undefined),
new Requirement<string>(
"item",
true,
undefined,
itemWaterfallDialog,
DialogIds.itemWaterfallDialog
),
];
// Create requirement manager for this dialog
this.requirementManager = new RequirementManager(reqs);
// Add all the prompt
this.addDialog(new ConfirmPrompt(DialogIds.beneficiaryConfirmPrompt));
this.addDialog(new TextPrompt(DialogIds.unrecognizedItemPrompt));
this.addDialog(new TextPrompt(DialogIds.askBeneficiaryPrompt));
this.addDialog(new TextPrompt(DialogIds.reasonPrompt));
this.addDialog(new ConfirmPrompt(DialogIds.orderConfirm));
}
/**
* We save the token, query graph is necessary and
* execute the next dialog if any, if not we'll
* execute the summary waterfallDialog.
* #param sc context
*/
async route(sc: WaterfallStepContext): Promise<DialogTurnResult> {
this.requirementManager.set("claimant", 'nothing');
let next = this.requirementManager.getNext();
while (next) {
await sc.beginDialog(next.dialogId!);
// Execute summary if there are no elements left
if (!this.requirementManager.getNextBool()) {
await sc.beginDialog(DialogIds.summaryWaterfallDialog);
}
next = this.requirementManager.getNext();
}
return sc.endDialog();
}
/**
* ITEM
* #param sc
*/
async itemStep(sc: WaterfallStepContext): Promise<DialogTurnResult> {
// Couldn't recgonize any item
if (this.luisResult!.entities.length === 0) {
await sc.context.sendActivity(
this.responseManager.getResponse(
OrderResponses.itemNotRecognized
)
);
// prompt user for the item again
return await sc.prompt(
DialogIds.unrecognizedItemPrompt,
this.responseManager.getResponse(OrderResponses.rePromptItem)
);
}
const entities = this.luisResult!.entities as generalLuis["entities"];
if (entities.PhoneItem || entities.ComputerItem) {
const item = entities.PhoneItem
? entities.PhoneItem
: entities.ComputerItem;
if (item) {
this.requirementManager.set("item", item[0][0]);
}
}
return await sc.next();
}
async itemEndStep(sc: WaterfallStepContext): Promise<DialogTurnResult> {
// Save result from itemStep(prompt triggered) if any
if (sc.result) {
await sc.context.sendActivity(
this.responseManager.getResponse(OrderResponses.thanksUser)
);
// retrieve item from result and save it
const item = sc.result as string;
this.requirementManager.set("item", item);
}
return sc.endDialog();
}
}
The line
const result = await sc.beginDialog(next.dialogId!);
Is starting a WaterfallDialog declared in the constructor of the Dialog, and the route method is also inside a general waterfallDialog.
The problem is that, when one of the child dialogs prompts the user, the code doesn't wait for the user response, and because of the way the route works it will call the same dialog again(if a value on an object is not filled it will call the indicated dialog, that's what the requirement manager does).
If saving the return from that line, we can see that the status is "waiting", how could I fix it, or should I create independent dialogs for each requirement, and not just waterfallDialogs?
Thanks.

Related

Avoid multiple Class Instances using Singleton

Project: Create a dynamodb package using #aws-sdk v3 (client-dynamodb & lib-dynamodb)
Base class
class DynamoDB {
#client: DynamoDBDocumentClient;
constructor() {
const db_client = new DynamoDBClient();
this.#client = DynamoDBDocumentClient.from(db_client);
}
// class methods are get_item, delelte_item etc..
// snip--
}
Class for a table with useful methods
class NoSqlTable extends DynamoDB {
table_name: string;
constructor(table_name: DynamodbTableName) {
super();
this.table_name = table_name;
}
async get_record<K,R>(key: K, params: GetRecordParams = {}) {
const result = await super.get_item({ TableName: this.table_name, Key: key, ...params });
return result.Item as R;
}
// other methods
}
Finally, a set of functions exposed to users of the npm package/project
export const get_user = async(user_id:string) => {
const table = new NosqlTable("user");
const user = await table.get_record({ key: user_id });
return user;
}
export const get_accounts = async(user_id:string) => {
const table = new NosqlTable("account");
const accounts = await table.query_table({ IndexName: "user_id-account_id-index", ...});
return accounts;
}
Target:
Each class NosqlTable and DynamoDB is to be initiated once only.
Note: Once means: 1 if in a server setting (long-running process) or once per request if in serverless mode
Each class should only be initialized if used.
Issue
With the current settings, each function exposed to users like get_user, and get_accounts will initialize the NosqlTable class every time they are called and in turn, calls the DynamoDB class.
For example, in a single request, get_user is called 10 times, and get_accounts is called 5 times,
10 calls to initialize the NosqlTable class with table_name="user",
5 calls to initialize the NosqlTable class with table_name="account"
15 calls to the DynamoDB class.
Solution
Attempt 1
Use a singleton pattern,
let singleton_dynamodb: Dynamodb;
const connect_dynamodb = (params) => {
if(!singleton_dynamodb) singleton_dynamodb = new DynamoDB(params);
return singleton_dynamodb;
}
/* The updated `NosqlTable` class now does not extend DynamoDB class; instead calls the `connect_dynamodb` function
*/
class NoSqlTable {
table_name: string;
this.dynamodb = DynamoDB;
constructor(table_name: DynamodbTableName) {
this.table_name = table_name;
this.dynamodb = connect_dynamodb();
}
async get_record<K,R>(key: K, params: GetRecordParams = {}) {
const result = await this.dynamodb.get_item({ TableName: this.table_name, Key: key, ...params });
return result.Item as R;
}
}
let singleton_nosql_table: NosqlTable;
const connect_nosql_table = (table_name) => {
if(!singleton_nosql_table) singleton_nosql_table = new NosqlTable(table_name);
return singleton_nosql_table;
}
export const get_user = async(user_id:string) => {
const table = connect_nosql_table ("user");
const user = await table.get_record({ key: user_id });
return user;
}
Now, if get_user is called the first time, it will call connect_nosql_table which sets the singleton_nosql_table variable
any subsequent calls to this function will return the singleton_nosql_table setup earlier.
Similarly, any call to the new NosqlTable(table_name) internally calls connect_dynamodb rather than extending Dynamodb class.
Is this the correct way to do it, and what are the best practices to improve this setup?
The ultimate target is to improve performance based on the assumption that initializing 1 class instance is more performant than initializing 100 class instances.
Note: The pattern will be used for other packages/projects, like AWS SQS, AWS SNS, and HTTP Client class per domain.

How to pass default parameters to the #Query class in Nest.Js?

I'm trying to pass the default parameters maxnodes=3 and addstats=false to the controller via the #Query parameter in Nest.Js.
The code works fine, but the default parameters are not used. When I pass on the query parameters the ones that are passed are shown, but if none are passed, the default values (3 and false) are not used.
How to fix that?
context.contructor.ts:
import { CreateContextQuery } from './context.query';
import { CreateContextDto } from './context.dto';
#Post('graph')
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
context.query.ts:
import { ApiProperty } from '#nestjs/swagger';
export class CreateContextQuery {
#ApiProperty({
description: 'Maximum number of nodes to show on the graph',
})
maxnodes;
#ApiProperty({
description: 'Include graph statistics',
})
addstats;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
So basically in your DTO, you can give default values.
export class CreateContextQuery {
#IsOptional()
#Type(() => Number)
#IsNumber()
#Min(0)
maxnodes?: number = 3;
#IsOptional()
#Type(() => Boolean)
#IsBoolean()
addstats?: boolean = false;
constructor(maxnodes = 3, addstats = false) {
this.maxnodes = maxnodes;
this.addstats = addstats;
}
}
// as you can see i am using validation too
And in your controller :
#Post('graph')
#UsePipes(new ValidationPipe({ transform: true }))
// you need to add this for tansformation
public async createGraphForContext(
#Body('context') contextData: CreateContextDto,
#Query()
contextQuery: CreateContextQuery,
) {
const before = Date.now();
const { context } = await this.contextService.createContext(contextData);
const graph = await this.contextService.getGraphOfContext(
context.id,
contextQuery.maxnodes,
contextQuery.addstats,
);
}
PS
Also if you want you can add custom decorators, in your case:
// add this decorator
export const GetContextQuery = createParamDecorator((_data: unknown, ctx: ExecutionContext): CreateContextDto => {
const request = ctx.switchToHttp().getRequest();
const query = request.query;
const maxnodes = parseInt(query.maxnodes) || 3;//default values here in case it fails to parse
const addstats = Boolean(query.addstats) || 0;
return { addstats, addstats };
});
and in your controller, you can call the decorator instead of #Query
just add your decorator #GetContextQuery() context: CreateContextDto, and now you do not need the UsePipes
What you receive in the query param is a plain object. You can achieve what you want putting a pipe in your query param and applying a class transform to instantiate the class.
Read this: https://docs.nestjs.com/pipes#providing-defaults
contextQuery isn't an instance of CreateContextQuery because, without any configuration, Nest won't call new CreateContextQuery any time. This is why you end up using pipes (read this https://docs.nestjs.com/techniques/validation#transform-payload-objects)

Hide payment method if specific shipping method selected in odoo eCommerce website

I want to hide specific payment method (cash on delivery) if specific shipping method selected before. For example, if I check shipping method A , then in the next step, payment methods I have only one method to check ( other methods disabled or unable to check). I try to edit and add many2many relational field of payment.acquirer model in shdelivery.carrier and use this filed in controller but it not work , so far I havent' find the solution.
this is my code snapshot:
my python code:
class ShippingMethod(models.Model):
_inherit = 'delivery.carrier'
payment_acquirer_ids = fields.Many2many('payment.acquirer',string='Payment Mathods')
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
my controller
class WebsiteSaleDeliveryExtraCost(WebsiteSale):
#http.route(['/shop/change_shipping_method'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
def change_shipping_method(self, **post):
carrier_id = post.get('carrier_id')
print('******00******', request.session)
carrier = request.env['delivery.carrier'].browse(int(carrier_id))
acquirer_ids = carrier.payment_acquirer_ids.ids
acquirers = request.env['payment.acquirer'].search([('id','in',acquirer_ids)])
# if acquirers:
# return request.redirect("/shop/payment")
return acquirers.ids
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
her is JavaScript code to route controller:
odoo.define('website_sale_delivery_extra_cost.checkout', function (require) {
'use strict';
var core = require('web.core');
var publicWidget = require('web.public.widget');
require('website_sale_delivery.checkout')
var _t = core._t;
// hide payment method base on shipping method
publicWidget.registry.websiteSaleDelivery.include({
selector: '.oe_website_sale',
events: {
'click #delivery_carrier .o_delivery_carrier_select': '_change_shipping_method',
},
_change_shipping_method: function (ev) {
var self = this;
var $radio = $(ev.currentTarget).find('input[type="radio"]');
if ($radio.val()){
this._rpc({
route: '/shop/change_shipping_method',
params: {
carrier_id: $radio.val(),
}}).then(function (data) {
if (data.length >= 1) {
console.log(data[0]);
console.log('---------');
return { location.reload(); };
} else {
return false;
}
}); // end of then
} //end of if
},
/**
* #private
*/
_trackGA: function () {
var websiteGA = window.ga || function () {};
websiteGA.apply(this, arguments);
},
/**
* #private
*/
_vpv: function (page) { //virtual page view
this._trackGA('send', 'pageview', {
'page': page,
'title': document.title,
});
},
});
});
Any other ideas how to solve this issue ?

GuzzleHttp Parallel Progress For Async Client in Azure and Flysystem

I would like to get the actual block progress and not the Progress of all the transfers. Currently i don't know how to detect the blockId of each individual transfer. The information on the progress callback im currently retrieving is pointless.
Here's the progress function, contained within ServiceRestProxy.php
Original Function https://github.com/Azure/azure-storage-php/blob/master/azure-storage-common/src/Common/Internal/ServiceRestProxy.php#L99
/**
* Create a Guzzle client for future usage.
*
* #param array $options Optional parameters for the client.
*
* #return Client
*/
private static function createClient(array $options)
{
$verify = true;
//Disable SSL if proxy has been set, and set the proxy in the client.
$proxy = getenv('HTTP_PROXY');
// For testing with Fiddler
// $proxy = 'localhost:8888';
// $verify = false;
if (!empty($proxy)) {
$options['proxy'] = $proxy;
}
if (!empty($options['verify'])) {
$verify = $options['verify'];
}
$downloadTotal = 0;
return (new \GuzzleHttp\Client(
array_merge(
$options,
array(
"defaults" => array(
"allow_redirects" => true,
"exceptions" => true,
"decode_content" => true,
),
'cookies' => true,
'verify' => $verify,
'progress' => function (
$downloadTotal,
$downloadedBytes,
$uploadTotal,
$uploadedBytes
){
// i need to detect which block the progress is for.
echo ("progress: download: {$downloadedBytes}/{$downloadTotal}, upload: {$uploadedBytes}/{$uploadTotal}");
}
)
)
));
}
I got a solution to get each block progress.
I needed to use the Async Function for this. Updated version.
/**
* Send the requests concurrently. Number of concurrency can be modified
* by inserting a new key/value pair with the key 'number_of_concurrency'
* into the $requestOptions of $serviceOptions. Return only the promise.
*
* #param callable $generator the generator function to generate
* request upon fulfillment
* #param int $statusCode The expected status code for each of the
* request generated by generator.
* #param ServiceOptions $options The service options for the concurrent
* requests.
*
* #return \GuzzleHttp\Promise\Promise|\GuzzleHttp\Promise\PromiseInterface
*/
protected function sendConcurrentAsync(
callable $generator,
$statusCode,
ServiceOptions $options
) {
$client = $this->client;
$middlewareStack = $this->createMiddlewareStack($options);
$progress = [];
$sendAsync = function ($request, $options) use ($client, $progress) {
if ($request->getMethod() == 'HEAD') {
$options['decode_content'] = false;
}
$options["progress"] = function(
$downloadTotal,
$downloadedBytes,
$uploadTotal,
$uploadedBytes) use($request, $progress){
// extract blockid from url
$url = $request->getUri()->getQuery();
parse_str($url, $array);
// this array can be written to file or session etc
$progress[$array["blockid"]] = ["download_total" => $downloadTotal, "downloaded_bytes" => $downloadedBytes, "upload_total" => $uploadTotal, "uploaded_bytes" => $uploadedBytes];
};
return $client->sendAsync($request, $options);
};
$handler = $middlewareStack->apply($sendAsync);
$requestOptions = $this->generateRequestOptions($options, $handler);
$promises = \call_user_func(
function () use (
$generator,
$handler,
$requestOptions
) {
while (is_callable($generator) && ($request = $generator())) {
yield \call_user_func($handler, $request, $requestOptions);
}
}
);
$eachPromise = new EachPromise($promises, [
'concurrency' => $options->getNumberOfConcurrency(),
'fulfilled' => function ($response, $index) use ($statusCode) {
//the promise is fulfilled, evaluate the response
self::throwIfError(
$response,
$statusCode
);
},
'rejected' => function ($reason, $index) {
//Still rejected even if the retry logic has been applied.
//Throwing exception.
throw $reason;
}
]);
return $eachPromise->promise();
}

How to define different context menus for different objects in autodesk forge

I want to define different context menus for different objects in forge viewer,this is my code
viewer.addEventListener(Autodesk.Viewing.AGGREGATE_SELECTION_CHANGED_EVENT,function(e){
if(viewer.getSelection().length==0){return;}
var selectId=viewer.getSelection()[0];
viewer.search("Cabinet",function(ids){
if(ids.indexOf(selectId)!=-1){
viewer.registerContextMenuCallback('CabinetMsg', function (menu, status) {
if (status.hasSelected) {
menu.push({
title: "CabinetMsg",
target: function () {
openLayer('CabinetMsg','954','775','CabinetMsg.html')
}
});
}
});
}else{
viewer.registerContextMenuCallback('CabinetMsg', function (menu, status) {
if (status.hasSelected) {
menu.forEach(function(el,index){
if(el.title=="CabinetMsg"){
menu.splice(menu.indexOf(index),1)
}
})
}
});
}
})
});
But push elements to the array is always later than the context menus show. My custom context menu is always show when I select another object. What I can do?
The codes you provided will create 2 new sub items to the context menu. Here is a way for this case, i.e. you have to write your own ViewerObjectContextMenu. In addition, you need do hitTest in ViewerObjectContextMenu.buildMenu to get dbId selected by the mouse right clicking. Here is the example for you:
class MyContextMenu extends Autodesk.Viewing.Extensions.ViewerObjectContextMenu {
constructor( viewer ) {
super( viewer );
}
isCabinet( dbId ) {
// Your logic for determining if selected element is cabinet or not.
return false;
}
buildMenu( event, status ) {
const menu = super.buildMenu( event, status );
const viewport = this.viewer.container.getBoundingClientRect();
const canvasX = event.clientX - viewport.left;
const canvasY = event.clientY - viewport.top;
const result = that.viewer.impl.hitTest(canvasX, canvasY, false);
if( !result || !result.dbId ) return menu;
if( status.hasSelected && this.isCabinet( result.dbId ) ) {
menu.push({
title: 'CabinetMsg',
target: function () {
openLayer( 'CabinetMsg', '954', '775', 'CabinetMsg.html' );
}
});
}
return menu;
}
}
After this, you could write an extension to replace default viewer context menu with your own menu. Here also is the example:
class MyContextMenuExtension extends Autodesk.Viewing.Extension {
constructor( viewer, options ) {
super( viewer, options );
}
load() {
this.viewer.setContextMenu( new MyContextMenu( this.viewer ) );
return true;
}
unload() {
this.viewer.setContextMenu( new Autodesk.Viewing.Extensions.ViewerObjectContextMenu( this.viewer ) );
return true;
}
}
Hope this help.

Resources