ported BBNode project to VS Code and restructured things a bit for GIT

This commit is contained in:
Steven Ickman 2016-03-16 20:29:23 -07:00
Родитель b5ba392b88
Коммит 7fdbfc7373
33 изменённых файлов: 8938 добавлений и 0 удалений

27
Node/package.json Normal file
Просмотреть файл

@ -0,0 +1,27 @@
{
"name": "botbuilder",
"author": "Microsoft Corp.",
"description": "Bot Builder is a dialog system for building rich bots on virtually any platform.",
"version": "0.5.0",
"license": "MIT",
"keywords": [
"bots",
"chatbots"
],
"bugs": {
"url": "https://github.com/Microsoft/BotBuilder/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/BotBuilder.git"
},
"main": "./lib/botbuilder.js",
"typings": "./lib/botbuilder.d.ts",
"dependencies": {
"chrono-node": "^1.1.3",
"node-uuid": "^1.4.7",
"request": "^2.69.0",
"skype-botkit": "http://skypetriviabot.blob.core.windows.net/botkit/skype-botkit.tar.gz",
"sprintf-js": "^1.0.3"
}
}

190
Node/src/.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,190 @@
// Available variables which can be used inside of strings.
// ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
// A task runner that calls the Typescript compiler (tsc) and
// Compiles a HelloWorld.ts program
/*
{
"version": "0.1.0",
// The command is tsc. Assumes that tsc has been installed using npm install -g typescript
"command": "tsc",
// The command is a shell script
"isShellCommand": true,
// Show the output window only if unrecognized errors occur.
"showOutput": "silent",
// args is the HelloWorld program to compile.
"args": ["HelloWorld.ts"],
// use the standard tsc problem matcher to find compile problems
// in the output.
"problemMatcher": "$tsc"
}
*/
// A task runner that calls the Typescript compiler (tsc) and
// compiles based on a tsconfig.json file that is present in
// the root of the folder open in VSCode
{
"version": "0.1.0",
// The command is tsc. Assumes that tsc has been installed using npm install -g typescript
"command": "tsc",
// The command is a shell script
"isShellCommand": true,
// Show the output window only if unrecognized errors occur.
"showOutput": "silent",
// Tell the tsc compiler to use the tsconfig.json from the open folder.
"args": ["-p", "."],
// use the standard tsc problem matcher to find compile problems
// in the output.
"problemMatcher": "$tsc"
}
// A task runner configuration for gulp. Gulp provides a less task
// which compiles less to css.
/*
{
"version": "0.1.0",
"command": "gulp",
"isShellCommand": true,
"tasks": [
{
"taskName": "less",
// Make this the default build command.
"isBuildCommand": true,
// Show the output window only if unrecognized errors occur.
"showOutput": "silent",
// Use the standard less compilation problem matcher.
"problemMatcher": "$lessCompile"
}
]
}
*/
// Uncomment the following section to use jake to build a workspace
// cloned from https://github.com/Microsoft/TypeScript.git
/*
{
"version": "0.1.0",
// Task runner is jake
"command": "jake",
// Need to be executed in shell / cmd
"isShellCommand": true,
"showOutput": "silent",
"tasks": [
{
// TS build command is local.
"taskName": "local",
// Make this the default build command.
"isBuildCommand": true,
// Show the output window only if unrecognized errors occur.
"showOutput": "silent",
// Use the redefined Typescript output problem matcher.
"problemMatcher": [
"$tsc"
]
}
]
}
*/
// Uncomment the section below to use msbuild and generate problems
// for csc, cpp, tsc and vb. The configuration assumes that msbuild
// is available on the path and a solution file exists in the
// workspace folder root.
/*
{
"version": "0.1.0",
"command": "msbuild",
"args": [
// Ask msbuild to generate full paths for file names.
"/property:GenerateFullPaths=true"
],
"taskSelector": "/t:",
"showOutput": "silent",
"tasks": [
{
"taskName": "build",
// Show the output window only if unrecognized errors occur.
"showOutput": "silent",
// Use the standard MS compiler pattern to detect errors, warnings
// and infos in the output.
"problemMatcher": "$msCompile"
}
]
}
*/
// Uncomment the following section to use msbuild which compiles Typescript
// and less files.
/*
{
"version": "0.1.0",
"command": "msbuild",
"args": [
// Ask msbuild to generate full paths for file names.
"/property:GenerateFullPaths=true"
],
"taskSelector": "/t:",
"showOutput": "silent",
"tasks": [
{
"taskName": "build",
// Show the output window only if unrecognized errors occur.
"showOutput": "silent",
// Use the standard MS compiler pattern to detect errors, warnings
// and infos in the output.
"problemMatcher": [
"$msCompile",
"$lessCompile"
]
}
]
}
*/
// A task runner example that defines a problemMatcher inline instead of using
// a predefined one.
/*
{
"version": "0.1.0",
"command": "tsc",
"isShellCommand": true,
"args": ["HelloWorld.ts"],
"showOutput": "silent",
"problemMatcher": {
// The problem is owned by the typescript language service. Ensure that the problems
// are merged with problems produced by Visual Studio's language service.
"owner": "typescript",
// The file name for reported problems is relative to the current working directory.
"fileLocation": ["relative", "${cwd}"],
// The actual pattern to match problems in the output.
"pattern": {
// The regular expression. Matches HelloWorld.ts(2,10): error TS2339: Property 'logg' does not exist on type 'Console'.
"regexp": "^([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
// The match group that denotes the file containing the problem.
"file": 1,
// The match group that denotes the problem location.
"location": 2,
// The match group that denotes the problem's severity. Can be omitted.
"severity": 3,
// The match group that denotes the problem code. Can be omitted.
"code": 4,
// The match group that denotes the problem's message.
"message": 5
}
}
}
*/

25
Node/src/Scripts/typings/chrono-node/chrono-node.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,25 @@

declare module chrono_node {
export class ParsedResult {
start: ParsedComponents;
end: ParsedComponents;
index: number;
text: string;
ref: Date;
}
export class ParsedComponents {
assign(component: string, value: number): void;
imply(component: string, value: number): void;
get(component: string): number;
isCertain(component: string): boolean;
date(): Date;
}
export function parseDate(text: string, refDate?: Date, opts?: any): Date;
export function parse(text: string, refDate?: Date, opts?: any): ParsedResult[];
}
declare module "chrono-node" {
export = chrono_node;
}

1817
Node/src/Scripts/typings/express/express.d.ts поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

16
Node/src/Scripts/typings/form-data/form-data.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,16 @@
// Type definitions for form-data
// Project: https://github.com/felixge/node-form-data
// Definitions by: Carlos Ballesteros Velasco <https://github.com/soywiz>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
// Imported from: https://github.com/soywiz/typescript-node-definitions/form-data.d.ts
declare module "form-data" {
export class FormData {
append(key: string, value: any, options?: any): FormData;
getHeaders(): Object;
// TODO expand pipe
pipe(to: any): any;
submit(params: string|Object, callback: (error: any, response: any) => void): any;
}
}

46
Node/src/Scripts/typings/node-uuid/node-uuid-base.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,46 @@
// Type definitions for node-uuid.js
// Project: https://github.com/broofa/node-uuid
// Definitions by: Jeff May <https://github.com/jeffmay>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
/** Common definitions for all environments */
declare namespace __NodeUUID {
interface UUIDOptions {
/**
* Node id as Array of 6 bytes (per 4.1.6).
* Default: Randomly generated ID. See note 1.
*/
node?: any[];
/**
* (Number between 0 - 0x3fff) RFC clock sequence.
* Default: An internally maintained clockseq is used.
*/
clockseq?: number;
/**
* (Number | Date) Time in milliseconds since unix Epoch.
* Default: The current time is used.
*/
msecs?: number|Date;
/**
* (Number between 0-9999) additional time, in 100-nanosecond units. Ignored if msecs is unspecified.
* Default: internal uuid counter is used, as per 4.2.1.2.
*/
nsecs?: number;
}
interface UUID {
v1(options?: UUIDOptions): string;
v1(options?: UUIDOptions, buffer?: number[], offset?: number): number[];
v4(options?: UUIDOptions): string;
v4(options?: UUIDOptions, buffer?: number[], offset?: number): number[];
parse(id: string, buffer?: number[], offset?: number): number[];
unparse(buffer: number[], offset?: number): string;
}
}

15
Node/src/Scripts/typings/node-uuid/node-uuid-cjs.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,15 @@
// Type definitions for node-uuid.js
// Project: https://github.com/broofa/node-uuid
// Definitions by: Jeff May <https://github.com/jeffmay>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
/// <reference path="./node-uuid-base.d.ts" />
/**
* Expose as CommonJS module
* For use in node environment or browser environment (using webpack or other module loaders)
*/
declare module "node-uuid" {
var uuid: __NodeUUID.UUID;
export = uuid;
}

36
Node/src/Scripts/typings/node-uuid/node-uuid.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,36 @@
// Type definitions for node-uuid.js
// Project: https://github.com/broofa/node-uuid
// Definitions by: Jeff May <https://github.com/jeffmay>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
/// <reference path="../node/node.d.ts" />
/// <reference path="./node-uuid-base.d.ts" />
/// <reference path="./node-uuid-cjs.d.ts" />
/**
* Definitions for use in node environment
*
* !! For browser enviroments, use node-uuid-global or node-uuid-cjs
*/
declare module __NodeUUID {
/**
* Overloads for node environment
* We need to duplicate some declarations because
* interface merging doesn't work with overloads
*/
interface UUID {
v1(options?: UUIDOptions): string;
v1(options?: UUIDOptions, buffer?: number[], offset?: number): number[];
v1(options?: UUIDOptions, buffer?: Buffer, offset?: number): Buffer;
v4(options?: UUIDOptions): string;
v4(options?: UUIDOptions, buffer?: number[], offset?: number): number[];
v4(options?: UUIDOptions, buffer?: Buffer, offset?: number): Buffer;
parse(id: string, buffer?: number[], offset?: number): number[];
parse(id: string, buffer?: Buffer, offset?: number): Buffer;
unparse(buffer: number[], offset?: number): string;
unparse(buffer: Buffer, offset?: number): string;
}
}

2093
Node/src/Scripts/typings/node/node.d.ts поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

186
Node/src/Scripts/typings/request/request.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,186 @@
// Type definitions for request
// Project: https://github.com/mikeal/request
// Definitions by: Carlos Ballesteros Velasco <https://github.com/soywiz>, bonnici <https://github.com/bonnici>, Bart van der Schoor <https://github.com/Bartvds>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
// Imported from: https://github.com/soywiz/typescript-node-definitions/d.ts
/// <reference path="../node/node.d.ts" />
/// <reference path="../form-data/form-data.d.ts" />
declare module 'request' {
import stream = require('stream');
import http = require('http');
import FormData = require('form-data');
import url = require('url');
export = RequestAPI;
function RequestAPI(uri: string, options?: RequestAPI.Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): RequestAPI.Request;
function RequestAPI(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): RequestAPI.Request;
function RequestAPI(options: RequestAPI.Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): RequestAPI.Request;
module RequestAPI {
export function defaults(options: Options): typeof RequestAPI;
export function request(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function request(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function request(options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function get(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function get(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function get(options: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function post(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function post(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function post(options: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function put(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function put(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function put(options: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function head(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function head(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function head(options: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function patch(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function patch(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function patch(options: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function del(uri: string, options?: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function del(uri: string, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function del(options: Options, callback?: (error: any, response: http.IncomingMessage, body: any) => void): Request;
export function forever(agentOptions: any, optionsArg: any): Request;
export function jar(): CookieJar;
export function cookie(str: string): Cookie;
export var initParams: any;
export interface Options {
url?: string;
uri?: string;
callback?: (error: any, response: http.IncomingMessage, body: any) => void;
jar?: any; // CookieJar
formData?: any; // Object
form?: any; // Object or string
auth?: AuthOptions;
oauth?: OAuthOptions;
aws?: AWSOptions;
hawk ?: HawkOptions;
qs?: any;
json?: any;
multipart?: RequestPart[];
agentOptions?: any;
agentClass?: any;
forever?: any;
host?: string;
port?: number;
method?: string;
headers?: Headers;
body?: any;
followRedirect?: boolean|((response: http.IncomingMessage) => boolean);
followAllRedirects?: boolean;
maxRedirects?: number;
encoding?: string;
pool?: any;
timeout?: number;
proxy?: any;
strictSSL?: boolean;
gzip?: boolean;
}
export interface RequestPart {
headers?: Headers;
body: any;
}
export interface Request extends stream.Stream {
readable: boolean;
writable: boolean;
getAgent(): http.Agent;
//start(): void;
//abort(): void;
pipeDest(dest: any): void;
setHeader(name: string, value: string, clobber?: boolean): Request;
setHeaders(headers: Headers): Request;
qs(q: Object, clobber?: boolean): Request;
form(): FormData.FormData;
form(form: any): Request;
multipart(multipart: RequestPart[]): Request;
json(val: any): Request;
aws(opts: AWSOptions, now?: boolean): Request;
auth(username: string, password: string, sendInmediately?: boolean, bearer?: string): Request;
oauth(oauth: OAuthOptions): Request;
jar(jar: CookieJar): Request;
on(event: string, listener: Function): Request;
write(buffer: Buffer, cb?: Function): boolean;
write(str: string, cb?: Function): boolean;
write(str: string, encoding: string, cb?: Function): boolean;
write(str: string, encoding?: string, fd?: string): boolean;
end(): void;
end(chunk: Buffer, cb?: Function): void;
end(chunk: string, cb?: Function): void;
end(chunk: string, encoding: string, cb?: Function): void;
pause(): void;
resume(): void;
abort(): void;
destroy(): void;
toJSON(): string;
}
export interface Headers {
[key: string]: any;
}
export interface AuthOptions {
user?: string;
username?: string;
pass?: string;
password?: string;
sendImmediately?: boolean;
bearer?: string;
}
export interface OAuthOptions {
callback?: string;
consumer_key?: string;
consumer_secret?: string;
token?: string;
token_secret?: string;
verifier?: string;
}
export interface HawkOptions {
credentials: any;
}
export interface AWSOptions {
secret: string;
bucket?: string;
}
export interface CookieJar {
setCookie(cookie: Cookie, uri: string|url.Url, options?: any): void
getCookieString(uri: string|url.Url): string
getCookies(uri: string|url.Url): Cookie[]
}
export interface CookieValue {
name: string;
value: any;
httpOnly: boolean;
}
export interface Cookie extends Array<CookieValue> {
constructor(name: string, req: Request): void;
str: string;
expires: Date;
path: string;
toString(): string;
}
}
}

144
Node/src/Scripts/typings/skype-botkit/skype-botkit.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,144 @@

declare module 'skype-botkit' {
export function messagingHandler(botService: BotService): (req: any, res: any) => void;
function ensureHttps(redirect: any, errorStatus: any): (req: any, res: any, next: Function) => void;
function verifySkypeCert(options: any): (req: any, res: any, next: Function) => void;
export interface IBotServiceConfiguration {
messaging?: {
username: string;
appId: string;
appSecret: string;
};
requestTimeout?: number;
calling?: {
callbackUri: string;
};
}
export interface IMessage {
type: string;
from: string;
to: string;
content: any;
messageId: number;
contentType: string;
eventTime: number;
}
export interface IAttachment {
type: string;
from: string;
to: string;
id: string;
attachmentName: string;
attachmentType: string;
views: IAttachmentViewInfo[];
eventTime: string;
}
export interface IAttachmentViewInfo {
}
export interface IContactNotification {
type: string;
from: string;
to: string;
fromUserDisplayName: string;
action: string;
}
export interface IHistoryDisclosed {
type: string;
from: string;
to: string;
historyDisclosed: boolean;
eventTime: number;
}
export interface ITopicUpdated {
type: string;
from: string;
to: string;
topic: string;
eventTime: number;
}
export interface IUserAdded {
type: string;
from: string;
to: string;
targets: string[];
eventTime: number;
}
export interface IUserRemoved {
type: string;
from: string;
to: string;
targets: string[];
eventTime: number;
}
export interface ICommandCallback {
(bot: Bot, request: IMessage): void;
}
export interface ISendMessageCallback {
}
export interface ICallNotificationHandler {
conversationResult: IConversationResult;
workflow: IWorkflow;
callback: IFinishEventHandling;
}
export interface IConversationResult {
}
export interface IWorkflow {
}
export interface IFinishEventHandling {
error?: Error;
workflow?: IWorkflow;
}
export interface IIncomingCallHandler {
conversation: IConversation;
workflow: IWorkflow;
callback: IFinishEventHandling;
}
export interface IConversation {
}
export interface IProcessCallCallback {
error?: Error;
responseMessage?: string;
}
export class BotService {
constructor(configuration: IBotServiceConfiguration);
on(event: string, listener: Function): void;
onPersonalCommand(regex: RegExp, callback: ICommandCallback): void;
onGroupCommand(regex: RegExp, callback: ICommandCallback): void;
send(to: string, content: string, callback: ISendMessageCallback): void;
onAnswerCompleted(handler: ICallNotificationHandler): void;
onIncomingCall(handler: IIncomingCallHandler): void;
onCallStateChange(handler: Function): void;
onHangupCompleted(handler: Function): void;
onPlayPromptCompleted(handler: Function): void;
onRecognizeCompleted(handler: Function): void;
onRecordCompleted(handler: Function): void;
onRejectCompleted(handler: Function): void;
onWorkflowValidationCompleted(handler: Function): void;
processCall(content: any, callback: IProcessCallCallback): void;
processCallback(content: any, additionalData: any, callback: Function): void;
}
export class Bot {
reply(content: string, callback: ISendMessageCallback): void;
send(to: string, content: string, callback: ISendMessageCallback): void;
}
}

78
Node/src/Scripts/typings/sprintf-js/sprintf-js.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,78 @@
// Type definitions for sprintf-js
// Project: https://www.npmjs.com/package/sprintf-js
// Definitions by: Jason Swearingen <https://jasonswearingen.github.io>
// Definitions: https://github.com/borisyankov/DefinitelyTyped
/** sprintf.js is a complete open source JavaScript sprintf implementation for the browser and node.js.
Its prototype is simple:
string sprintf(string format , [mixed arg1 [, mixed arg2 [ ,...]]])
*/
declare module sprintf_js {
/** sprintf.js is a complete open source JavaScript sprintf implementation for the browser and node.js.
Its prototype is simple:
string sprintf(string format , [mixed arg1 [, mixed arg2 [ ,...]]])
==Placeholders==
The placeholders in the format string are marked by % and are followed by one or more of these elements. see "fmt" arg for more docs on placeholders.
==Argument swapping==
You can also swap the arguments. That is, the order of the placeholders doesn't have to match the order of the arguments. You can do that by simply indicating in the format string which arguments the placeholders refer to:
sprintf("%2$s %3$s a %1$s", "cracker", "Polly", "wants")
And, of course, you can repeat the placeholders without having to increase the number of arguments.
==Named arguments==
Format strings may contain replacement fields rather than positional placeholders. Instead of referring to a certain argument, you can now refer to a certain key within an object. Replacement fields are surrounded by rounded parentheses - ( and ) - and begin with a keyword that refers to a key:
var user = {name: "Dolly"}
sprintf("Hello %(name)s", user) // Hello Dolly
Keywords in replacement fields can be optionally followed by any number of keywords or indexes:
var users = [{name: "Dolly"},{name: "Molly"},{name: "Polly"}]
sprintf("Hello %(users[0].name)s, %(users[1].name)s and %(users[2].name)s", {users: users}) // Hello Dolly, Molly and Polly
Note: mixing positional and named placeholders is not (yet) supported
==Computed values==
You can pass in a function as a dynamic value and it will be invoked (with no arguments) in order to compute the value on-the-fly.
sprintf("Current timestamp: %d", Date.now) // Current timestamp: 1398005382890
sprintf("Current date and time: %s", function() { return new Date().toString() })
*/
export function sprintf(
/** The placeholders in the format string are marked by % and are followed by one or more of these elements, in this order:
An optional number followed by a $ sign that selects which argument index to use for the value. If not specified, arguments will be placed in the same order as the placeholders in the input string.
An optional + sign that forces to preceed the result with a plus or minus sign on numeric values. By default, only the - sign is used on negative numbers.
An optional padding specifier that says what character to use for padding (if specified). Possible values are 0 or any other character precedeed by a ' (single quote). The default is to pad with spaces.
An optional - sign, that causes sprintf to left-align the result of this placeholder. The default is to right-align the result.
An optional number, that says how many characters the result should have. If the value to be returned is shorter than this number, the result will be padded.
An optional precision modifier, consisting of a . (dot) followed by a number, that says how many digits should be displayed for floating point numbers. When used on a string, it causes the result to be truncated.
A type specifier that can be any of:
% - yields a literal % character
b - yields an integer as a binary number
c - yields an integer as the character with that ASCII value
d or i - yields an integer as a signed decimal number
e - yields a float using scientific notation
u - yields an integer as an unsigned decimal number
f - yields a float as is
o - yields an integer as an octal number
s - yields a string as is
x - yields an integer as a hexadecimal number (lower-case)
X - yields an integer as a hexadecimal number (upper-case)
*/
fmt: string,
/** */
...args: any[]
): string;
/** vsprintf is the same as sprintf except that it accepts an array of arguments, rather than a variable number of arguments:
vsprintf("The first 4 letters of the english alphabet are: %s, %s, %s and %s", ["a", "b", "c", "d"])
*/
export function vsprintf(fmt: string, args: any[]): string;
}
declare module "sprintf-js" {
export =sprintf_js;
}
declare var sprintf: typeof sprintf_js.sprintf;
declare var vsprintf: typeof sprintf_js.vsprintf;

245
Node/src/Session.ts Normal file
Просмотреть файл

@ -0,0 +1,245 @@
import collection = require('./dialogs/DialogCollection');
import dialog = require('./dialogs/Dialog');
import consts = require('./Consts');
import sprintf = require('sprintf-js');
import events = require('events');
export interface ISessionArgs {
dialogs: collection.DialogCollection;
dialogId: string;
dialogArgs?: any;
localizer?: ILocalizer;
}
export class Session extends events.EventEmitter implements ISession {
private msgSent = false;
private _isReset = false;
constructor(protected args: ISessionArgs) {
super();
this.dialogs = args.dialogs;
}
public dispatch(sessionState: ISessionState, message: IMessage): ISession {
var index = 0;
var handlers: { (session: Session, next: Function): void; }[];
var session = this;
var next = () => {
var handler = index < handlers.length ? handlers[index] : null;
if (handler) {
index++;
handler(session, next);
} else {
this.routeMessage();
}
};
// Dispatch message
this.sessionState = sessionState || { callstack: [], lastAccess: 0 };
this.sessionState.lastAccess = new Date().getTime();
this.message = message || { text: '' };
if (!this.message.type) {
this.message.type = 'Message';
}
handlers = this.dialogs.getMiddleware();
next();
return this;
}
public dialogs: collection.DialogCollection;
public sessionState: ISessionState;
public message: IMessage;
public userData: any;
public dialogData: any;
public error(err: Error): ISession {
err = err instanceof Error ? err : new Error(err.toString());
console.error('Session Error: ' + err.message);
this.emit('error', err);
return this;
}
public gettext(msgid: string, ...args: any[]): string {
return this.vgettext(msgid, args);
}
public ngettext(msgid: string, msgid_plural: string, count: number): string {
var tmpl: string;
if (this.args.localizer && this.message) {
tmpl = this.args.localizer.ngettext(this.message.language || '', msgid, msgid_plural, count);
} else if (count == 1) {
tmpl = msgid;
} else {
tmpl = msgid_plural;
}
return sprintf.sprintf(tmpl, count);
}
public send(): ISession;
public send(msg: string, ...args: any[]): ISession;
public send(msg: IMessage): ISession;
public send(msg?: any, ...args: any[]): ISession {
// Update dialog state
// - Deals with a situation where the user assigns a whole new object to dialogState.
var ss = this.sessionState;
if (ss.callstack.length > 0) {
ss.callstack[ss.callstack.length - 1].state = this.dialogData || {};
}
// Compose message
this.msgSent = true;
var message: IMessage = typeof msg == 'string' ? this.createMessage(msg, args) : msg;
this.emit('send', message);
return this;
}
public messageSent(): boolean {
return this.msgSent;
}
public beginDialog<T>(id: string, args?: T): ISession {
var dialog = this.dialogs.getDialog(id);
if (!dialog) {
throw new Error('Dialog[' + id + '] not found.');
}
var ss = this.sessionState;
var cur: IDialogState = { id: id, state: {} };
ss.callstack.push(cur);
this.dialogData = cur.state;
dialog.begin(this, args);
return this;
}
public endDialog(result?: any): ISession {
var ss = this.sessionState;
var r: dialog.IDialogResult<any> = result || { resumed: dialog.ResumeReason.completed };
r.childId = ss.callstack[ss.callstack.length - 1].id;
ss.callstack.pop();
if (ss.callstack.length > 0) {
var cur = ss.callstack[ss.callstack.length - 1];
var d = this.dialogs.getDialog(cur.id);
this.dialogData = cur.state;
d.dialogResumed(this, r);
} else if (!this.msgSent) {
this.send();
this.emit('quit');
}
return this;
}
public compareConfidence(language: string, utterance: string, score: number, callback: (handled: boolean) => void): void {
var comparer = new SessionConfidenceComparor(this, language, utterance, score, callback);
comparer.next();
}
public reset(dialogId: string, dialogArgs?: any): ISession {
this._isReset = true;
this.sessionState.callstack = [];
this.beginDialog(dialogId, dialogArgs);
return this;
}
public isReset(): boolean {
return this._isReset;
}
public createMessage(text: string, args?: any[]): IMessage {
var message: IMessage = {
text: this.vgettext(text, args)
};
if (this.message.language) {
message.language = this.message.language
}
return message;
}
private routeMessage(): void {
try {
// Route message to dialog.
var ss = this.sessionState;
if (ss.callstack.length == 0) {
this.beginDialog(this.args.dialogId, this.args.dialogArgs);
} else if (this.validateCallstack()) {
var cur = ss.callstack[ss.callstack.length - 1];
var dialog = this.dialogs.getDialog(cur.id);
this.dialogData = cur.state;
dialog.replyReceived(this);
} else {
console.error('Callstack is invalid, resetting session.');
this.reset(this.args.dialogId, this.args.dialogArgs);
}
} catch (e) {
this.error(e);
}
}
private vgettext(msgid: string, args?: any[]): string {
var tmpl: string;
if (this.args.localizer && this.message) {
tmpl = this.args.localizer.gettext(this.message.language || '', msgid);
} else {
tmpl = msgid;
}
return args && args.length > 0 ? sprintf.vsprintf(tmpl, args) : tmpl;
}
/** Checks for any unsupported dialogs on the callstack. */
private validateCallstack(): boolean {
var ss = this.sessionState;
for (var i = 0; i < ss.callstack.length; i++) {
var id = ss.callstack[i].id;
if (!this.dialogs.hasDialog(id)) {
return false;
}
}
return true;
}
}
class SessionConfidenceComparor implements ISessionAction {
private index: number;
constructor(
private session: Session,
private language: string,
private utterance: string,
private score: number,
private callback: (handled: boolean) => void) {
this.index = session.sessionState.callstack.length - 1;
this.userData = session.userData;
}
public userData: any;
public dialogData: any;
public next(): void {
this.index--;
if (this.index >= 0) {
this.getDialog().compareConfidence(this, this.language, this.utterance, this.score);
} else {
this.callback(false);
}
}
public endDialog<T>(result?: dialog.IDialogResult<T>): void {
// End dialog up to current point in the stack.
this.session.sessionState.callstack.splice(this.index + 1);
this.getDialog().dialogResumed(this.session, result || { resumed: dialog.ResumeReason.childEnded });
this.callback(true);
}
public send(msg: string, ...args: any[]): void;
public send(msg: IMessage): void;
public send(msg: any, ...args: any[]): void {
// Send a message to the user.
args.splice(0, 0, [msg]);
Session.prototype.send.apply(this.session, args);
this.callback(true);
}
private getDialog(): dialog.IDialog {
var cur = this.session.sessionState.callstack[this.index];
this.dialogData = cur.state;
return this.session.dialogs.getDialog(cur.id);
}
}

1627
Node/src/botbuilder.d.ts поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

39
Node/src/botbuilder.ts Normal file
Просмотреть файл

@ -0,0 +1,39 @@
import consts = require('./consts');
import utils = require('./utils');
import session = require('./Session');
import dialog = require('./dialogs/Dialog');
import collection = require('./dialogs/DialogCollection');
import prompts = require('./dialogs/Prompts');
import intent = require('./dialogs/IntentDialog');
import luis = require('./dialogs/LuisDialog');
import command = require('./dialogs/CommandDialog');
import simple = require('./dialogs/SimpleDialog');
import entities = require('./dialogs/EntityRecognizer');
import storage = require('./storage/Storage');
import connector = require('./bots/BotConnectorBot');
import skype = require('./bots/SkypeBot');
import slack = require('./bots/SlackBot');
import text = require('./bots/TextBot');
declare var exports: any;
exports.Session = session.Session;
exports.Dialog = dialog.Dialog;
exports.ResumeReason = dialog.ResumeReason;
exports.DialogCollection = collection.DialogCollection;
exports.PromptType = prompts.PromptType;
exports.ListStyle = prompts.ListStyle;
exports.Prompts = prompts.Prompts;
exports.SimplePromptRecognizer = prompts.SimplePromptRecognizer;
exports.IntentDialog = intent.IntentDialog;
exports.IntentGroup = intent.IntentGroup;
exports.LuisDialog = luis.LuisDialog;
exports.CommandDialog = command.CommandDialog;
exports.EntityRecognizer = entities.EntityRecognizer;
exports.MemoryStorage = storage.MemoryStorage;
exports.BotConnectorBot = connector.BotConnectorBot;
exports.BotConnectorSession = connector.BotConnectorSession;
exports.SkypeBot = skype.SkypeBot;
exports.SlackBot = slack.SlackBot;
exports.TextBot = text.TextBot;

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

@ -0,0 +1,281 @@
import collection = require('../dialogs/DialogCollection');
import session = require('../Session');
import consts = require('../consts');
import utils = require('../utils');
import request = require('request');
export interface IBotConnectorOptions {
endpoint?: string;
appId?: string;
appSecret?: string;
subscriptionKey?: string;
defaultFrom?: IChannelAccount;
localizer?: ILocalizer;
defaultDialogId?: string;
defaultDialogArgs?: any;
groupWelcomeMessage?: string;
userWelcomeMessage?: string;
goodbyeMessage?: string;
}
export interface IBotConnectorMessage extends IMessage {
botUserData?: any;
botConversationData?: any;
botPerUserInConversationData?: any;
}
/** Express or Restify Request object. */
interface IRequest {
body: any;
headers: {
[name: string]: string;
};
on(event: string, ...args: any[]): void;
}
/** Express or Restify Response object. */
interface IResponse {
send(status: number, body?: any): void;
send(body: any): void;
}
/** Express or Restify Middleware Function. */
interface IMiddleware {
(req: IRequest, res: IResponse, next?: Function): void;
}
export class BotConnectorBot extends collection.DialogCollection {
private options: IBotConnectorOptions = {
endpoint: process.env['endpoint'] || 'https://intercomppe.azure-api.net',
appId: process.env['appId'] || '',
appSecret: process.env['appSecret'] || '',
subscriptionKey: process.env['subscriptionKey'] || '',
defaultDialogId: '/'
}
constructor(options?: IBotConnectorOptions) {
super();
this.configure(options);
}
public configure(options: IBotConnectorOptions) {
if (options) {
for (var key in options) {
if (options.hasOwnProperty(key)) {
(<any>this.options)[key] = (<any>options)[key];
}
}
}
}
public verifyBotFramework(options?: IBotConnectorOptions): IMiddleware {
this.configure(options);
return (req: IRequest, res: IResponse, next: Function) => {
// Check authorization
var authorized: boolean;
if (this.options.appId && this.options.appSecret) {
if (req.headers && req.headers.hasOwnProperty('authorization')) {
var tmp = req.headers['authorization'].split(' ');
var buf = new Buffer(tmp[1], 'base64');
var cred = buf.toString().split(':');
if (cred[0] == this.options.appId && cred[1] == this.options.appSecret) {
authorized = true;
} else {
authorized = false;
}
} else {
authorized = false;
}
} else {
authorized = true;
}
if (authorized) {
next();
} else {
res.send(403);
}
};
}
public listen(options?: IBotConnectorOptions): IMiddleware {
this.configure(options);
return (req: IRequest, res: IResponse) => {
if (req.body) {
this.processMessage(req.body, this.options.defaultDialogId, this.options.defaultDialogArgs, res);
} else {
var requestData = '';
req.on('data', (chunk: string) => {
requestData += chunk
});
req.on('end', () => {
try {
var msg = JSON.parse(requestData);
this.processMessage(msg, this.options.defaultDialogId, this.options.defaultDialogArgs, res);
} catch (e) {
this.emit('error', new Error('Invalid Bot Framework Message'));
res.send(400);
}
});
}
};
}
public beginDialog(address: IBeginDialogAddress, dialogId: string, dialogArgs?: any): void {
// Fixup address fields
var msg: IBotConnectorMessage = address;
msg.type = 'Message';
if (!msg.from) {
msg.from = this.options.defaultFrom;
}
// Validate args
if (!msg.to || !msg.from) {
throw new Error('Invalid address passed to BotConnectorBot.beginDialog().');
}
if (!this.hasDialog(dialogId)) {
throw new Error('Invalid dialog passed to BotConnectorBot.beginDialog().');
}
// Dispatch message
this.processMessage(msg, dialogId, dialogArgs);
}
private processMessage(message: IBotConnectorMessage, dialogId: string, dialogArgs: any, res?: IResponse) {
try {
// Validate message
if (!message || !message.type) {
this.emit('error', new Error('Invalid Bot Framework Message'));
return res.send(400);
}
// Dispatch messages
this.emit(message.type, message);
if (message.type == 'Message') {
// Initialize session
var ses = new BotConnectorSession({
localizer: this.options.localizer,
dialogs: this,
dialogId: dialogId,
dialogArgs: dialogArgs
});
ses.on('send', (message: IMessage) => {
// Compose reply
var reply: IBotConnectorMessage = message || {};
reply.botUserData = utils.clone(ses.userData);
reply.botConversationData = utils.clone(ses.conversationData);
reply.botPerUserInConversationData = utils.clone(ses.perUserInConversationData);
reply.botPerUserInConversationData[consts.Data.SessionState] = ses.sessionState;
if (reply.text && !reply.language) {
reply.language = ses.message.language;
}
// Send message
if (res) {
this.emit('reply', reply);
res.send(200, reply);
res = null;
} else if (ses.message.conversationId) {
// Post an additional reply
reply.from = ses.message.to;
reply.to = ses.message.replyTo ? ses.message.replyTo : ses.message.from;
reply.replyToMessageId = ses.message.id;
reply.conversationId = ses.message.conversationId;
reply.channelConversationId = ses.message.channelConversationId;
reply.channelMessageId = ses.message.channelMessageId;
reply.participants = ses.message.participants;
reply.totalParticipants = ses.message.totalParticipants;
this.emit('reply', reply);
this.post('/bot/v1.0/messages', reply, (err) => {
this.emit('error', err);
});
} else {
// Start a new conversation
reply.from = ses.message.from;
reply.to = ses.message.to;
this.emit('send', reply);
this.post('/bot/v1.0/messages', reply, (err) => {
this.emit('error', err);
});
}
});
ses.on('error', (err: Error) => {
this.emit('error', err, ses.message);
if (res) {
res.send(500);
}
});
ses.on('quit', () => {
this.emit('quit', ses.message);
});
// Unpack data fields
var sessionState: ISessionState;
if (message.botUserData) {
ses.userData = message.botUserData;
delete message.botUserData;
} else {
ses.userData = {};
}
if (message.botConversationData) {
ses.conversationData = message.botConversationData;
delete message.botConversationData;
} else {
ses.conversationData = {};
}
if (message.botPerUserInConversationData) {
if (message.botPerUserInConversationData.hasOwnProperty(consts.Data.SessionState)) {
sessionState = message.botPerUserInConversationData[consts.Data.SessionState];
delete message.botPerUserInConversationData[consts.Data.SessionState];
}
ses.perUserInConversationData = message.botPerUserInConversationData;
delete message.botPerUserInConversationData;
} else {
ses.perUserInConversationData = {};
}
// Dispatch message
ses.dispatch(sessionState, message);
} else if (res) {
var msg: string;
switch (message.type) {
case "botAddedToConversation":
msg = this.options.groupWelcomeMessage;
break;
case "userAddedToConversation":
msg = this.options.userWelcomeMessage;
break;
case "endOfConversation":
msg = this.options.goodbyeMessage;
break;
}
res.send(msg ? { type: message.type, text: msg } : {});
}
} catch (e) {
this.emit('error', e instanceof Error ? e : new Error(e.toString()));
res.send(500);
}
}
protected post(path: string, body: any, callback?: (error: any) => void): void {
var settings = this.options;
var options: request.Options = {
url: settings.endpoint + path,
body: body
};
if (settings.appId && settings.appSecret) {
options.auth = {
username: 'Bot_' + settings.appId,
password: 'Bot_' + settings.appSecret
};
options.headers = {
'Ocp-Apim-Subscription-Key': settings.subscriptionKey || settings.appSecret
};
}
request.post(options, callback);
}
}
export class BotConnectorSession extends session.Session {
public conversationData: any;
public perUserInConversationData: any;
}

226
Node/src/bots/SkypeBot.ts Normal file
Просмотреть файл

@ -0,0 +1,226 @@
import collection = require('../dialogs/DialogCollection');
import session = require('../Session');
import storage = require('../storage/Storage');
import botkit = require('skype-botkit');
export interface ISkypeBotOptions {
userStore?: storage.IStorage;
sessionStore?: storage.IStorage;
maxSessionAge?: number;
localizer?: ILocalizer;
defaultDialogId?: string;
defaultDialogArgs?: any;
contactAddedmessage?: string;
botAddedMessage?: string;
botRemovedMessage?: string;
memberAddedMessage?: string;
memberRemovedMessage?: string;
}
export class SkypeBot extends collection.DialogCollection {
private options: ISkypeBotOptions = {
maxSessionAge: 14400000, // <-- default max session age of 4 hours
defaultDialogId: '/'
};
constructor(protected botService: botkit.BotService, options?: ISkypeBotOptions) {
super();
this.configure(options);
var events = 'message|personalMessage|groupMessage|attachment|threadBotAdded|threadAddMember|threadBotRemoved|threadRemoveMember|contactAdded|threadTopicUpdated|threadHistoryDisclosedUpdate'.split('|');
events.forEach((value) => {
botService.on(value, (bot: botkit.Bot, data: botkit.IMessage) => {
this.emit(value, bot, data);
});
});
}
public configure(options: ISkypeBotOptions) {
if (options) {
for (var key in options) {
if (options.hasOwnProperty(key)) {
(<any>this.options)[key] = (<any>options)[key];
}
}
}
}
public beginDialog(address: IBeginDialogAddress, dialogId: string, dialogArgs?: any): void {
// Validate args
if (!address.to) {
throw new Error('Invalid address passed to SkypeBot.beginDialog().');
}
if (!this.hasDialog(dialogId)) {
throw new Error('Invalid dialog passed to SkypeBot.beginDialog().');
}
// Dispatch message
this.dispatchMessage(null, this.toSkypeMessage(address), dialogId, dialogArgs);
}
private handleEvent(event: string, bot: botkit.Bot, data: any) {
var onError = (err: Error) => {
this.emit('error', err, data);
};
switch (event) {
case 'personalMessage':
this.dispatchMessage(bot, data, this.options.defaultDialogId, this.options.defaultDialogArgs);
break;
case 'threadBotAdded':
if (this.options.botAddedMessage) {
bot.reply(this.options.botAddedMessage, onError);
}
break;
case 'threadAddMember':
if (this.options.memberAddedMessage) {
bot.reply(this.options.memberAddedMessage, onError);
}
break;
case 'threadBotRemoved':
if (this.options.botRemovedMessage) {
bot.reply(this.options.botRemovedMessage, onError);
}
break;
case 'threadRemoveMember':
if (this.options.memberRemovedMessage) {
bot.reply(this.options.memberRemovedMessage, onError);
}
break;
case 'contactAdded':
if (this.options.contactAddedmessage) {
bot.reply(this.options.contactAddedmessage, onError);
}
break;
}
}
private dispatchMessage(bot: botkit.Bot, data: botkit.IMessage, dialogId: string, dialogArgs: any) {
var onError = (err: Error) => {
this.emit('error', err, data);
};
// Initialize session
var ses = new session.Session({
localizer: this.options.localizer,
dialogs: this,
dialogId: dialogId,
dialogArgs: dialogArgs
});
ses.on('send', (reply: IMessage) => {
this.saveData(msg.from.address, ses.userData, ses.sessionState, () => {
// If we have no message text then we're just saving state.
if (reply && reply.text) {
// Do we have a bot?
var skypeReply = this.toSkypeMessage(reply);
if (bot) {
// Check for a different TO address
if (skypeReply.to && skypeReply.to != data.from) {
this.emit('send', skypeReply);
bot.send(skypeReply.to, skypeReply.content, onError);
} else {
this.emit('reply', skypeReply);
bot.reply(skypeReply.content, onError);
}
} else {
skypeReply.to = ses.message.to.address;
this.emit('send', skypeReply);
this.botService.send(skypeReply.to, skypeReply.content, onError);
}
}
});
});
ses.on('error', (err: Error) => {
this.emit('error', err, data);
});
ses.on('quit', () => {
this.emit('quit', data);
});
// Load data and dispatch message
var msg = this.fromSkypeMessage(data);
this.getData(msg.from.address, (userData, sessionState) => {
ses.userData = userData || {};
ses.dispatch(sessionState, msg);
});
}
private getData(userId: string, callback: (userData: any, sessionState: ISessionState) => void) {
// Ensure stores specified
if (!this.options.userStore) {
this.options.userStore = new storage.MemoryStorage();
}
if (!this.options.sessionStore) {
this.options.sessionStore = new storage.MemoryStorage();
}
// Load data
var ops = 2;
var userData: any, sessionState: ISessionState;
this.options.userStore.get(userId, (err, data) => {
if (!err) {
userData = data;
if (--ops == 0) {
callback(userData, sessionState);
}
} else {
this.emit('error', err);
}
});
this.options.sessionStore.get(userId, (err: Error, data: ISessionState) => {
if (!err) {
if (data && (new Date().getTime() - data.lastAccess) < this.options.maxSessionAge) {
sessionState = data;
}
if (--ops == 0) {
callback(userData, sessionState);
}
} else {
this.emit('error', err);
}
});
}
private saveData(userId: string, userData: any, sessionState: ISessionState, callback: Function) {
var ops = 2;
function onComplete(err: Error) {
if (!err) {
if (--ops == 0) {
callback(null);
}
} else {
callback(err);
}
}
this.options.userStore.save(userId, userData, onComplete);
this.options.sessionStore.save(userId, sessionState, onComplete);
}
private fromSkypeMessage(msg: botkit.IMessage): IMessage {
return {
type: msg.type,
id: msg.messageId.toString(),
from: {
channelId: 'skype',
address: msg.from
},
to: {
channelId: 'skype',
address: msg.to
},
text: msg.content,
channelData: msg
};
}
private toSkypeMessage(msg: IMessage): botkit.IMessage {
return {
type: msg.type,
from: msg.from ? msg.from.address : '',
to: msg.to ? msg.to.address : '',
content: msg.text,
messageId: msg.id ? Number(msg.id) : Number.NaN,
contentType: "RichText",
eventTime: msg.channelData ? msg.channelData.eventTime : new Date().getTime()
};
}
}

201
Node/src/bots/SlackBot.ts Normal file
Просмотреть файл

@ -0,0 +1,201 @@
import collection = require('../dialogs/DialogCollection');
import session = require('../Session');
import storage = require('../storage/Storage');
interface ISlackMessage {
type: string;
subtype?: string;
channel: string;
user: string;
text: string;
attachments?: ISlackAttachment[];
ts: string;
}
interface ISlackAttachment {
}
declare class BotKitController {
on(event: string, listener: Function): void;
}
declare class Bot {
reply(message: ISlackMessage, text: string): void;
say(message: ISlackMessage, cb: (err: Error) => void): void;
}
export interface ISlackBotOptions {
userStore?: storage.IStorage;
sessionStore?: storage.IStorage;
maxSessionAge?: number;
localizer?: ILocalizer;
defaultDialogId?: string;
defaultDialogArgs?: any;
}
export class SlackBot extends collection.DialogCollection {
protected options: ISlackBotOptions = {
maxSessionAge: 14400000, // <-- default max session age of 4 hours
defaultDialogId: '/'
};
constructor(protected controller: BotKitController, protected bot?: Bot, options?: ISlackBotOptions) {
super();
this.configure(options);
controller.on('direct_message', (bot: Bot, msg: ISlackMessage) => {
this.emit('message', msg);
this.dispatchMessage(bot, msg, this.options.defaultDialogId, this.options.defaultDialogArgs);
});
}
public configure(options: ISlackBotOptions): this {
if (options) {
for (var key in options) {
if (options.hasOwnProperty(key)) {
(<any>this.options)[key] = (<any>options)[key];
}
}
}
return this;
}
public beginDialog(address: IBeginDialogAddress, dialogId: string, dialogArgs?: any): void {
// Validate args
if (!this.bot) {
throw new Error('Spawned BotKit Bot not passed to constructor.');
}
if (!address.to) {
throw new Error('Invalid address passed to SlackBot.beginDialog().');
}
if (!this.hasDialog(dialogId)) {
throw new Error('Invalid dialog passed to SlackBot.beginDialog().');
}
// Dispatch message
this.dispatchMessage(null, this.toSlackMessage(address), dialogId, dialogArgs);
}
private dispatchMessage(bot: Bot, data: ISlackMessage, dialogId: string, dialogArgs: any) {
var onError = (err: Error) => {
this.emit('error', err, data);
};
// Initialize session
var ses = new session.Session({
localizer: this.options.localizer,
dialogs: this,
dialogId: this.options.defaultDialogId,
dialogArgs: this.options.defaultDialogArgs
});
ses.on('send', (reply: IMessage) => {
this.saveData(message.from.address, ses.userData, ses.sessionState, () => {
// If we have no message text then we're just saving state.
if (reply && reply.text) {
var slackReply = this.toSlackMessage(reply);
if (bot) {
// Check for a different TO address
if (slackReply.user && slackReply.user != data.user) {
this.emit('send', slackReply);
bot.say(slackReply, onError);
} else {
this.emit('reply', slackReply);
bot.reply(data, slackReply.text);
}
} else {
slackReply.user = ses.message.to.address;
this.emit('send', slackReply);
this.bot.say(slackReply, onError);
}
}
});
});
ses.on('error', (err: Error) => {
this.emit('error', err, data);
});
ses.on('quit', () => {
this.emit('quit', data);
});
// Dispatch message
var message = this.fromSlackMessage(data);
this.getData(message.from.address, (err, userData, sessionState) => {
ses.userData = userData || {};
ses.dispatch(sessionState, message);
});
}
private getData(userId: string, callback: (err: Error, userData: any, sessionState: ISessionState) => void) {
// Ensure stores specified
if (!this.options.userStore) {
this.options.userStore = new storage.MemoryStorage();
}
if (!this.options.sessionStore) {
this.options.sessionStore = new storage.MemoryStorage();
}
// Load data
var ops = 2;
var userData: any, sessionState: ISessionState;
this.options.userStore.get(userId, (err, data) => {
if (!err) {
userData = data;
if (--ops == 0) {
callback(null, userData, sessionState);
}
} else {
callback(err, null, null);
}
});
this.options.sessionStore.get(userId, (err: Error, data: ISessionState) => {
if (!err) {
if (data && (new Date().getTime() - data.lastAccess) < this.options.maxSessionAge) {
sessionState = data;
}
if (--ops == 0) {
callback(null, userData, sessionState);
}
} else {
callback(err, null, null);
}
});
}
private saveData(userId: string, userData: any, sessionState: ISessionState, callback: (err: Error) => void) {
var ops = 2;
function onComplete(err: Error) {
if (!err) {
if (--ops == 0) {
callback(null);
}
} else {
callback(err);
}
}
this.options.userStore.save(userId, userData, onComplete);
this.options.sessionStore.save(userId, sessionState, onComplete);
}
private fromSlackMessage(msg: ISlackMessage): IMessage {
return {
type: msg.type,
id: msg.ts,
text: msg.text,
from: {
channelId: 'slack',
address: msg.user
},
channelConversationId: msg.channel,
channelData: msg
};
}
private toSlackMessage(msg: IMessage): ISlackMessage {
return {
type: msg.type,
ts: msg.id,
text: msg.text,
user: msg.to ? msg.to.address : msg.from.address,
channel: msg.channelConversationId
};
}
}

170
Node/src/bots/TextBot.ts Normal file
Просмотреть файл

@ -0,0 +1,170 @@
import collection = require('../dialogs/DialogCollection');
import session = require('../Session');
import storage = require('../storage/Storage');
import uuid = require('node-uuid');
import readline = require('readline');
export interface ITextBotOptions {
userStore?: storage.IStorage;
sessionStore?: storage.IStorage;
maxSessionAge?: number;
localizer?: ILocalizer;
defaultDialogId?: string;
defaultDialogArgs?: any;
}
export class TextBot extends collection.DialogCollection {
private options: ITextBotOptions = {
maxSessionAge: 14400000, // <-- default max session age of 4 hours
defaultDialogId: '/'
};
constructor(options?: ITextBotOptions) {
super();
this.configure(options);
}
public configure(options: ITextBotOptions) {
if (options) {
for (var key in options) {
if (options.hasOwnProperty(key)) {
(<any>this.options)[key] = (<any>options)[key];
}
}
}
}
public beginDialog(address: IBeginDialogAddress, dialogId: string, dialogArgs?: any): void {
// Validate args
if (!this.hasDialog(dialogId)) {
throw new Error('Invalid dialog passed to SkypeBot.beginDialog().');
}
// Dispatch message
this.dispatchMessage(address || {}, null, dialogId, dialogArgs);
}
public processMessage(message: IMessage, callback?: (err: Error, reply: IMessage) => void): void {
this.emit('message', message);
if (!message.id) {
message.id = uuid.v1();
}
if (!message.from) {
message.from = { channelId: 'text', address: 'user' };
}
this.dispatchMessage(message, callback, this.options.defaultDialogId, this.options.defaultDialogArgs);
}
public listenStdin(): void {
function onMessage(message: IMessage) {
console.log(message.text);
}
this.on('reply', onMessage);
this.on('send', onMessage);
this.on('quit', () => {
rl.close();
process.exit();
});
var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
rl.on('line', (line: string) => {
this.processMessage({ text: line || '' });
});
}
private dispatchMessage(message: IMessage, callback: (err: Error, reply: IMessage) => void, dialogId: string, dialogArgs: any): void {
// Initialize session
var ses = new session.Session({
localizer: this.options.localizer,
dialogs: this,
dialogId: dialogId,
dialogArgs: dialogArgs
});
ses.on('send', (reply: IMessage) => {
this.saveData(message.from.address, ses.userData, ses.sessionState, () => {
// If we have no message text then we're just saving state.
if (reply && reply.text) {
if (callback) {
callback(null, reply);
callback = null;
} else if (message.id || message.conversationId) {
reply.from = message.to;
reply.to = reply.replyTo || reply.to;
reply.conversationId = message.conversationId;
reply.language = message.language;
this.emit('reply', reply);
} else {
this.emit('send', reply);
}
}
});
});
ses.on('error', (err: Error) => {
if (callback) {
callback(err, null);
callback = null;
} else {
this.emit('error', err, message);
}
});
ses.on('quit', () => {
this.emit('quit', message);
});
// Dispatch message
this.getData(message.from.address, (err, userData, sessionState) => {
ses.userData = userData || {};
ses.dispatch(sessionState, message);
});
}
private getData(userId: string, callback: (err: Error, userData: any, sessionState: ISessionState) => void) {
// Ensure stores specified
if (!this.options.userStore) {
this.options.userStore = new storage.MemoryStorage();
}
if (!this.options.sessionStore) {
this.options.sessionStore = new storage.MemoryStorage();
}
// Load data
var ops = 2;
var userData: any, sessionState: ISessionState;
this.options.userStore.get(userId, (err, data) => {
if (!err) {
userData = data;
if (--ops == 0) {
callback(null, userData, sessionState);
}
} else {
callback(err, null, null);
}
});
this.options.sessionStore.get(userId, (err: Error, data: ISessionState) => {
if (!err) {
if (data && (new Date().getTime() - data.lastAccess) < this.options.maxSessionAge) {
sessionState = data;
}
if (--ops == 0) {
callback(null, userData, sessionState);
}
} else {
callback(err, null, null);
}
});
}
private saveData(userId: string, userData: any, sessionState: ISessionState, callback: (err: Error) => void) {
var ops = 2;
function onComplete(err: Error) {
if (!err) {
if (--ops == 0) {
callback(null);
}
} else {
callback(err);
}
}
this.options.userStore.save(userId, userData, onComplete);
this.options.sessionStore.save(userId, sessionState, onComplete);
}
}

19
Node/src/consts.ts Normal file
Просмотреть файл

@ -0,0 +1,19 @@
export var Data = {
SessionState: 'BotBuilder.Data.SessionState',
Handler: 'BotBuilder.Data.Handler',
Group: 'BotBuilder.Data.Group',
Intent: 'BotBuilder.Data.Intent',
WaterfallStep: 'BotBuilder.Data.WaterfallStep'
};
export var DialogId = {
Prompts: 'BotBuilder.Dialogs.Prompts'
};
export var Id = {
DefaultGroup: 'BotBuilder.Id.DefaultGroup'
};
export var Intents = {
Default: 'BotBuilder.Intents.Default'
};

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

@ -0,0 +1,138 @@
import session = require('../Session');
import dialog = require('./Dialog');
import actions = require('./DialogAction');
import consts = require('../consts');
import util = require('util');
export interface ICommandArgs {
expression: RegExp;
matches: RegExpExecArray;
}
interface ICommandDialogEntry {
expressions?: RegExp[];
fn: IDialogHandler<ICommandArgs>;
}
export class CommandDialog extends dialog.Dialog {
private beginDialog: (session: ISession, args: any, next: (handled: boolean) => void) => void;
private commands: ICommandDialogEntry[] = [];
private default: ICommandDialogEntry;
public begin<T>(session: ISession, args: T): void {
if (this.beginDialog) {
this.beginDialog(session, args, (handled) => {
if (!handled) {
super.begin(session, args);
}
});
} else {
super.begin(session, args);
}
}
public replyReceived(session: ISession): void {
var score = 0.0;
var expression: RegExp;
var matches: RegExpExecArray;
var text = session.message.text;
var matched: ICommandDialogEntry;
for (var i = 0; i < this.commands.length; i++) {
var cmd = this.commands[i];
for (var j = 0; j < cmd.expressions.length; j++) {
expression = cmd.expressions[j];
if (expression.test(text)) {
matched = cmd;
session.dialogData[consts.Data.Handler] = i;
matches = expression.exec(text);
if (matches) {
var length = 0;
matches.forEach((value) => {
length += value.length;
});
score = length / text.length;
}
break;
}
}
if (matched) break;
}
if (!matched && this.default) {
expression = null;
matched = this.default;
session.dialogData[consts.Data.Handler] = -1;
}
if (matched) {
session.compareConfidence(session.message.language, text, score, (handled) => {
if (!handled) {
matched.fn(session, { expression: expression, matches: matches });
}
});
} else {
session.send();
}
}
public dialogResumed<T>(session: ISession, result: dialog.IDialogResult<T>): void {
var cur: ICommandDialogEntry;
var handler = session.dialogData[consts.Data.Handler];
if (handler >= 0 && handler < this.commands.length) {
cur = this.commands[handler];
} else if (this.default) {
cur = this.default;
}
if (cur) {
cur.fn(session, <any>result);
} else {
super.dialogResumed(session, result);
}
}
public onBegin(handler: IBeginDialogHandler): this {
this.beginDialog = handler;
return this;
}
public matches(pattern: string, fn: IDialogHandler<ICommandArgs>): this;
public matches(patterns: string[], fn: IDialogHandler<ICommandArgs>): this;
public matches(pattern: string, waterfall: actions.IDialogWaterfallStep[]): this;
public matches(patterns: string[], waterfall: actions.IDialogWaterfallStep[]): this;
public matches(pattern: string, dialogId: string, dialogArgs?: any): this;
public matches(patterns: string[], dialogId: string, dialogArgs?: any): this;
public matches(patterns: any, dialogId: any, dialogArgs?: any): this {
// Fix args
var fn: IDialogHandler<ICommandArgs>;
var patterns = !util.isArray(patterns) ? [patterns] : patterns;
if (Array.isArray(dialogId)) {
fn = actions.DialogAction.waterfall(dialogId);
} else if (typeof dialogId == 'string') {
fn = actions.DialogAction.beginDialog(dialogId, dialogArgs);
} else {
fn = dialogId;
}
// Save compiled expressions
var expressions: RegExp[] = [];
for (var i = 0; i < (<string[]>patterns).length; i++) {
expressions.push(new RegExp((<string[]>patterns)[i], 'i'));
}
this.commands.push({ expressions: expressions, fn: fn });
return this;
}
public onDefault(fn: IDialogHandler<ICommandArgs>): this;
public onDefault(waterfall: actions.IDialogWaterfallStep[]): this;
public onDefault(dialogId: string, dialogArgs?: any): this;
public onDefault(dialogId: any, dialogArgs?: any): this {
var fn: IDialogHandler<ICommandArgs>;
if (Array.isArray(dialogId)) {
fn = actions.DialogAction.waterfall(dialogId);
} else if (typeof dialogId == 'string') {
fn = actions.DialogAction.beginDialog(dialogId, dialogArgs);
} else {
fn = dialogId;
}
this.default = { fn: fn };
return this;
}
}

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

@ -0,0 +1,38 @@

export interface IDialog {
begin<T>(session: ISession, args?: T): void;
replyReceived(session: ISession): void;
dialogResumed(session: ISession, result: any): void;
compareConfidence(action: ISessionAction, language: string, utterance: string, score: number): void;
}
export enum ResumeReason { completed, notCompleted, canceled, back, forward, captureCompleted, childEnded }
export interface IDialogResult<T> {
resumed: ResumeReason;
childId?: string;
error?: Error;
response?: T;
}
export abstract class Dialog implements IDialog {
public begin<T>(session: ISession, args?: T): void {
this.replyReceived(session);
}
abstract replyReceived(session: ISession): void;
public dialogResumed<T>(session: ISession, result: IDialogResult<T>): void {
if (!session.messageSent()) {
if (result.error) {
session.error(result.error);
} else {
session.send();
}
}
}
public compareConfidence(action: ISessionAction, language: string, utterance: string, score: number): void {
action.next();
}
}

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

@ -0,0 +1,95 @@
import session = require('../Session');
import dialog = require('./Dialog');
import consts = require('../consts');
export interface IDialogWaterfallStep {
(session: ISession, result?: any, skip?: IDialogWaterfallCursor): void;
}
export interface IDialogWaterfallCursor {
(count?: number, results?: dialog.IDialogResult<any>): void;
}
export class DialogAction {
static send(msg: string, ...args: any[]): IDialogHandler<any> {
args.splice(0, 0, msg);
return function sendAction(s: ISession) {
// Send a message to the user.
session.Session.prototype.send.apply(s, args);
};
}
static beginDialog<T>(id: string, args?: T): IDialogHandler<T> {
return function beginDialogAction(s: ISession, a: any) {
// Ignore calls where we're being resumed.
if (!a || !a.hasOwnProperty('resumed')) {
// Merge args
if (args) {
a = a || {};
for (var key in args) {
if (args.hasOwnProperty(key)) {
a[key] = (<any>args)[key];
}
}
}
// Begin a new dialog
s.beginDialog(id, a);
}
};
}
static endDialog(result?: any): IDialogHandler<any> {
return function endDialogAction(s: ISession) {
// End dialog
s.endDialog(result);
};
}
static waterfall(steps: IDialogWaterfallStep[]): IDialogHandler<any> {
return function waterfallAction(s: ISession, r: dialog.IDialogResult<any>) {
var skip = (count = 1, result?: dialog.IDialogResult<any>) => {
result = result || { resumed: dialog.ResumeReason.forward };
s.dialogData[consts.Data.WaterfallStep] += count;
waterfallAction(s, result);
};
try {
// Check for continuation of waterfall
if (r && r.hasOwnProperty('resumed')) {
// Adjust step based on users utterance
var step = s.dialogData[consts.Data.WaterfallStep];
switch (r.resumed) {
case dialog.ResumeReason.back:
step -= 1;
break;
case dialog.ResumeReason.forward:
step += 2;
break;
default:
step++;
}
// Handle result
if (step >= 0 && step < steps.length) {
s.dialogData[consts.Data.WaterfallStep] = step;
steps[step](s, r, skip);
} else {
delete s.dialogData[consts.Data.WaterfallStep];
s.send();
}
} else if (steps && steps.length > 0) {
// Start waterfall
s.dialogData[consts.Data.WaterfallStep] = 0;
steps[0](s, r, skip);
} else {
delete s.dialogData[consts.Data.WaterfallStep];
s.send();
}
} catch (e) {
delete s.dialogData[consts.Data.WaterfallStep];
s.endDialog({ resumed: dialog.ResumeReason.notCompleted, error: e instanceof Error ? e : new Error(e.toString()) });
}
};
}
}

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

@ -0,0 +1,63 @@
import dialog = require('./Dialog');
import actions = require('./DialogAction');
import simpleDialog = require('./SimpleDialog');
import events = require('events');
interface IDialogMap {
[id: string]: dialog.IDialog;
}
export class DialogCollection extends events.EventEmitter {
private middleware: { (session: ISession, next: Function): void; }[] = [];
private dialogs: IDialogMap = {};
constructor() {
super();
}
public add(dialogs: { [id: string]: dialog.IDialog; }): DialogCollection;
public add(id: string, fn: IDialogHandler<any>): DialogCollection;
public add(id: string, waterfall: actions.IDialogWaterfallStep[]): DialogCollection;
public add(id: string, dialog: dialog.IDialog): DialogCollection;
public add(id: any, dialog?: any): DialogCollection {
// Fixup params
var dialogs: { [id: string]: dialog.IDialog; };
if (typeof id == 'string') {
if (Array.isArray(dialog)) {
dialog = new simpleDialog.SimpleDialog(actions.DialogAction.waterfall(dialog));
} else if (typeof dialog == 'function') {
dialog = new simpleDialog.SimpleDialog(dialog);
}
dialogs = { [id]: dialog };
} else {
dialogs = id;
}
// Add dialogs
for (var key in dialogs) {
if (!this.dialogs.hasOwnProperty(key)) {
this.dialogs[key] = dialogs[key];
} else {
throw new Error('Dialog[' + key + '] already exists.');
}
}
return this;
}
public getDialog(id: string): dialog.IDialog {
return this.dialogs[id];
}
public getMiddleware(): { (session: ISession, next: Function): void; }[] {
return this.middleware;
}
public hasDialog(id: string): boolean {
return this.dialogs.hasOwnProperty(id);
}
public use(fn: (session: ISession, next: Function) => void): DialogCollection {
this.middleware.push(fn);
return this;
}
}

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

@ -0,0 +1,207 @@
import utils = require('../utils');
import sprintf = require('sprintf-js');
import chrono = require('chrono-node');
interface ILuisDateTimeEntity extends IEntity {
resolution: {
resolution_type: string;
date?: string;
time?: string;
comment?: string;
duration?: string;
};
}
interface IChronoDuration extends IEntity {
resolution: {
resolution_type: string;
start: Date;
end?: Date;
ref?: Date;
};
}
export interface IFindMatchResult {
index: number;
entity: string;
score: number;
}
export class EntityRecognizer {
static yesExp = /^(1|y|yes|yep|sure|ok|true)\z/i;
static noExp = /^(0|n|no|nope|not|false)\z/i;
static numberExp = /[+-]?(?:\d+\.?\d*|\d*\.?\d+)/;
static findEntity(entities: IEntity[], type: string): IEntity {
for (var i = 0; i < entities.length; i++) {
if (entities[i].type == type) {
return entities[i];
}
}
return null;
}
static findAllEntities(entities: IEntity[], type: string): IEntity[] {
var found: IEntity[] = [];
for (var i = 0; i < entities.length; i++) {
if (entities[i].type == type) {
found.push(entities[i]);
}
}
return found;
}
static parseTime(utterance: string): Date;
static parseTime(entities: IEntity[]): Date;
static parseTime(entities: any): Date {
if (typeof entities == 'string') {
entities = EntityRecognizer.recognizeTime(entities);
}
return EntityRecognizer.resolveTime(entities);
}
static resolveTime(entities: IEntity[], timezoneOffset?: number): Date {
var now = new Date();
var date: string;
var time: string;
entities.forEach((entity: ILuisDateTimeEntity) => {
if (entity.resolution) {
switch (entity.resolution.resolution_type) {
case 'builtin.datetime.date':
if (!date) {
date = entity.resolution.date;
}
break;
case 'builtin.datetime.time':
if (!time) {
time = entity.resolution.time;
if (time.length == 3) {
time = time + ':00:00';
} else if (time.length == 6) {
time = time + ':00';
}
// TODO: resolve "ampm" comments
}
break;
case 'chrono.duration':
// Date is already calculated
var duration = <IChronoDuration>entity;
return duration.resolution.start;
}
}
});
if (date || time) {
// The user can just say "at 9am" so we'll use today if no date.
if (!date) {
date = utils.toDate8601(now);
}
if (time) {
// Append time but adjust timezone. Default is to use bots timezone.
if (typeof timezoneOffset !== 'number') {
timezoneOffset = now.getTimezoneOffset() / 60;
}
date = sprintf.sprintf('%s%s%s%02d:00', date, time, (timezoneOffset > 0 ? '-' : '+'), timezoneOffset);
}
return new Date(date);
}
return null;
}
static recognizeTime(utterance: string, refDate?: Date): IChronoDuration {
var response: IChronoDuration;
try {
var results = chrono.parse(utterance, refDate);
if (results && results.length > 0) {
var duration = results[0];
response = {
type: 'chrono.duration',
entity: duration.text,
startIndex: duration.index,
endIndex: duration.index + duration.text.length,
resolution: {
resolution_type: 'chrono.duration',
start: duration.start.date()
}
};
if (duration.end) {
response.resolution.end = duration.end.date();
}
if (duration.ref) {
response.resolution.ref = duration.ref;
}
// Calculate a confidence score based on text coverage and call compareConfidence.
response.score = duration.text.length / utterance.length;
}
} catch (err) {
console.error('Error recognizing time: ' + err.toString());
response = null;
}
return response;
}
static parseNumber(utterance: string): number;
static parseNumber(entities: IEntity[]): number;
static parseNumber(entities: any): number {
var entity: IEntity;
if (typeof entities == 'string') {
entity = { type: 'text', entity: entities.trim() };
} else {
entity = EntityRecognizer.findEntity(entities, 'builtin.number');
}
if (entity) {
var match = this.numberExp.exec(entity.entity);
if (match) {
return Number(match[0]);
}
}
return undefined;
}
static parseBoolean(utterance: string): boolean {
utterance = utterance.trim();
if (EntityRecognizer.yesExp.test(utterance)) {
return true;
} else if (EntityRecognizer.noExp.test(utterance)) {
return false;
}
return undefined;
}
static findBestMatch(choices: string[], utterance: string, threshold = 0.6): IFindMatchResult {
var best: IFindMatchResult;
var matches = EntityRecognizer.findAllMatches(choices, utterance, threshold);
matches.forEach((value) => {
if (!best || value.score > best.score) {
best = value;
}
});
return best;
}
static findAllMatches(choices: string[], utterance: string, threshold = 0.6): IFindMatchResult[] {
var matches: IFindMatchResult[] = [];
utterance = utterance.trim().toLowerCase();
var tokens = utterance.split(' ');
choices.forEach((choice, index) => {
var score = 0.0;
var value = choice.trim().toLowerCase();
if (value.indexOf(utterance) >= 0) {
score = utterance.length / value.length;
} else if (utterance.indexOf(value) >= 0) {
score = value.length / utterance.length;
} else {
var matched = '';
tokens.forEach((token) => {
if (value.indexOf(token) >= 0) {
matched += token;
}
});
score = matched.length / value.length;
}
if (score > threshold) {
matches.push({ index: index, entity: choice, score: score });
}
});
return matches;
}
}

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

@ -0,0 +1,263 @@
import session = require('../Session');
import dialog = require('./Dialog');
import actions = require('./DialogAction');
import consts = require('../consts');
export interface IIntentHandler {
(session: ISession, entities?: IEntity[], intents?: IIntent[]): void;
}
export interface ICaptureIntentHandler {
(action: ISessionAction, intent: IIntent, entities?: IEntity[]): void;
}
interface ICaptureResult extends dialog.IDialogResult<any> {
captured: {
intents: IIntent[];
entities: IEntity[];
};
}
export interface IIntentArgs {
intents: IIntent[];
entities: IEntity[];
}
interface IHandlerMatch {
groupId: string;
handler: IDialogHandler<IIntentArgs>;
}
export abstract class IntentDialog extends dialog.Dialog {
private static CAPTURE_THRESHOLD = 0.6;
private groups: { [id: string]: IntentGroup; } = {};
private beginDialog: IBeginDialogHandler;
private captureIntent: ICaptureIntentHandler;
private intentThreshold = 0.3;
public begin<T>(session: ISession, args: IntentGroup): void {
if (this.beginDialog) {
this.beginDialog(session, args, (handled) => {
if (!handled) {
super.begin(session, args);
}
});
} else {
super.begin(session, args);
}
}
public replyReceived(session: ISession): void {
var msg = session.message;
this.recognizeIntents(msg.language, msg.text, (err, intents, entities) => {
if (!err) {
var topIntent = this.findTopIntent(intents);
var score = topIntent ? topIntent.score : 0;
session.compareConfidence(msg.language, msg.text, score, (handled) => {
if (!handled) {
this.invokeIntent(session, intents, entities);
}
});
} else {
session.endDialog({ error: new Error('Intent recognition error: ' + err.message) });
}
});
}
public dialogResumed(session: ISession, result: ICaptureResult): void {
if (result.captured) {
this.invokeIntent(session, result.captured.intents, result.captured.entities);
} else {
var activeGroup: string = session.dialogData[consts.Data.Group];
var activeIntent: string = session.dialogData[consts.Data.Intent];
var group = activeGroup ? this.groups[activeGroup] : null;
var handler = group && activeIntent ? group._intentHandler(activeIntent) : null;
if (handler) {
handler(session, <any>result);
} else {
super.dialogResumed(session, result);
}
}
}
public compareConfidence(action: ISessionAction, language: string, utterance: string, score: number): void {
// First check to see if the childs confidence is low and that we have a capture handler.
if (score < IntentDialog.CAPTURE_THRESHOLD && this.captureIntent) {
this.recognizeIntents(language, utterance, (err, intents, entities) => {
var handled = false;
if (!err) {
// Ensure capture handler is worth invoking. Requirements are the top intents
// score should be greater then the childs score and there should be a handler
// registered for that intent. The last requirement addresses the fact that the
// 'None' intent from LUIS is likely to have a score that's greater then the
// intent threshold.
var matches: IHandlerMatch;
var topIntent = this.findTopIntent(intents);
if (topIntent && topIntent.score > this.intentThreshold && topIntent.score > score) {
matches = this.findHandler(topIntent);
}
if (matches) {
this.captureIntent({
next: action.next,
userData: action.userData,
dialogData: action.dialogData,
endDialog: () => {
action.endDialog({
resumed: dialog.ResumeReason.completed,
captured: {
intents: intents,
entities: entities
}
});
},
send: action.send
}, topIntent, entities);
} else {
action.next();
}
} else {
console.error('Intent recognition error: ' + err.message);
action.next();
}
});
} else {
action.next();
}
}
public addGroup(group: IntentGroup): this {
var id = group.getId();
if (!this.groups.hasOwnProperty(id)) {
this.groups[id] = group;
} else {
throw "Group of " + id + " already exists within the dialog.";
}
return this;
}
public onBegin(handler: IBeginDialogHandler): this {
this.beginDialog = handler;
return this;
}
public on(intent: string, fn: IDialogHandler<IIntentArgs>): this;
public on(intent: string, waterfall: actions.IDialogWaterfallStep[]): this;
public on(intent: string, dialogId: string, dialogArgs?: any): this;
public on(intent: string, dialogId: any, dialogArgs?: any): this {
this.getDefaultGroup().on(intent, dialogId, dialogArgs);
return this;
}
public onDefault(fn: IDialogHandler<IIntentArgs>): this;
public onDefault(waterfall: actions.IDialogWaterfallStep[]): this;
public onDefault(dialogId: string, dialogArgs?: any): this;
public onDefault(dialogId: any, dialogArgs?: any): this {
this.getDefaultGroup().on(consts.Intents.Default, dialogId, dialogArgs);
return this;
}
public getThreshold(): number {
return this.intentThreshold;
}
public setThreshold(score: number): this {
this.intentThreshold = score;
return this;
}
private invokeIntent(session: ISession, intents: IIntent[], entities: IEntity[]): void {
try {
// Find top intent, group, and handler;
var match: IHandlerMatch;
var topIntent = this.findTopIntent(intents);
if (topIntent && topIntent.score > this.intentThreshold) {
match = this.findHandler(topIntent);
}
if (!match) {
match = {
groupId: consts.Id.DefaultGroup,
handler: this.getDefaultGroup()._intentHandler(consts.Intents.Default)
};
}
// Invoke handler
if (match) {
session.dialogData[consts.Data.Group] = match.groupId;
session.dialogData[consts.Data.Intent] = topIntent.intent;
match.handler(session, { intents: intents, entities: entities });
} else {
session.send();
}
} catch (e) {
session.endDialog({ error: new Error('Exception handling intent: ' + e.message) });
}
}
private findTopIntent(intents: IIntent[]): IIntent {
var topIntent: IIntent;
for (var i = 0; i < intents.length; i++) {
var intent = intents[i];
if (!topIntent) {
topIntent = intent;
} else if (intent.score > topIntent.score) {
topIntent = intent;
}
}
return topIntent;
}
private findHandler(intent: IIntent): IHandlerMatch {
for (var groupId in this.groups) {
var handler = this.groups[groupId]._intentHandler(intent.intent);
if (handler) {
return { groupId: groupId, handler: handler };
}
}
return null;
}
private getDefaultGroup(): IntentGroup {
var group = this.groups[consts.Id.DefaultGroup];
if (!group) {
this.groups[consts.Id.DefaultGroup] = group = new IntentGroup(consts.Id.DefaultGroup);
}
return group;
}
protected abstract recognizeIntents(language: string, utterance: string, callback: (err: Error, intents?: IIntent[], entities?: IEntity[]) => void): void;
}
export class IntentGroup {
private handlers: { [id: string]: IDialogHandler<IIntentArgs>; } = {};
constructor(private id: string) {
}
public getId(): string {
return this.id;
}
/** Returns the handler registered for an intent if it exists. */
public _intentHandler(intent: string): IDialogHandler<IIntentArgs> {
return this.handlers[intent];
}
public on(intent: string, fn: IDialogHandler<IIntentArgs>): this;
public on(intent: string, waterfall: actions.IDialogWaterfallStep[]): this;
public on(intent: string, dialogId: string, dialogArgs?: any): this;
public on(intent: string, dialogId: any, dialogArgs?: any): this {
if (!this.handlers.hasOwnProperty(intent)) {
if (Array.isArray(dialogId)) {
this.handlers[intent] = actions.DialogAction.waterfall(dialogId);
} else if (typeof dialogId == 'string') {
this.handlers[intent] = actions.DialogAction.beginDialog(dialogId, dialogArgs);
} else {
this.handlers[intent] = dialogId;
}
} else {
throw new Error('Intent[' + intent + '] already exists.');
}
return this;
}
}

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

@ -0,0 +1,124 @@
import intent = require('./IntentDialog');
import dialog = require('./Dialog');
import utils = require('../utils');
import request = require('request');
import sprintf = require('sprintf-js');
interface ILuisResults {
query: string;
intents: IIntent[];
entities: IEntity[];
}
export class LuisDialog extends intent.IntentDialog {
constructor(private serviceUri: string) {
super();
}
protected recognizeIntents(language: string, utterance: string, callback: (err: Error, intents?: IIntent[], entities?: IEntity[]) => void): void {
LuisDialog.recognize(utterance, this.serviceUri, callback);
}
static recognize(utterance: string, serviceUri: string, callback: (err: Error, intents?: IIntent[], entities?: IEntity[]) => void): void {
var uri = serviceUri.trim();
if (uri.lastIndexOf('&q=') != uri.length - 3) {
uri += '&q=';
}
uri += encodeURIComponent(utterance || '');
request.get(uri, (err: Error, res: any, body: string) => {
try {
if (!err) {
var result: ILuisResults = JSON.parse(body);
if (result.intents.length == 1 && !result.intents[0].hasOwnProperty('score')) {
// Intents for the builtin Cortana app don't return a score.
result.intents[0].score = 1.0;
}
callback(null, result.intents, result.entities);
} else {
callback(err);
}
} catch (e) {
callback(e);
}
});
}
}
interface ILuisDateTimeEntity extends IEntity {
resolution: ILuisDateTimeResolution;
}
interface ILuisDateTimeResolution {
resolution_type: string;
date?: string;
time?: string;
comment?: string;
duration?: string;
}
export class LuisEntityResolver {
static findEntity(entities: IEntity[], type: string): IEntity {
for (var i = 0; i < entities.length; i++) {
if (entities[i].type == type) {
return entities[i];
}
}
return null;
}
static findAllEntities(entities: IEntity[], type: string): IEntity[] {
var found: IEntity[] = [];
for (var i = 0; i < entities.length; i++) {
if (entities[i].type == type) {
found.push(entities[i]);
}
}
return found;
}
static resolveDate(entities: IEntity[], timezoneOffset?: number): Date {
var now = new Date();
var date: string;
var time: string;
for (var i = 0; i < entities.length; i++) {
var entity = <ILuisDateTimeEntity>entities[i];
if (entity.resolution) {
switch (entity.resolution.resolution_type) {
case 'builtin.datetime.date':
if (!date) {
date = entity.resolution.date;
}
break;
case 'builtin.datetime.time':
if (!time) {
time = entity.resolution.time;
if (time.length == 3) {
time = time + ':00:00';
} else if (time.length == 6) {
time = time + ':00';
}
// TODO: resolve "ampm" comments
}
break;
}
}
}
if (date || time) {
// The user can just say "at 9am" so we'll use today if no date.
if (!date) {
date = utils.toDate8601(now);
}
if (time) {
// Append time but adjust timezone. Default is to use bots timezone.
if (typeof timezoneOffset !== 'number') {
timezoneOffset = now.getTimezoneOffset() / 60;
}
date = sprintf.sprintf('%s%s%s%02d:00', date, time, (timezoneOffset > 0 ? '-' : '+'), timezoneOffset);
}
return new Date(date);
} else {
return null;
}
}
}

317
Node/src/dialogs/Prompts.ts Normal file
Просмотреть файл

@ -0,0 +1,317 @@
import dialog = require('./Dialog');
import session = require('../Session');
import consts = require('../consts');
import entities = require('./EntityRecognizer');
export enum PromptType { text, number, confirm, choice, time }
export enum ListStyle { none, inline, list }
export interface IPromptOptions {
retryPrompt?: string;
maxRetries?: number;
refDate?: number;
listStyle?: ListStyle;
}
export interface IPromptArgs extends IPromptOptions {
promptType: PromptType;
prompt: string;
enumValues?: string[];
}
export interface IPromptResult<T> extends dialog.IDialogResult<T> {
promptType?: PromptType;
}
export interface IPromptRecognizerResult<T> extends IPromptResult<T> {
handled?: boolean;
}
export interface IPromptRecognizer {
recognize<T>(args: IPromptRecognizerArgs, callback: (result: IPromptRecognizerResult<T>) => void, session?: ISession): void;
}
export interface IPromptRecognizerArgs {
promptType: PromptType;
language: string;
utterance: string;
enumValues?: string[];
refDate?: number;
compareConfidence(language: string, utterance: string, score: number, callback: (handled: boolean) => void): void;
}
export interface IPromptsOptions {
recognizer?: IPromptRecognizer
}
export interface IChronoDuration extends IEntity {
resolution: {
start: Date;
end?: Date;
ref?: Date;
};
}
export class SimplePromptRecognizer implements IPromptRecognizer {
private cancelExp = /^(cancel|nevermind|never mind|back|stop|forget it)/i;
public recognize(args: IPromptRecognizerArgs, callback: (result: IPromptRecognizerResult<any>) => void, session?: ISession): void {
this.checkCanceled(args, () => {
try {
// Recognize value
var score = 0.0;
var response: any;
var text = args.utterance.trim();
switch (args.promptType) {
case PromptType.text:
// This is an open ended question so it's a little tricky to know what to pass as a confidence
// score. Currently we're saying that we have 0.1 confidence that we understand the users intent
// which will give all of the prompts parents a chance to capture the utterance. If no one
// captures the utterance we'll return the full text of the utterance as the result.
score = 0.1;
response = text;
break;
case PromptType.number:
var n = entities.EntityRecognizer.parseNumber(text);
if (!isNaN(n)) {
var score = n.toString().length / text.length;
response = n;
}
break;
case PromptType.confirm:
var b = entities.EntityRecognizer.parseBoolean(text);
if (typeof b == 'boolean') {
score = 1.0;
response = b;
}
break;
case PromptType.time:
var entity = entities.EntityRecognizer.recognizeTime(text, args.refDate ? new Date(args.refDate) : null);
if (entity) {
score = entity.entity.length / text.length;
response = entity;
}
break;
case PromptType.choice:
var best = entities.EntityRecognizer.findBestMatch(args.enumValues, text);
if (!best) {
var n = entities.EntityRecognizer.parseNumber(text);
if (!isNaN(n) && n > 0 && n <= args.enumValues.length) {
best = { index: n, entity: args.enumValues[n - 1], score: 1.0 };
}
}
if (best) {
score = best.score;
response = best;
}
break;
default:
}
// Return results
args.compareConfidence(args.language, text, score, (handled) => {
if (!handled && score > 0) {
callback({ resumed: dialog.ResumeReason.completed, promptType: args.promptType, response: response });
} else {
callback({ resumed: dialog.ResumeReason.notCompleted, promptType: args.promptType, handled: handled });
}
});
} catch (err) {
callback({ resumed: dialog.ResumeReason.notCompleted, promptType: args.promptType, error: err instanceof Error ? err : new Error(err.toString()) });
}
}, callback);
}
protected checkCanceled(args: IPromptRecognizerArgs, onContinue: Function, callback: (result: IPromptRecognizerResult<IEntity>) => void) {
if (!this.cancelExp.test(args.utterance.trim())) {
onContinue();
} else {
callback({ resumed: dialog.ResumeReason.canceled, promptType: args.promptType });
}
}
}
export class Prompts extends dialog.Dialog {
private static options: IPromptsOptions = {
recognizer: new SimplePromptRecognizer()
};
public begin(session: ISession, args: IPromptArgs): void {
args = <any>args || {};
args.maxRetries = args.maxRetries || 1;
for (var key in args) {
if (args.hasOwnProperty(key)) {
session.dialogData[key] = (<any>args)[key];
}
}
session.send(args.prompt);
}
public replyReceived(session: ISession): void {
var args: IPromptArgs = session.dialogData;
Prompts.options.recognizer.recognize(
{
promptType: args.promptType,
utterance: session.message.text,
language: session.message.language,
enumValues: args.enumValues,
refDate: args.refDate,
compareConfidence: function (language, utterance, score, callback) {
session.compareConfidence(language, utterance, score, callback);
}
}, (result) => {
if (!result.handled) {
if (result.error || result.resumed == dialog.ResumeReason.completed ||
result.resumed == dialog.ResumeReason.canceled || args.maxRetries == 0) {
result.promptType = args.promptType;
session.endDialog(result);
} else {
args.maxRetries--;
session.send(args.retryPrompt || "I didn't understand. " + args.prompt);
}
}
});
}
static configure(options: IPromptsOptions): void {
if (options) {
for (var key in options) {
if (options.hasOwnProperty(key)) {
(<any>Prompts.options)[key] = (<any>options)[key];
}
}
}
}
static text(ses: session.Session, prompt: string): void {
beginPrompt(ses, {
promptType: PromptType.text,
prompt: prompt
});
}
static recognizeText(language: string, text: string, callback: (result: IPromptResult<string>) => void): void {
Prompts.options.recognizer.recognize(
{
promptType: PromptType.text,
language: language,
utterance: text,
compareConfidence: (language, utterance, score, callback) => {
callback(false);
}
}, callback);
}
static number(ses: session.Session, prompt: string, options?: IPromptOptions): void {
var args: IPromptArgs = <any>options || {};
args.promptType = PromptType.number;
args.prompt = prompt;
beginPrompt(ses, args);
}
static recognizeNumber(language: string, text: string, callback: (result: IPromptResult<number>) => void): void {
Prompts.options.recognizer.recognize(
{
promptType: PromptType.number,
language: language,
utterance: text,
compareConfidence: (language, utterance, score, callback) => {
callback(false);
}
}, callback);
}
static confirm(ses: session.Session, prompt: string, options?: IPromptOptions): void {
var args: IPromptArgs = <any>options || {};
args.promptType = PromptType.confirm;
args.prompt = prompt;
beginPrompt(ses, args);
}
static recognizeConfirm(language: string, text: string, callback: (result: IPromptResult<boolean>) => void): void {
Prompts.options.recognizer.recognize(
{
promptType: PromptType.confirm,
language: language,
utterance: text,
compareConfidence: (language, utterance, score, callback) => {
callback(false);
}
}, callback);
}
static choice(ses: session.Session, prompt: string, enumValues: string[], options?: IPromptOptions): void {
var args: IPromptArgs = <any>options || {};
args.promptType = PromptType.choice;
args.prompt = prompt;
args.enumValues = enumValues;
args.listStyle = args.listStyle || ListStyle.list;
// Format list
var connector = '', list: string;
switch (args.listStyle) {
case ListStyle.list:
list = '\n ';
enumValues.forEach((value, index) => {
list += connector + (index + 1) + '. ' + value;
connector = '\n ';
});
args.prompt += list;
break;
case ListStyle.inline:
list = ' ';
enumValues.forEach((value, index) => {
list += connector + (index + 1) + '. ' + value;
if (index == enumValues.length - 2) {
connector = index == 0 ? ' or ' : ', or ';
} else {
connector = ', ';
}
});
args.prompt += list;
break;
}
beginPrompt(ses, args);
}
static recognizeChoice(language: string, text: string, enumValues: string[], callback: (result: IPromptResult<string>) => void): void {
Prompts.options.recognizer.recognize(
{
promptType: PromptType.choice,
language: language,
utterance: text,
enumValues: enumValues,
compareConfidence: (language, utterance, score, callback) => {
callback(false);
}
}, callback);
}
static time(ses: session.Session, prompt: string, options?: IPromptOptions): void {
var args: IPromptArgs = <any>options || {};
args.promptType = PromptType.time;
args.prompt = prompt;
beginPrompt(ses, args);
}
static recognizeTime(language: string, text: string, refDate: Date, callback: (result: IPromptResult<IEntity>) => void): void {
Prompts.options.recognizer.recognize(
{
promptType: PromptType.time,
language: language,
utterance: text,
refDate: (refDate || new Date()).getTime(),
compareConfidence: (language, utterance, score, callback) => {
callback(false);
}
}, callback);
}
}
function beginPrompt(ses: session.Session, args: IPromptArgs) {
if (!ses.dialogs.hasDialog(consts.DialogId.Prompts)) {
ses.dialogs.add(consts.DialogId.Prompts, new Prompts());
}
ses.beginDialog(consts.DialogId.Prompts, args);
}

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

@ -0,0 +1,24 @@
import dialog = require('./Dialog');
export class SimpleDialog extends dialog.Dialog {
constructor(private fn: (session: ISession, arg?: any) => void) {
super();
}
public begin<T>(session: ISession, args?: T): void {
this.fn(session, args);
}
public replyReceived(session: ISession): void {
session.compareConfidence(session.message.language, session.message.text, 0.0, (handled) => {
if (!handled) {
this.fn(session);
}
});
}
public dialogResumed(session: ISession, result: any): void {
this.fn(session, result);
}
}

127
Node/src/interfaces.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,127 @@

interface IMessage {
type?: string;
id?: string;
conversationId?: string;
created?: string;
sourceText?: string;
sourceLanguage?: string;
language?: string;
text?: string;
attachments?: IAttachment[];
from?: IChannelAccount;
to?: IChannelAccount;
replyTo?: IChannelAccount;
replyToMessageId?: string;
participants?: IChannelAccount[];
totalParticipants?: number;
mentions?: IMention[];
place?: string;
channelMessageId?: string;
channelConversationId?: string;
channelData?: any;
location?: ILocation;
hashtags?: string[];
eTag?: string;
}
interface IAttachment {
contentType: string;
contentUrl?: string;
content?: any;
fallbackText?: string;
title?: string;
titleLink?: string;
text?: string;
thumbnailUrl?: string;
}
interface IChannelAccount {
name?: string;
channelId: string;
address: string;
id?: string;
isBot?: boolean;
}
interface IMention {
mentioned?: IChannelAccount;
text?: string;
}
interface ILocation {
altitude?: number;
latitude: number;
longitude: number;
}
interface IBeginDialogAddress {
to: IChannelAccount;
from?: IChannelAccount;
language?: string;
text?: string;
}
interface ILocalizer {
gettext(language: string, msgid: string): string;
ngettext(language: string, msgid: string, msgid_plural: string, count: number): string;
}
interface ISession {
sessionState: ISessionState;
message: IMessage;
userData: any;
dialogData: any;
error(err: Error): ISession;
gettext(msgid: string, ...args: any[]): string;
ngettext(msgid: string, msgid_plural: string, count: number): string;
send(): ISession;
send(msg: string, ...args: any[]): ISession;
send(msg: IMessage): ISession;
messageSent(): boolean;
beginDialog<T>(id: string, args?: T): ISession;
endDialog(result?: any): ISession;
compareConfidence(language: string, utterance: string, score: number, callback: (handled: boolean) => void): void;
reset(id: string): ISession;
isReset(): boolean;
}
interface ISessionAction {
userData: any;
dialogData: any;
next(): void;
endDialog(result?: any): void;
send(msg: string, ...args: any[]): void;
send(msg: IMessage): void;
}
interface ISessionState {
callstack: IDialogState[];
lastAccess: number;
}
interface IDialogState {
id: string;
state: any;
}
interface IBeginDialogHandler {
(session: ISession, args: any, next: (handled: boolean) => void): void;
}
interface IDialogHandler<T> {
(session: ISession, args?: T): void;
}
interface IIntent {
intent: string;
score: number;
}
interface IEntity {
entity: string;
type: string;
startIndex?: number;
endIndex?: number;
score?: number;
}

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

@ -0,0 +1,31 @@
import utils = require('../utils');
export interface IStorage {
get(id: string, callback: (err: Error, data: any) => void): void;
save(id: string, data: any, callback?: (err: Error) => void): void;
}
export class MemoryStorage implements IStorage {
private store: { [id: string]: any; } = {};
public get(id: string, callback: (err: Error, data: any) => void): void {
if (this.store.hasOwnProperty(id)) {
callback(null, utils.clone(this.store[id]));
} else {
callback(null, null);
}
}
public save(id: string, data: any, callback?: (err: Error) => void): void {
this.store[id] = utils.clone(data || {});
if (callback) {
callback(null);
}
}
public delete(id: string) {
if (this.store.hasOwnProperty(id)) {
delete this.store[id];
}
}
}

13
Node/src/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": false,
"noImplicitAny": true,
"outDir": "..\build",
"noEmitOnError": true
},
"exclude": [
"botbuilder.d.ts"
]
}

17
Node/src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,17 @@
import sprintf = require('sprintf-js');
export function clone(obj: any): any {
var cpy: any = {};
if (obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
cpy[key] = obj[key];
}
}
}
return cpy;
}
export function toDate8601(date: Date): string {
return sprintf.sprintf('%04d-%02d-%02d', date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
}