iopa-bot
Version:
API-First Bot Framework for Internet of Things (IoT), based on Internet of Protocols Alliance (IOPA) specification
205 lines (167 loc) • 5.88 kB
text/typescript
/*
* Iopa Bot Framework
* Copyright (c) 2016-2019 Internet of Protocols Alliance
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Iopa from 'iopa'
const { IOPA, SERVER } = Iopa.constants
import { BOT } from '../constants'
export interface DialogCapability {
dialogs: { [key: string]: Dialog }
beginDialog(
name: string,
context: Iopa.Context,
next: () => Promise<void>
): Promise<void>
}
class Dialog {
public name: string
public steps: DialogStep[]
constructor(name, steps) {
this.name = name
this.steps = steps
}
}
type DialogStep = string[] | Iopa.FC
export default class DialogManager {
app: any
dialogs: { [key: string]: Dialog } = {}
constructor(app) {
this.app = app
app.dialog = (name, ...args) => {
if (!(typeof name === 'string')) {
throw new Error(
'dialog must start with dialog name, then array of intents, then function to call'
)
}
this.dialogs[name] = new Dialog(name, args)
}
app.properties[SERVER.Capabilities][BOT.CAPABILITIES.Dialog] = {
dialogs: this.dialogs,
beginDialog: (
name: string,
context: Iopa.Context,
next: () => Promise<void>
) => {
const dialog = this.dialogs[name]
if (!dialog) {
console.log(`Dialog ${name} not a v1 dialog`)
return next()
}
if (context['urn:bot:dialog:invoke']) {
context[BOT.Session][BOT.Skill] = 'default'
}
let dialogFunc = dialog.steps[0] as Iopa.FC
if (typeof dialogFunc != 'function') {
dialogFunc = dialog.steps[1] as Iopa.FC
context[BOT.Session][BOT.CurrentDialog] = {
name: dialog.name,
step: 2,
totalSteps: dialog.steps.length
}
} else {
context[BOT.Session][BOT.CurrentDialog] = {
name: dialog.name,
step: 1,
totalSteps: dialog.steps.length
}
}
resetSessionSkill(context)
return dialogFunc(context, next)
}
} as DialogCapability
app.properties[SERVER.Capabilities][BOT.CAPABILITIES.Dialog][IOPA.Version] =
BOT.VERSION
}
invoke(context, next) {
if (context['urn:bot:dialog:invoke']) {
const dialogId = context['urn:bot:dialog:invoke']
return context[SERVER.Capabilities][BOT.CAPABILITIES.Dialog].beginDialog(
dialogId,
context,
next
)
}
if (!context[BOT.Intent]) return next()
// must have an intent to process dialog
console.log('> skill', context[BOT.Session][BOT.Skill])
console.log('> intent', context[BOT.Intent])
console.log('> dialog', JSON.stringify(context[BOT.Session][BOT.CurrentDialog], null, 2))
if (!context[BOT.Session][BOT.CurrentDialog])
return this._matchBeginDialog(context, next)
return this._continueDialog(context, next)
}
private _matchBeginDialog(context, next) {
let dialogFunc: Iopa.FC | null = null
for (var key in this.dialogs) {
const dialog = this.dialogs[key]
if (typeof dialog.steps[0] != 'function') {
let intents = (dialog.steps[0] as unknown) as Array<string>
if (intents.includes(context[BOT.Intent]) || intents.includes('*')) {
dialogFunc = dialog.steps[1] as Iopa.FC
context[BOT.Session][BOT.CurrentDialog] = {
name: dialog.name,
step: 2,
totalSteps: dialog.steps.length
}
resetSessionSkill(context)
break
}
}
}
if (dialogFunc) return dialogFunc(context, next)
else return next()
}
private _continueDialog(context, next) {
var sessionDialog = context[BOT.Session][BOT.CurrentDialog]
var dialog = this.dialogs[sessionDialog.name]
if (!dialog) {
// not a recognized flow but do not clear in case its a V2 dialog
// with invalid consumer input, in which case we need to retain the
// current dialog information to continue after the
return this._matchBeginDialog(context, next)
}
if (sessionDialog.step >= dialog.steps.length) {
// was at end of dialog so just clear
context[BOT.Session][BOT.CurrentDialog] = null
context[BOT.Session][BOT.LastDialogEndedDate] = new Date().getTime()
resetSessionSkill(context)
return this._matchBeginDialog(context, next)
}
let intentFilter: string[] | null
let dialogFunc: Iopa.FC
intentFilter = dialog.steps[sessionDialog.step] as string[]
if (typeof intentFilter == 'function') {
// Dialog step has no intent filter, invoke dialogFunc
dialogFunc = intentFilter
intentFilter = null
} else if (
intentFilter &&
!intentFilter.includes(context[BOT.Intent]) &&
!intentFilter.includes('*')
) {
// No matching intent for current dialog step, see if we should start another dialog
return this._matchBeginDialog(context, next)
} else {
// Match with current dialog step intent filter, advance and invoke dialogFunc
sessionDialog.step++
dialogFunc = dialog.steps[sessionDialog.step] as Iopa.FC
}
sessionDialog.step++
return dialogFunc(context, next)
}
}
function resetSessionSkill(context: Iopa.Context) {
delete context[BOT.Session][BOT.SkillVersion]
}