Azure queues and ContinueConversationLater/OnContinueConversation (#3083)

* added `QueueStorage` as abstract class

* new package `botbuilder-azure-queues`

* new action ContinueConversationLater and OnContinueConversation

* resolved lint errors

* use Buffer instead of btoa

* added `relatesTo` in continueConversation

* added test for azureQueueStorage

* removed `await` keyword

* use `Record<string, unknown>` instead of `object`

* initializePromise

* allow skipping tests

* await _initialize()

* fixed docs

Co-authored-by: Josh Gummersall <1235378+joshgummersall@users.noreply.github.com>
This commit is contained in:
Zichuan Ma 2020-12-22 23:24:13 +08:00 коммит произвёл GitHub
Родитель b740054fe7
Коммит 9c0850d967
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 469 добавлений и 1 удалений

Просмотреть файл

@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.json"
}

4
libraries/botbuilder-azure-queues/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
_ts3.4
lib
coverage
.nyc_output

Просмотреть файл

@ -0,0 +1,52 @@
{
"name": "botbuilder-azure-queues",
"author": "Microsoft Corp.",
"description": "BotBuilder storage bindings for Azure Queue services",
"version": "4.1.6",
"preview": true,
"license": "MIT",
"keywords": [
"botbuilder",
"botframework",
"bots",
"chatbots"
],
"bugs": {
"url": "https://github.com/Microsoft/botbuilder-js/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/botbuilder-js.git"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"typesVersions": {
"<3.9": {
"*": [
"_ts3.4/*"
]
}
},
"dependencies": {
"@azure/storage-queue": "^12.2.0",
"botbuilder-core": "4.1.6"
},
"devDependencies": {
"sinon": "^9.2.0",
"ts-essentials": "^7.0.1"
},
"scripts": {
"build": "tsc -b",
"build-docs": "typedoc --theme markdown --entryPoint botbuilder-azure-queues --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder-azure-queues .\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK - Azure Queues\" --readme none",
"clean": "rimraf _ts3.4 lib tsconfig.tsbuildinfo",
"lint": "eslint . --ext .js,.ts",
"postbuild": "downlevel-dts lib _ts3.4/lib",
"test": "yarn build && nyc mocha --check-leaks tests",
"test:compat": "api-extractor run --verbose"
},
"files": [
"_ts3.4",
"lib",
"src"
]
}

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { QueueClient } from '@azure/storage-queue';
import { Activity, QueueStorage } from 'botbuilder-core';
/**
* Service used to add messages to an Azure Storage Queues.
*/
export class AzureQueueStorage extends QueueStorage {
private _initializePromise: Promise<unknown>;
private readonly _queueClient: QueueClient;
/**
* Initializes a new instance of the AzureQueueStorage class.
*
* @param {string} queuesStorageConnectionString Azure storage connection string.
* @param {string} queueName Name of the storage queue where entities will be queued.
*/
public constructor(queuesStorageConnectionString: string, queueName: string) {
super();
if (!queuesStorageConnectionString) {
throw new Error(`queuesStorageConnectionString cannot be empty`);
}
if (!queueName) {
throw new Error(`queueName cannot be empty`);
}
this._queueClient = new QueueClient(queuesStorageConnectionString, queueName);
}
/**
* Queue an Activity to an Azure storage queues. The visibility timeout specifies how long the message should be visible
* to Dequeue and Peek operations. The message content must be a UTF-8 encoded string that is up to 64KB in size.
*
* @param {Partial<Activity>} activity The [Activity](xref:botframework-core.Activity) to be queued for later processing.
* @param {number} visibilityTimeout Default value of 0. Cannot be larger than 7 days.
* @param {number} messageTimeToLive Specifies the time-to-live interval for the message.
* @returns {Promise<string>} [QueueSendMessageResponse](xref:@azure/storage-queue.QueueSendMessageResponse) as a JSON string.
*/
public async queueActivity(
activity: Partial<Activity>,
visibilityTimeout?: number,
messageTimeToLive?: number
): Promise<string> {
await this._initialize();
// Convert activity to base64 string
const activityString = JSON.stringify(activity);
const message = Buffer.from(activityString).toString('base64');
const receipt = await this._queueClient.sendMessage(message, {
visibilityTimeout,
messageTimeToLive,
});
return JSON.stringify(receipt);
}
private _initialize(): Promise<unknown> {
if (!this._initializePromise) {
this._initializePromise = this._queueClient.createIfNotExists();
}
return this._initializePromise;
}
}

Просмотреть файл

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
export * from './azureQueueStorage';

Просмотреть файл

@ -0,0 +1,78 @@
const assert = require('assert');
const { QueueClient } = require('@azure/storage-queue');
const { DialogManager, DialogTurnStateConstants } = require('botbuilder-dialogs');
const {
ActivityTypes,
ActivityEventNames,
ConversationState,
MemoryStorage,
TestAdapter,
useBotState,
UserState,
} = require('botbuilder-core');
const fetch = require('node-fetch');
const { ContinueConversationLater } = require('../../botbuilder-dialogs-adaptive/lib');
const { AzureQueueStorage } = require('../lib');
const connectionString = process.env.AZURE_QUEUE_STORAGE_CONNECTION_STRING || 'UseDevelopmentStorage=true';
const queueName = process.env.AZURE_QUEUE_NAME || 'azurequeuestoragetest';
const emulatorEndpoint = 'http://localhost:10001';
const checkEmulator = async () => {
let canConnectToEmulator = false;
try {
await fetch(emulatorEndpoint);
canConnectToEmulator = true;
} catch (err) {
canConnectToEmulator = false;
}
if (!canConnectToEmulator) {
console.warn(`Unable to connect to Azure Queue Storage Emulator at ${emulatorEndpoint}. Skipping tests.`);
}
return canConnectToEmulator;
};
describe('AzureQueueStorage', function () {
this.timeout(5000);
it('ContinueConversationLaterTest', async function () {
const canConnectToEmulator = await checkEmulator();
if (canConnectToEmulator) {
const queue = new QueueClient(connectionString, queueName);
await queue.createIfNotExists();
await queue.clearMessages();
const queueStorage = new AzureQueueStorage(connectionString, queueName);
const dm = new DialogManager(
new ContinueConversationLater().configure({
date: '=addSeconds(utcNow(), 2)',
value: 'foo',
})
);
dm.initialTurnState.set(DialogTurnStateConstants.queueStorage, queueStorage);
const adapter = new TestAdapter(async (context) => {
return dm.onTurn(context);
});
const memoryStorage = new MemoryStorage();
useBotState(adapter, new ConversationState(memoryStorage), new UserState(memoryStorage));
await adapter.send('hi').startTest();
const { receivedMessageItems } = await new Promise((resolve) => setTimeout(resolve, 2000)).then(() =>
queue.receiveMessages()
);
assert.strictEqual(receivedMessageItems.length, 1);
const message = receivedMessageItems[0];
const messageJson = Buffer.from(message.messageText, 'base64').toString();
const activity = JSON.parse(messageJson);
assert.strictEqual(activity.type, ActivityTypes.Event);
assert.strictEqual(activity.name, ActivityEventNames.ContinueConversation);
assert.strictEqual(activity.value, 'foo');
assert(activity.relatesTo);
}
});
});

Просмотреть файл

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"outDir": "lib",
"rootDir": "src",
"target": "es6"
},
"include": [
"src/**/*"
]
}

Просмотреть файл

@ -31,6 +31,7 @@ export * from './messageFactory';
export * from './middlewareSet';
export * from './privateConversationState';
export * from './propertyManager';
export * from './queueStorage';
export * from './recognizerResult';
export * from './registerClassMiddleware';
export * from './showTypingMiddleware';

Просмотреть файл

@ -0,0 +1,28 @@
/**
* @module botbuilder
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Activity } from 'botframework-schema';
/**
* A base class for enqueueing an Activity for later processing.
*/
export abstract class QueueStorage {
/**
* Enqueues an Activity for later processing. The visibility timeout specifies how long the message should be visible
* to Dequeue and Peek operations. The message content must be a UTF-8 encoded string that is up to 64KB in size.
*
* @param {Partial<Activity>} activity The [Activity](xref:botframework-schema.Activity) to be queued for later processing.
* @param {number} visibilityTimeout Visibility timeout in seconds. Optional with a default value of 0. Cannot be larger than 7 days.
* @param {number} timeToLive Specifies the time-to-live interval for the message in seconds.
*/
public abstract queueActivity(
activity: Partial<Activity>,
visibilityTimeout?: number,
timeToLive?: number
): Promise<string>;
}

Просмотреть файл

@ -0,0 +1,132 @@
/**
* @module botbuilder-dialogs-adaptive
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
BoolExpression,
BoolExpressionConverter,
Expression,
StringExpression,
StringExpressionConverter,
ValueExpression,
ValueExpressionConverter,
} from 'adaptive-expressions';
import {
Activity,
ActivityEventNames,
ActivityTypes,
ConversationReference,
QueueStorage,
TurnContext,
} from 'botbuilder-core';
import {
Converter,
ConverterFactory,
Dialog,
DialogConfiguration,
DialogContext,
DialogTurnResult,
DialogTurnStateConstants,
} from 'botbuilder-dialogs';
export interface ContinueConversationLaterConfiguration extends DialogConfiguration {
disabled?: boolean | string | Expression | BoolExpression;
date?: string | Expression | StringExpression;
value?: unknown | ValueExpression;
}
/**
* Action which schedules a conversation to be continued later by writing an EventActivity(Name=ContinueConversation) to a queue.
*/
export class ContinueConversationLater extends Dialog implements ContinueConversationLaterConfiguration {
public static $kind = 'Microsoft.ContinueConversationLater';
/**
* Gets or sets an optional expression which if is true will disable this action.
*/
public disabled?: BoolExpression;
/**
* Gets or sets the expression which resolves to the date/time to continue the conversation.
*/
public date: StringExpression;
/**
* Gets or sets the value to pass in the EventActivity payload.
*/
public value: ValueExpression;
public getConverter(property: keyof ContinueConversationLaterConfiguration): Converter | ConverterFactory {
switch (property) {
case 'disabled':
return new BoolExpressionConverter();
case 'date':
return new StringExpressionConverter();
case 'value':
return new ValueExpressionConverter();
default:
return super.getConverter(property);
}
}
/**
* Called when the [Dialog](xref:botbuilder-dialogs.Dialog) is started and pushed onto the dialog stack.
*
* @param {DialogContext} dc The [DialogContext](xref:botbuilder-dialogs.DialogContext) for the current turn of conversation.
* @param {object} options Optional. Initial information to pass to the dialog.
* @returns {Promise<DialogTurnResult>} A `Promise` representing the asynchronous operation.
*/
public async beginDialog(dc: DialogContext, options?: Record<string, unknown>): Promise<DialogTurnResult> {
if (this.disabled && this.disabled.getValue(dc.state)) {
return dc.endDialog();
}
const dateString = this.date.getValue(dc.state);
const date = Date.parse(dateString);
if (!date || isNaN(date)) {
throw new Error('Date is invalid');
}
if (date <= Date.now()) {
throw new Error('Date must be in the future');
}
// create ContinuationActivity from the conversation reference.
const reference = TurnContext.getConversationReference(dc.context.activity);
const activity: Partial<Activity> = TurnContext.applyConversationReference(
{
type: ActivityTypes.Event,
name: ActivityEventNames.ContinueConversation,
relatesTo: reference as ConversationReference,
},
reference,
true
);
activity.value = this.value && this.value.getValue(dc.state);
const visibility = (date - Date.now()) / 1000;
const ttl = visibility + 2 * 60;
const queueStorage: QueueStorage = dc.context.turnState.get(DialogTurnStateConstants.queueStorage);
if (!queueStorage) {
throw new Error('Unable to locate QueueStorage in HostContext');
}
const receipt = await queueStorage.queueActivity(activity, visibility, ttl);
// return the receipt as the result.
return dc.endDialog(receipt);
}
/**
* @protected
* @returns {string} A `string` representing the compute Id.
*/
protected onComputeId(): string {
return `ContinueConversationLater[${this.date && this.date.toString()}s]`;
}
}

Просмотреть файл

@ -15,6 +15,7 @@ export * from './cancelAllDialogs';
export * from './cancelDialog';
export * from './case';
export * from './codeAction';
export * from './continueConversationLater';
export * from './continueLoop';
export * from './deleteActivity';
export * from './deleteProperties';

Просмотреть файл

@ -24,6 +24,8 @@ import {
CancelAllDialogs,
CancelDialog,
CancelAllDialogsBaseConfiguration,
ContinueConversationLater,
ContinueConversationLaterConfiguration,
ContinueLoop,
ContinueLoopConfiguration,
DeleteActivity,
@ -97,6 +99,7 @@ import {
OnChoosePropertyConfiguration,
OnCondition,
OnConditionConfiguration,
OnContinueConversation,
OnConversationUpdateActivity,
OnDialogEvent,
OnDialogEventConfiguration,
@ -210,6 +213,9 @@ export class AdaptiveComponentRegistration extends ComponentRegistration impleme
this._addDeclarativeType<BreakLoop, BreakLoopConfiguration>(BreakLoop);
this._addDeclarativeType<CancelAllDialogs, CancelAllDialogsBaseConfiguration>(CancelAllDialogs);
this._addDeclarativeType<CancelDialog, CancelAllDialogsBaseConfiguration>(CancelDialog);
this._addDeclarativeType<ContinueConversationLater, ContinueConversationLaterConfiguration>(
ContinueConversationLater
);
this._addDeclarativeType<ContinueLoop, ContinueLoopConfiguration>(ContinueLoop);
this._addDeclarativeType<DeleteActivity, DeleteActivityConfiguration>(DeleteActivity);
this._addDeclarativeType<DeleteProperties, DeletePropertiesConfiguration>(DeleteProperties);
@ -250,6 +256,7 @@ export class AdaptiveComponentRegistration extends ComponentRegistration impleme
this._addDeclarativeType<OnChooseIntent, OnChooseIntentConfiguration>(OnChooseIntent);
this._addDeclarativeType<OnChooseProperty, OnChoosePropertyConfiguration>(OnChooseProperty);
this._addDeclarativeType<OnCondition, OnConditionConfiguration>(OnCondition);
this._addDeclarativeType<OnContinueConversation, OnActivityConfiguration>(OnContinueConversation);
this._addDeclarativeType<OnConversationUpdateActivity, OnActivityConfiguration>(OnConversationUpdateActivity);
this._addDeclarativeType<OnDialogEvent, OnDialogEventConfiguration>(OnDialogEvent);
this._addDeclarativeType<OnEndOfActions, OnDialogEventConfiguration>(OnEndOfActions);

Просмотреть файл

@ -13,6 +13,7 @@ export * from './onChooseEntity';
export * from './onChooseIntent';
export * from './onChooseProperty';
export * from './onCondition';
export * from './onContinueConversation';
export * from './onConversationUpdateActivity';
export * from './onDialogEvent';
export * from './onEndOfActions';

Просмотреть файл

@ -0,0 +1,41 @@
/**
* @module botbuilder-dialogs-adaptive
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Expression, ExpressionParserInterface } from 'adaptive-expressions';
import { Dialog, TurnPath } from 'botbuilder-dialogs';
import { OnEventActivity } from './onEventActivity';
/**
* Actions triggered when an EventActivity is received.
*/
export class OnContinueConversation extends OnEventActivity {
public static $kind = 'Microsoft.OnContinueConversation';
/**
* Initializes a new instance of the [OnContinueConversation](xref:botbuilder-dialogs-adaptive.OnContinueConversation) class.
*
* @param {Dialog[]} actions Optional. A [Dialog](xref:botbuilder-dialogs.Dialog) list containing the actions to add to the plan when the rule constraints are met.
* @param {string} condition Optional. Condition which needs to be met for the actions to be executed.
*/
public constructor(actions: Dialog[] = [], condition?: string) {
super(actions, condition);
}
/**
* Gets this activity representing expression.
*
* @param {ExpressionParserInterface} parser [ExpressionParserInterface](xref:adaptive-expressions.ExpressionParserInterface) used to parse a string into an [Expression](xref:adaptive-expressions.Expression).
* @returns {Expression} An [Expression](xref:adaptive-expressions.Expression) representing the [Activity](xref:botframework-schema.Activity).
*/
public getExpression(parser: ExpressionParserInterface): Expression {
// add constraints for activity type
return Expression.andExpression(
Expression.parse(`${TurnPath.activity}.name == 'ContinueConversation'`),
super.getExpression(parser)
);
}
}

Просмотреть файл

@ -12,4 +12,5 @@
export class DialogTurnStateConstants {
static dialogManager = Symbol('dialogManager');
static telemetryClient = Symbol('telemetryClient');
static queueStorage = Symbol('queueStorage');
}

Просмотреть файл

@ -15,6 +15,7 @@
"botbuilder-applicationinsights": "4.1.6",
"botbuilder-azure": "4.1.6",
"botbuilder-azure-blobs": "4.1.6",
"botbuilder-azure-queues": "4.1.6",
"botbuilder-core": "4.1.6",
"botbuilder-dialogs": "4.1.6",
"botbuilder-dialogs-adaptive": "4.1.6",

Просмотреть файл

@ -5,6 +5,7 @@ import * as botbuilderAiOrchestrator from 'botbuilder-ai-orchestrator';
import * as botbuilderApplicationInsights from 'botbuilder-applicationinsights';
import * as botbuilderAzure from 'botbuilder-azure';
import * as botbuilderAzureBlobs from 'botbuilder-azure-blobs';
import * as botbuilderAzureQueues from 'botbuilder-azure-queues';
import * as botbuilderCore from 'botbuilder-core';
import * as botbuilderDialogs from 'botbuilder-dialogs';
import * as botbuilderDialogsAdaptive from 'botbuilder-dialogs-adaptive';
@ -24,6 +25,7 @@ console.log(Object.keys(botbuilderAiOrchestrator));
console.log(Object.keys(botbuilderApplicationInsights));
console.log(Object.keys(botbuilderAzure));
console.log(Object.keys(botbuilderAzureBlobs));
console.log(Object.keys(botbuilderAzureQueues));
console.log(Object.keys(botbuilderCore));
console.log(Object.keys(botbuilderDialogs));
console.log(Object.keys(botbuilderDialogsAdaptive));

Просмотреть файл

@ -53,6 +53,27 @@
uuid "^8.1.0"
xml2js "^0.4.19"
"@azure/core-http@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@azure/core-http/-/core-http-1.2.0.tgz#eb2a1da9bdba8407a09d78450af5f13f8cc43d63"
integrity sha512-SQmyI1tpstWKePNmTseEUp8PMq1uNBslvGBrYF2zNM/fEfLD1q64XCatoH8nDQtSmDydEPsqlyyLSjjnuXrlOQ==
dependencies:
"@azure/abort-controller" "^1.0.0"
"@azure/core-auth" "^1.1.3"
"@azure/core-tracing" "1.0.0-preview.9"
"@azure/logger" "^1.0.0"
"@opentelemetry/api" "^0.10.2"
"@types/node-fetch" "^2.5.0"
"@types/tunnel" "^0.0.1"
form-data "^3.0.0"
node-fetch "^2.6.0"
process "^0.11.10"
tough-cookie "^4.0.0"
tslib "^2.0.0"
tunnel "^0.0.6"
uuid "^8.3.0"
xml2js "^0.4.19"
"@azure/core-lro@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@azure/core-lro/-/core-lro-1.0.2.tgz#b7b51ff7b84910b7eb152a706b0531d020864f31"
@ -141,6 +162,19 @@
events "^3.0.0"
tslib "^2.0.0"
"@azure/storage-queue@^12.2.0":
version "12.2.0"
resolved "https://registry.yarnpkg.com/@azure/storage-queue/-/storage-queue-12.2.0.tgz#0c82a0531d82358ee57754c21e1a1fce4ae5e408"
integrity sha512-HxFMLs6f0VQ4q1pyj4BKpdgH2amwvSwcNh+SFFcOmVuu6PpjTgP1HLiVPAf982iZlLEAEwNjzwnw2XrLbDumMA==
dependencies:
"@azure/abort-controller" "^1.0.0"
"@azure/core-http" "^1.2.0"
"@azure/core-paging" "^1.1.1"
"@azure/core-tracing" "1.0.0-preview.9"
"@azure/logger" "^1.0.0"
"@opentelemetry/api" "^0.10.2"
tslib "^2.0.0"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
@ -11819,7 +11853,7 @@ uuid@^3.0.0, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.1.0:
uuid@^8.1.0, uuid@^8.3.0:
version "8.3.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==