Skip to content

feat: Add FluentDialog as a flexible alternative to WaterFallDialog #4864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions libraries/botbuilder-dialogs-fluent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**/node_modules
/**/.vscode
/**/lib/*
coverage
.nyc_output
19 changes: 19 additions & 0 deletions libraries/botbuilder-dialogs-fluent/.nycrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extension": [
".js"
],
"include": [
"lib/**/*.js"
],
"exclude": [
"**/node_modules/**",
"**/tests/**",
"**/coverage/**",
"**/*.d.ts"
],
"reporter": [
"html"
],
"all": true,
"cache": true
}
21 changes: 21 additions & 0 deletions libraries/botbuilder-dialogs-fluent/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Microsoft Corporation. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
132 changes: 132 additions & 0 deletions libraries/botbuilder-dialogs-fluent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Bot Builder Fluent Dialog

A Microsoft BotBuilder dialog implementation using event sourcing.

- [Installing](#installing)
- [Basic Use](#use)
- [Learn More](#learn-more)
- [Documentation](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
- [Class Reference](https://docs.microsoft.com/en-us/javascript/api/botbuilder-dialogs/)
- [GitHub Repo](https://github.com/Microsoft/botbuilder-js)
- [Report Issues](https://github.com/Microsoft/botbuilder-js/issues)

## Installing
To add the latest version of this package to your bot:

```bash
npm install --save botbuilder-dialogs-fluent
```

#### How to Use Daily Builds
If you want to play with the very latest versions of botbuilder, you can opt in to working with the daily builds. This is not meant to be used in a production environment and is for advanced development. Quality will vary and you should only use daily builds for exploratory purposes.

To get access to the daily builds of this library, configure npm to use the MyGet feed before installing.

```bash
npm config set registry https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/
```

To reset the registry in order to get the latest published version, run:
```bash
npm config set registry https://registry.npmjs.org/
```

## What's included?

This module includes a dialog implementation using an approach similar to a durable function. The FluentDialog uses event sourcing to enable arbitrarily complex user interactions in a seemingly uninterrupted execution flow.
Behind the scenes, the yield operator in the dialog flow function yields control of the execution thread back to a dialog flow dispatcher. The dispatcher then commits any new actions that the dialog flow function scheduled (such as starting a child dialog, receiving an activity or making an async call) to storage.
The transparent commit action updates the execution history of the dialog flow by appending all new events into the dialog state, much like an append-only log.
Once the history is updated, the dialog ends its turn and, when it is later resumed, the dispatcher re-executes the entire function from the start to rebuild the local state.
During the replay, if the code tries to begin a child dialog (or do any async work), the dispatcher consults the execution history, replays that result and the function code continues to run.
The replay continues until the function code is finished or until it yields a new suspension task.

## Use

After adding the module to your application, modify your app's code to import the multi-turn dialog management capabilities. Near your other `import` and `require` statements, add:

```javascript
// Import some of the capabities from the module.
const { DialogSet, TextPrompt, ConfirmPrompt } = require("botbuilder-dialogs");
const { FluentDialog } = require("botbuilder-dialogs-fluent");
```

Then, create one or more `DialogSet` objects to manage the dialogs used in your bot.
A DialogSet is used to collect and execute dialogs. A bot may have more than one
DialogSet, which can be used to group dialogs logically and avoid name collisions.

Then, create one or more dialogs and add them to the DialogSet. Use the WaterfallDialog
class to construct dialogs defined by a series of functions for sending and receiving input
that will be executed in order.

More sophisticated multi-dialog sets can be created using the `ComponentDialog` class, which
contains a DialogSet, is itself also a dialog that can be triggered like any other. By building on top ComponentDialog,
developer can bundle multiple dialogs into a single unit which can then be packaged, distributed and reused.

```javascript
// Set up a storage system that will capture the conversation state.
const storage = new MemoryStorage();
const convoState = new ConversationState(storage);

// Define a property associated with the conversation state.
const dialogState = convoState.createProperty('dialogState');

// Initialize a DialogSet, passing in a property used to capture state.
const dialogs = new DialogSet(dialogState);

// Each dialog is identified by a unique name used to invoke the dialog later.
const MAIN_DIALOG = 'MAIN_DIALOG';
const TEXT_PROMPT = 'TEXT_PROMPT'
const CONFIRM_PROMPT = 'CONFIRM_PROMPT'

// Implement the dialog flow function
function *dialogFlow(context) {

let response = yield context.prompt(DIALOG_PROMPT, 'say something');
yield context.sendActivity(`you said: ${response}`);

let shouldContinue = yield context.prompt(CONFIRM_PROMPT, 'play another round?', ['yes', 'no'])
if (shouldContinue) {
yield context.restart();
}

yield context.sendActivity('good bye!');
}

// Add a dialog. Use the included FluentDialog type, initialized with the dialog flow function
dialogs.add(new FluentDialog(MAIN_DIALOG, dialogFlow));
dialogs.add(new TextPrompt(DIALOG_PROMPT));
dialogs.add(new ConfirmPrompt(CONFIRM_PROMPT));

```

Finally, from somewhere in your bot's code, invoke your dialog by name:

```javascript
// Receive and process incoming events into TurnContext objects in the normal way
adapter.processActivity(req, res, async (turnContext) => {
// Create a DialogContext object from the incoming TurnContext
const dc = await dialogs.createContext(turnContext);

// ...evaluate message and do other bot logic...

// If the bot hasn't yet responded, try to continue any active dialog
if (!turnContext.responded) {
const results = await dc.continueDialog();
if (results.status === DialogTurnStatus.empty) {
await dialogContext.beginDialog(MAIN_DIALOG);
}
}
});
```

## Examples

See this module in action in these example apps:

# Learn More

[Prompts](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-prompts?view=azure-bot-service-4.0&tabs=javascript) This module contains several types of built-in prompt that can be used to create dialogs that capture and validate specific data types like dates, numbers and multiple-choice answers.

[DialogSet](https://docs.microsoft.com/en-us/javascript/api/botbuilder-dialogs/dialogset) DialogSet is a container for multiple dialogs. Once added to a DialogSet, dialogs can be called and interlinked.

[ComponentDialog](https://docs.microsoft.com/en-us/javascript/api/botbuilder-dialogs/componentdialog) ComponentDialogs are containers that encapsulate multiple sub-dialogs, but can be invoked like normal dialogs. This is useful for re-usable dialogs, or creating multiple dialogs with similarly named sub-dialogs that would otherwise collide.
11 changes: 11 additions & 0 deletions libraries/botbuilder-dialogs-fluent/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../api-extractor.json",
"mainEntryPointFilePath": "./lib/index.d.ts",
"messages": {
"compilerMessageReporting": {
"TS2321": {
"logLevel": "none"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## API Report File for "botbuilder-dialogs-fluent"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts

import { Activity } from 'botbuilder-core';
import { Choice } from 'botbuilder-dialogs';
import { Dialog } from 'botbuilder-dialogs';
import { DialogContext } from 'botbuilder-dialogs';
import { DialogReason } from 'botbuilder-dialogs';
import { DialogTurnResult } from 'botbuilder-dialogs';
import type { Jsonify } from 'type-fest';
import type { JsonValue } from 'type-fest';
import { PromptOptions } from 'botbuilder-dialogs';
import { ResourceResponse } from 'botbuilder-core';
import { TurnContext } from 'botbuilder-core';
import { ZodType } from 'zod';

// @public
export interface DialogFlowBoundCallable<A extends any[], R extends JsonValue> {
(...args: A): R;
project<T>(projector: (value: R) => T): (...args: A) => T;
}

// @public
export interface DialogFlowContext<O extends object = {}> {
bind<T extends (...args: any[]) => any>(func: T): (...args: Parameters<T>) => Jsonify<ReturnType<T>>;
call<T>(task: (context: TurnContext) => Promise<T>): DialogFlowTask<T>;
callAsUser<T>(oauthDialogId: string, task: (token: string, context: TurnContext) => Promise<T>): DialogFlowTask<T>;
callDialog<T = any>(dialogId: string, options?: object): DialogFlowTask<T>;
get channelId(): string;
get currentUtcTime(): Date;
get dialogId(): string;
get isReplaying(): boolean;
newGuid(): string;
get options(): O;
prompt<T>(dialogId: string, promptOrOptions: string | Partial<Activity> | PromptOptions): DialogFlowTask<T>;
prompt<T>(dialogId: string, promptOrOptions: string | Partial<Activity> | PromptOptions, choices: (string | Choice)[]): DialogFlowTask<T>;
receiveActivity(): DialogFlowTask<Activity>;
restart(options?: O): DialogFlowTask<never>;
sendActivity(activityOrText: string | Partial<Activity>, speak?: string, inputHint?: string): DialogFlowTask<ResourceResponse | undefined>;
}

// @public
export class DialogFlowError extends Error {
constructor(message: string);
}

// @public
export interface DialogFlowTask<R = any, O = Jsonify<R>> {
configureRetry(shouldRetry: boolean): this;
configureRetry(settings: RetrySettings): this;
configureRetry(policy: RetryPolicy): this;
get id(): string;
get kind(): string;
project<T>(projector: (value: Jsonify<R>) => T): DialogFlowTask<R, T>;
project<T>(schema: ZodType<T>): DialogFlowTask<R, T>;
result(): Generator<DialogFlowTask, O, O>;
then<T>(continuation: (value: R, context: TurnContext) => T | Promise<T>): DialogFlowTask<T>;
}

// @public
export type ErrorFilter = (error: any) => boolean;

// @public
export function exponentialRetry(initialDelay: number, maxDelay: number): RetryDelay;

// @public
export class FluentDialog<O extends object = {}, T = any> extends Dialog<O> {
constructor(dialogId: string, dialogFlow: (context: DialogFlowContext<O>) => Generator<DialogFlowTask, T>);
// (undocumented)
beginDialog(dc: DialogContext, options?: O): Promise<DialogTurnResult>;
// (undocumented)
continueDialog(dc: DialogContext): Promise<DialogTurnResult>;
// (undocumented)
resumeDialog(dc: DialogContext, reason: DialogReason, result?: any): Promise<DialogTurnResult>;
protected runWorkflow(dc: DialogContext, reason: DialogReason, result?: any): Promise<DialogTurnResult>;
}

// @public
export const immediateRetry: RetryDelay;

// @public
export function linearRetry(retryDelay: number): RetryDelay;

// @public
export const noRetry: RetryPolicy;

// @public
export const retryAny: ErrorFilter;

// @public
export type RetryDelay = (attempt: number) => number;

// @public
export type RetryPolicy = (error: any, attempt: number) => Promise<boolean>;

// @public
export function retryPolicy(settings: RetrySettings): RetryPolicy;

// @public
export interface RetrySettings {
readonly errorFilter?: ErrorFilter;
readonly maxAttempts: number;
readonly retryDelay: RetryDelay;
}

// (No @packageDocumentation comment for this package)

```
51 changes: 51 additions & 0 deletions libraries/botbuilder-dialogs-fluent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "botbuilder-dialogs-fluent",
"author": "Microsoft Corp.",
"description": "Fluent execution flow library for the Microsoft BotBuilder dialog system.",
"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": {
"botbuilder-core": "4.1.6",
"botbuilder-dialogs": "4.1.6",
"zod": "^3.23.8",
"type-fest": "3.13.1"
},
"scripts": {
"build": "tsc -b",
"build-docs": "typedoc --theme markdown --entryPoint botbuilder-dialogs-fluent --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\botbuilder-dialogs .\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK - Dialogs\" --readme none",
"clean": "rimraf _ts3.4 lib tsconfig.tsbuildinfo",
"depcheck": "depcheck --config ../../.depcheckrc --ignores botbuilder-ai,botbuilder-dialogs-adaptive",
"lint": "eslint . --config ../../eslint.config.cjs",
"postbuild": "downlevel-dts lib _ts3.4/lib --checksum",
"test": "yarn build && nyc mocha tests/**/*.test.js --exit",
"test:compat": "api-extractor run --verbose"
},
"files": [
"_ts3.4",
"lib",
"src"
]
}
30 changes: 30 additions & 0 deletions libraries/botbuilder-dialogs-fluent/src/dialogFlowBoundCallable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { JsonValue } from 'type-fest';

/**
* Interface for a callable that is bound to a dialog flow context.
*
* @template A The parameter types of the bound function.
* @template R The return type of the bound function.
*/
export interface DialogFlowBoundCallable<A extends any[], R extends JsonValue> {
/**
* Invokes the bound function with the given arguments.
*
* @param args The arguments to pass to the function.
* @returns The observable result of the function call.
*/
(...args: A): R;

/**
* Gets a new function that has the same arguments as the bound function and returns an observable
* value of a different type.
*
* @template T The type of the observable value produced by the projector
* @param projector The callback used to convert the deserialized result to its observable value
* @returns The projected function.
*/
project<T>(projector: (value: R) => T): (...args: A) => T;
}
Loading
Loading