feat: change Init() to Factory patterns to avoid temporal coupling (Migrated branch) (#8)

* feat: use Factory pattern instead of static Init()

* feat: change Init() to Factory patterns to avoid temporal coupling

* fix: update sample demos

* fix: paths to import from source instead of root

* fix: clmodeloptions type

* fix: exclude webchat from windows test
This commit is contained in:
Matt Mazzola 2019-12-18 11:27:02 -08:00 коммит произвёл GitHub
Родитель 0b7e73ecb2
Коммит 07f262ec40
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
28 изменённых файлов: 427 добавлений и 344 удалений

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

@ -1,68 +1,68 @@
# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema
trigger:
- master
- master
pr:
autoCancel: true
branches:
include:
- master
- master
jobs:
- job: linux_build
pool:
vmImage: 'ubuntu-16.04'
- job: linux_build
pool:
vmImage: "ubuntu-16.04"
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js 10.x'
steps:
- task: NodeTool@0
inputs:
versionSpec: "10.x"
displayName: "Install Node.js 10.x"
- bash: npm --version
displayName: 'npm --version'
- bash: npm --version
displayName: "npm --version"
- bash: npx lerna boostrap
displayName: 'npx lerna boostrap'
- bash: npx lerna boostrap
displayName: "npx lerna boostrap"
# - task: CacheBeta@0
# inputs:
# key: $(Build.SourcesDirectory)/package-lock.json
# path: $(npm_config_cache)
# displayName: Cache npm
# - task: CacheBeta@0
# inputs:
# key: $(Build.SourcesDirectory)/package-lock.json
# path: $(npm_config_cache)
# displayName: Cache npm
- bash: npx lerna run build
displayName: 'npx lerna run build'
- bash: npx lerna run build
displayName: "npx lerna run build"
- bash: npx lerna run test --ignore @conversationlearner/webchat
displayName: 'npx lerna run test --ignore @conversationlearner/webchat'
- bash: npx lerna run test --ignore @conversationlearner/webchat
displayName: "npx lerna run test --ignore @conversationlearner/webchat"
- job: windows_build
pool:
vmImage: 'windows-2019'
- job: windows_build
pool:
vmImage: "windows-2019"
steps:
- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2
displayName: 'Run CredScan'
inputs:
debugMode: false
steps:
- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2
displayName: "Run CredScan"
inputs:
debugMode: false
- task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1
displayName: 'Post Analysis'
inputs:
CredScan: true
- task: securedevelopmentteam.vss-secure-development-tools.build-task-postanalysis.PostAnalysis@1
displayName: "Post Analysis"
inputs:
CredScan: true
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js 10.x'
- task: NodeTool@0
inputs:
versionSpec: "10.x"
displayName: "Install Node.js 10.x"
- script: npm ci
displayName: 'npm ci'
- script: npm ci
displayName: "npm ci"
- script: npx lerna run build
displayName: 'npx lerna run build'
- script: npx lerna run build
displayName: "npx lerna run build"
- script: npx lerna run test
displayName: 'npx lerna run test'
- script: npx lerna run test --ignore @conversationlearner/webchat
displayName: "npx lerna run test --ignore @conversationlearner/webchat"

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from './config'
@ -34,12 +34,12 @@ const fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const conversationLearnerFactory = new ConversationLearnerFactory(clOptions, fileStorage)
const includeSdk = ['development', 'test'].includes(process.env.NODE_ENV ?? '')
if (includeSdk) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', conversationLearnerFactory.sdkRouter)
// Note: Must be mounted at root to use internal /ui paths
console.log(chalk.greenBright(`Adding /ui routes`))
@ -49,7 +49,7 @@ if (includeSdk) {
// Serve default bot summary page. Should be customized by customer.
server.use(express.static(path.join(__dirname, '..', 'site')))
const cl = new ConversationLearner(modelId)
const cl = conversationLearnerFactory.create(modelId)
//=================================
// Add Entity Logic

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

@ -4,7 +4,7 @@
*/
import * as dotenv from 'dotenv'
import * as convict from 'convict'
import { ICLOptions } from '@conversationlearner/sdk'
import { CLOptions } from '@conversationlearner/sdk'
const result = dotenv.config()
if (result.error) {
@ -78,7 +78,7 @@ export const config = convict({
config.validate({ allowed: 'strict' })
export interface ICLSampleConfig extends ICLOptions {
export interface ICLSampleConfig extends CLOptions {
modelId: string | undefined
redisServer: string | undefined
redisKey: string | undefined

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, uiRouter, CosmosLogStorage } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, uiRouter, CosmosLogStorage } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -54,16 +54,15 @@ async function main() {
// Use custom log storage
const logStorage = cosmosServer ? await CosmosLogStorage.Get({ endpoint: cosmosServer, key: cosmosKey }) : undefined
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage, logStorage)
const conversationLearnerFactory = new ConversationLearnerFactory(clOptions, fileStorage, logStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', conversationLearnerFactory.sdkRouter)
}
const cl = new ConversationLearner(modelId)
const cl = conversationLearnerFactory.create(modelId)
cl.EntityDetectionCallback = (async (text: string, memoryManager: ClientMemoryManager): Promise<void> => {
memoryManager.Get("name", ClientMemoryManager.AS_STRING)
})

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter, ConversationState, AutoSaveStateMiddleware } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, ReadOnlyClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, ReadOnlyClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -45,10 +45,10 @@ let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
//=================================
@ -59,7 +59,7 @@ var isInStock = function (topping: string) {
return (inStock.indexOf(topping.toLowerCase()) > -1)
}
let clPizza = new ConversationLearner("2d9884f4-75a3-4f63-8b1e-d885ac02663e")
let clPizza = clFactory.create("2d9884f4-75a3-4f63-8b1e-d885ac02663e");
clPizza.EntityDetectionCallback = async (text: string, memoryManager: ClientMemoryManager): Promise<void> => {
// Clear OutOfStock List
@ -112,7 +112,7 @@ var resolveApps = function (appName: string) {
return apps.filter(n => n.includes(appName))
}
let clVr = new ConversationLearner("997dc1e2-c0c0-4812-9429-446e31cfdf99")
let clVr = clFactory.create("997dc1e2-c0c0-4812-9429-446e31cfdf99");
clVr.EntityDetectionCallback = async (text: string, memoryManager: ClientMemoryManager): Promise<void> => {
// Clear disambigApps

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -46,12 +46,12 @@ let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
let cl = clFactory.create(modelId);
//=================================
// Add Entity Logic
@ -65,8 +65,8 @@ let cl = new ConversationLearner(modelId)
// Define any API callbacks
//=================================
//
// No API calls are used in this demo, so there are no calls to ConversationLearner.AddAPICallback
// See other demos, or app.ts in the src directory, for an example of ConversationLearner.AddAPICallback
// No API calls are used in this demo, so there are no calls to ConversationLearner.AddCallback
// See other demos, or app.ts in the src directory, for an example of ConversationLearner.AddCallback
//
//=================================

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, ReadOnlyClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, ReadOnlyClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -46,12 +46,12 @@ let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
let cl = clFactory.create(modelId);
//=========================================================
// Bots Buisness Logic

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

@ -4,7 +4,7 @@
*/
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, RedisStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, RedisStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -40,14 +40,14 @@ const adapter = new BotFrameworkAdapter({ appId: bfAppId, appPassword: bfAppPass
// IN-MEMORY STORAGE
// Stores bot state in memory.
// If the bot is stopped and re-started, the state of in-progress sessions will be lost.
//ConversationLearner.Init(clOptions);
// const clFactory = new ConversationLearnerFactory(clOptions)
// FILE STORAGE
// Stores bot state in a local file.
// With this option, the bot can be stopped and re-started without losing the state of in-progress sessions.
// Requires local disk access.
//let fileStorage = new FileStorage( {path: path.join(__dirname, 'storage')})
//ConversationLearner.Init(clOptions, fileStorage);
// const fileStorage = new FileStorage( {path: path.join(__dirname, 'storage')})
// const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
// REDIS
// Stores bot state in a redis cache.
@ -61,23 +61,22 @@ if (typeof config.redisKey !== 'string' || config.redisKey.length === 0) {
throw new Error(`When using Redis storage: redisKey value must be non-empty. You passed: ${config.redisKey}`)
}
let redisStorage = new RedisStorage({ server: config.redisServer, key: config.redisKey })
const redisStorage = new RedisStorage({ server: config.redisServer, key: config.redisKey })
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, redisStorage)
const clFactory = new ConversationLearnerFactory(clOptions, redisStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
const cl = clFactory.create(modelId)
//=================================
// Handle Incoming Messages
//=================================
server.post('/api/messages', (req, res) => {
adapter.processActivity(req, res, async context => {
let result = await cl.recognize(context)

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -41,23 +41,23 @@ const adapter = new BotFrameworkAdapter({ appId: bfAppId, appPassword: bfAppPass
// Initialize ConversationLearner using file storage.
// Recommended only for development
// See "storageDemo.ts" for other storage options
let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
const fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
const cl = clFactory.create(modelId)
//=========================================================
// Bots Buisness Logic
// Bots Business Logic
//=========================================================
var apps = ["skype", "outlook", "amazon video", "amazon music"]
var resolveApps = function (appName: string) {
const apps = ["skype", "outlook", "amazon video", "amazon music"]
const resolveApps = (appName: string) => {
return apps.filter(n => n.includes(appName))
}
@ -77,7 +77,7 @@ cl.EntityDetectionCallback = async (text: string, memoryManager: ClientMemoryMan
memoryManager.Delete("UnknownAppName")
// Get list of (possibly) ambiguous apps
var appNames = memoryManager.Get("AppName", ClientMemoryManager.AS_STRING_LIST)
const appNames = memoryManager.Get("AppName", ClientMemoryManager.AS_STRING_LIST)
if (appNames.length > 0) {
const resolvedAppNames = appNames
.map(appName => resolveApps(appName))

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

@ -4,7 +4,7 @@
*/
import * as path from 'path'
import * as express from 'express'
import { ConversationLearner, ClientMemoryManager, FileStorage, ReadOnlyClientMemoryManager, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, ReadOnlyClientMemoryManager, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import * as request from 'request'
@ -43,25 +43,25 @@ const adapter = new BB.BotFrameworkAdapter({ appId: bfAppId, appPassword: bfAppP
// Initialize ConversationLearner using file storage.
// Recommended only for development
// See "storageDemo.ts" for other storage options
let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
const fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
const cl = clFactory.create(modelId)
//=========================================================
// Bots Buisness Logic
// Bots Business Logic
//=========================================================
var greetings = [
const greetings = [
"Hello!",
"Greetings!",
"Hi there!"
"Hi there!",
]
//=================================

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -41,23 +41,23 @@ const adapter = new BotFrameworkAdapter({ appId: bfAppId, appPassword: bfAppPass
// Initialize ConversationLearner using file storage.
// Recommended only for development
// See "storageDemo.ts" for other storage options
let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
const fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
const cl = clFactory.create(modelId)
//=========================================================
// Bots Business Logic
//=========================================================
let cities = ['new york', 'boston', 'new orleans', 'chicago']
let cityMap: { [index: string]: string } = {}
const cities = ['new york', 'boston', 'new orleans', 'chicago']
const cityMap: { [key: string]: string } = {}
cityMap['big apple'] = 'new york'
cityMap['windy city'] = 'chicago'

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

@ -6,7 +6,7 @@ import * as path from 'path'
import * as express from 'express'
import * as BB from 'botbuilder'
import { BotFrameworkAdapter, AutoSaveStateMiddleware } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, SessionEndState, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, SessionEndState, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -47,12 +47,12 @@ let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
let cl = clFactory.create(modelId)
//==================================
// Add Start / End Session callbacks

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

@ -6,7 +6,7 @@ import * as path from 'path'
import * as express from 'express'
import * as BB from 'botbuilder'
import { BotFrameworkAdapter } from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -47,12 +47,12 @@ let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
//==================================
// Initialize Conversation Learner
//==================================
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
if (isDevelopment) {
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
}
let cl = new ConversationLearner(modelId)
const cl = clFactory.create(modelId)
//==================================
// Add Start / End Session callbacks

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

@ -5,7 +5,7 @@
import * as path from 'path'
import * as express from 'express'
import * as botBuilder from 'botbuilder'
import { ConversationLearner, ClientMemoryManager, FileStorage, ReadOnlyClientMemoryManager, uiRouter } from '@conversationlearner/sdk'
import { ConversationLearnerFactory, ClientMemoryManager, FileStorage, ReadOnlyClientMemoryManager, uiRouter } from '@conversationlearner/sdk'
import chalk from 'chalk'
import config from '../config'
import getDolRouter from '../dol'
@ -14,15 +14,15 @@ const server = express()
const { bfAppId, bfAppPassword, modelId, ...clOptions } = config
const adapter = new botBuilder.BotFrameworkAdapter({ appId: bfAppId, appPassword: bfAppPassword })
let fileStorage = new FileStorage(path.join(__dirname, 'storage'))
const sdkRouter = ConversationLearner.Init(clOptions, fileStorage)
const fileStorage = new FileStorage(path.join(__dirname, 'storage'))
const clFactory = new ConversationLearnerFactory(clOptions, fileStorage)
const isDevelopment = process.env.NODE_ENV === 'development'
if (isDevelopment) {
console.log(chalk.yellowBright(`Adding /directline routes`))
server.use(getDolRouter(config.botPort))
console.log(chalk.cyanBright(`Adding /sdk routes`))
server.use('/sdk', sdkRouter)
server.use('/sdk', clFactory.sdkRouter)
console.log(chalk.greenBright(`Adding /ui routes`))
server.use(uiRouter as any)
@ -35,14 +35,13 @@ server.listen(config.botPort, () => {
console.log(`Server listening at: http://localhost:${config.botPort}`)
})
const cl = new ConversationLearner(modelId)
const cl = clFactory.create(modelId)
//=========================================================
// Bots Business Logic
//=========================================================
var inStock = ["cheese", "sausage", "mushrooms", "olives", "peppers"]
var isInStock = function (topping: string) {
const inStock = ["cheese", "sausage", "mushrooms", "olives", "peppers"]
const isInStock = function (topping: string) {
return (inStock.indexOf(topping.toLowerCase()) > -1)
}
@ -55,7 +54,7 @@ var isInStock = function (topping: string) {
* @param {ClientMemoryManager} memoryManager Allows for viewing and manipulating Bot's memory
* @returns {Promise<void>}
*/
cl.EntityDetectionCallback = async (text: string, memoryManager: ClientMemoryManager): Promise<void> => {
cl.EntityDetectionCallback = async (text, memoryManager) => {
let entityError = memoryManager.Get("entityError", ClientMemoryManager.AS_STRING)
console.log(chalk.redBright(`entityError: ${entityError}`))
if (entityError === "entityError") {

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

@ -7,7 +7,6 @@ export const DEFAULT_MAX_SESSION_LENGTH = 20 * 60 * 1000 // 20 minutes
// Model Settings
export interface CLModelOptions {
// How long before a session automatically times out
sessionTimout: number
sessionTimeout: number
}

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

@ -2,14 +2,8 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface CLOptions {
import { ICLClientOptions } from './CLClient'
export interface CLOptions extends ICLClientOptions {
botPort: any
LUIS_AUTHORING_KEY: string | undefined
LUIS_SUBSCRIPTION_KEY: string | undefined
APIM_SUBSCRIPTION_KEY: string | undefined
// URL for Conversation Learner service
CONVERSATION_LEARNER_SERVICE_URI: string
CONVERSATION_LEARNER_UI_PORT: number
}

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

@ -8,6 +8,7 @@ import * as CLM from '@conversationlearner/models'
import * as Utils from './Utils'
import * as ModelOptions from './CLModelOptions'
import { CLState } from './Memory/CLState'
import { CLStateFactory } from './Memory/CLStateFactory'
import { CLDebug, DebugType } from './CLDebug'
import { CLClient } from './CLClient'
import { CLStrings } from './CLStrings'
@ -18,6 +19,8 @@ import { ConversationLearner } from './ConversationLearner'
import { InputQueue } from './Memory/InputQueue'
import { UIMode } from './Memory/BotState'
import { EntityState } from './Memory/EntityState'
import { ILogStorage } from './Memory/ILogStorage'
import { CLOptions } from './CLOptions'
interface RunnerLookup {
[appId: string]: CLRunner
@ -115,29 +118,36 @@ export class CLRunner {
private static Runners: RunnerLookup = {}
private static UIRunner: CLRunner
public clClient: CLClient
public adapter: BB.BotAdapter | undefined
private stateFactory: CLStateFactory
private options: CLOptions
private modelOptions: ModelOptions.CLModelOptions
private logStorage: ILogStorage | undefined
// Used to detect changes in API callbacks / Templates when bot reloaded and UI running
private checksum: string | null = null
public clClient: CLClient
public adapter: BB.BotAdapter | undefined
/* Model Id passed in from configuration. Used when not running in Conversation Learner UI */
public readonly configModelId: string | undefined
private modelOptions: ModelOptions.CLModelOptions
public readonly modelId: string | undefined
/* Mapping between user defined API names and functions */
public callbacks: CallbackMap = {}
public static Create(configModelId: string | undefined, client: CLClient, newOptions?: Partial<ModelOptions.CLModelOptions>): CLRunner {
public static Create(
stateFactory: CLStateFactory,
client: CLClient,
option: CLOptions,
modelId: string | undefined,
logStorage: ILogStorage | undefined,
newOptions?: Partial<ModelOptions.CLModelOptions>,
): CLRunner {
const modelOptions = this.validiatedModelOptions(newOptions)
// Ok to not provide modelId when just running in training UI.
// If not, Use UI_RUNNER_APPID const as lookup value
let newRunner = new CLRunner(configModelId, client, modelOptions)
CLRunner.Runners[configModelId ?? Utils.UI_RUNNER_APPID] = newRunner
const newRunner = new CLRunner(stateFactory, client, option, modelId, modelOptions, logStorage)
CLRunner.Runners[modelId ?? Utils.UI_RUNNER_APPID] = newRunner
// Bot can define multiple CLs. Always run UI on first CL defined in the bot
// Bot can define multiple CLs. Always run UI on first CL defined in the bot
if (!CLRunner.UIRunner) {
CLRunner.UIRunner = newRunner
}
@ -146,14 +156,12 @@ export class CLRunner {
}
private static validiatedModelOptions(modelOptions?: Partial<ModelOptions.CLModelOptions>): ModelOptions.CLModelOptions {
const sessionTimout = (!modelOptions
|| !modelOptions.sessionTimout
|| typeof modelOptions.sessionTimout !== 'number')
const sessionTimeout = (typeof modelOptions?.sessionTimeout !== 'number')
? ModelOptions.DEFAULT_MAX_SESSION_LENGTH
: modelOptions.sessionTimout
: modelOptions.sessionTimeout
return {
sessionTimout
sessionTimeout
}
}
@ -171,10 +179,20 @@ export class CLRunner {
return CLRunner.Runners[appId]
}
private constructor(configModelId: string | undefined, client: CLClient, modelOptions: ModelOptions.CLModelOptions) {
this.configModelId = configModelId
this.modelOptions = modelOptions
private constructor(
stateFactory: CLStateFactory,
client: CLClient,
options: CLOptions,
modelId: string | undefined,
modelOptions: ModelOptions.CLModelOptions,
logStorage: ILogStorage | undefined,
) {
this.stateFactory = stateFactory
this.clClient = client
this.options = options
this.modelId = modelId
this.modelOptions = modelOptions
this.logStorage = logStorage
}
public botChecksum(): string {
@ -205,10 +223,10 @@ export class CLRunner {
public async InTrainingUI(turnContext: BB.TurnContext): Promise<boolean> {
if (turnContext.activity.from?.name === Utils.CL_DEVELOPER) {
const state = CLState.GetFromContext(turnContext, this.configModelId)
const state = this.stateFactory.getFromContext(turnContext, this.modelId)
const app = await state.BotState.GetApp()
// If no app selected in UI or no app set in config, or they don't match return true
if (!app || !this.configModelId || app.appId !== this.configModelId) {
if (!app || !this.modelId || app.appId !== this.modelId) {
return true
}
}
@ -228,7 +246,7 @@ export class CLRunner {
}
try {
const state = CLState.GetFromContext(turnContext, this.configModelId)
const state = this.stateFactory.getFromContext(turnContext, this.modelId)
const app = await this.GetRunningApp(state, false)
if (app) {
@ -271,7 +289,7 @@ export class CLRunner {
return null
}
const state = CLState.GetFromContext(turnContext, this.configModelId)
const state = this.stateFactory.getFromContext(turnContext, this.modelId)
// If I'm in teach or edit mode, or testing process message right away
let uiMode = await state.BotState.getUIMode()
@ -342,14 +360,14 @@ export class CLRunner {
const saveToLog = createParams.saveToLog
// Don't save logs on server if custom storage was provided
if (ConversationLearner.logStorage) {
if (this.logStorage) {
createParams.saveToLog = false
}
const session = await this.clClient.StartSession(appId, createParams as CLM.SessionCreateParams)
// If using customer storage add to log storage
if (ConversationLearner.logStorage && saveToLog) {
if (this.logStorage && saveToLog) {
// For self-hosted log storage logDialogId is sessionId
session.logDialogId = session.sessionId
const logDialog: CLM.LogDialog = {
@ -365,7 +383,7 @@ export class CLRunner {
lastModifiedDateTime: new Date().toJSON(),
metrics: ""
}
await ConversationLearner.logStorage.Add(appId, logDialog)
await this.logStorage.Add(appId, logDialog)
}
return session
}
@ -375,13 +393,13 @@ export class CLRunner {
const extractResponse = await this.clClient.SessionExtract(appId, sessionId, userInput)
const stepEndDatetime = new Date().toJSON()
// Add to dev's self-hosted log storage account (if it exists)
if (ConversationLearner.logStorage) {
// For self-holsted logDialogId = sessionId
// If dev provided log storage, save round in storage
if (this.logStorage) {
// For provided storage logDialogId = sessionId
const logDialogId = sessionId
// Append an extractor step to already existing log dialog
const logDialog: CLM.LogDialog | undefined = await ConversationLearner.logStorage.Get(appId, logDialogId)
const logDialog: CLM.LogDialog | undefined = await this.logStorage.Get(appId, logDialogId)
if (!logDialog) {
throw new Error(`Log Dialog does not exist App:${appId} Id:${logDialogId}`)
}
@ -391,8 +409,9 @@ export class CLRunner {
}
logDialog.rounds.push(newRound)
logDialog.lastModifiedDateTime = new Date().toJSON()
await ConversationLearner.logStorage.Replace(appId, logDialog)
await this.logStorage.Replace(appId, logDialog)
}
return extractResponse
}
@ -400,9 +419,9 @@ export class CLRunner {
const stepBeginDatetime = new Date().toJSON()
const scoreResponse = await this.clClient.SessionScore(appId, sessionId, scoreInput)
// Add to dev's log storage account (if it exists)
if (ConversationLearner.logStorage) {
// For self-hosted storage logDialogId is sessionId
// If log storage was provided save score there
if (this.logStorage) {
// For provided storage logDialogId is sessionId
const logDialogId = sessionId
const predictedAction = scoreResponse.scoredActions[0]?.actionId ?? ""
@ -419,6 +438,7 @@ export class CLRunner {
actionId: sa.actionId
}
})
// Need to use recursive partial as scored and unscored have only partial data
const logScorerStep: Utils.RecursivePartial<CLM.LogScorerStep> = {
input: scoreInput,
@ -427,10 +447,10 @@ export class CLRunner {
predictionDetails: { scoredActions, unscoredActions },
stepBeginDatetime,
stepEndDatetime: new Date().toJSON(),
metrics: scoreResponse.metrics
metrics: scoreResponse.metrics,
}
const logDialog: CLM.LogDialog | undefined = await ConversationLearner.logStorage.Get(appId, logDialogId)
const logDialog: CLM.LogDialog | undefined = await this.logStorage.Get(appId, logDialogId)
if (!logDialog) {
throw new Error(`Log Dialog does not exist App:${appId} Log:${logDialogId}`)
}
@ -440,7 +460,7 @@ export class CLRunner {
}
lastRound.scorerSteps.push(logScorerStep as any)
logDialog.lastModifiedDateTime = new Date().toJSON()
await ConversationLearner.logStorage.Replace(appId, logDialog)
await this.logStorage.Replace(appId, logDialog)
}
return scoreResponse
}
@ -450,15 +470,15 @@ export class CLRunner {
let app = await state.BotState.GetApp()
// If this instance is configured to use a specific model, check conditions to use that model.
if (this.configModelId
if (this.modelId
// If current app is not set
&& (!app
// If I'm not in the editing UI and config model id differs than the current app
|| (!inEditingUI && this.configModelId != app.appId))
|| (!inEditingUI && this.modelId != app.appId))
) {
// Get app specified by options
CLDebug.Log(`Switching to app specified in config: ${this.configModelId}`)
app = await this.clClient.GetApp(this.configModelId)
CLDebug.Log(`Switching to app specified in config: ${this.modelId}`)
app = await this.clClient.GetApp(this.modelId)
await state.SetAppAsync(app)
}
@ -495,19 +515,19 @@ export class CLRunner {
const inEditingUI = conversationReference.user?.name === Utils.CL_DEVELOPER
// Validate setup
if (!inEditingUI && !this.configModelId) {
if (!inEditingUI && !this.modelId) {
const msg = 'Must specify modelId in ConversationLearner constructor when not running bot in Editing UI\n\n'
CLDebug.Error(msg)
return null
}
if (!ConversationLearner.options?.LUIS_AUTHORING_KEY) {
if (!this.options?.LUIS_AUTHORING_KEY) {
const msg = 'Options must specify luisAuthoringKey. Set the LUIS_AUTHORING_KEY.\n\n'
CLDebug.Error(msg)
return null
}
const state = CLState.GetFromContext(turnContext, this.configModelId)
const state = this.stateFactory.getFromContext(turnContext, this.modelId)
let app = await this.GetRunningApp(state, inEditingUI)
const uiMode = await state.BotState.getUIMode()
@ -529,7 +549,7 @@ export class CLRunner {
const currentTicks = new Date().getTime()
let lastActive = await state.BotState.GetLastActive()
let passedTicks = currentTicks - lastActive
if (passedTicks > this.modelOptions.sessionTimout!) {
if (passedTicks > this.modelOptions.sessionTimeout) {
// Parameters for new session
const sessionCreateParams: CLM.SessionCreateParams = {
@ -551,13 +571,13 @@ export class CLRunner {
// If I'm not in the UI, reload the App to get any changes (live package version may have been updated)
if (!inEditingUI) {
if (!this.configModelId) {
if (!this.modelId) {
let error = "ERROR: ModelId not specified. When running in a channel (i.e. Skype) or the Bot Framework Emulator, CONVERSATION_LEARNER_MODEL_ID must be specified in your Bot's .env file or Application Settings on the server"
await this.SendMessage(state, error, activity)
return null
}
app = await this.clClient.GetApp(this.configModelId)
app = await this.clClient.GetApp(this.modelId)
await state.SetAppAsync(app)
if (!app) {
@ -644,7 +664,7 @@ export class CLRunner {
} catch (error) {
// Try to end the session, so use can potentially recover
try {
const state = CLState.GetFromContext(turnContext, this.configModelId)
const state = this.stateFactory.getFromContext(turnContext, this.modelId)
await this.EndSessionAsync(state, CLM.SessionEndState.OPEN)
} catch {
CLDebug.Log(`Failed to End Session`)
@ -1194,19 +1214,24 @@ export class CLRunner {
}
private async forwardInputToModel(modelId: string, state: CLState, changeActiveModel: boolean = false) {
if (modelId === this.configModelId) {
if (modelId === this.modelId) {
throw new Error(`Cannot forward input to model with same ID as active model. This shouldn't be possible open an issue.`)
}
// Reuse model instance from cache or create it
let model = ConversationLearner.models.find(m => m.clRunner.configModelId === modelId)
let model = ConversationLearner.models.find(m => m.clRunner.modelId === modelId)
if (!model) {
model = new ConversationLearner(modelId)
model = new ConversationLearner(
this.stateFactory,
this.clClient,
this.options,
modelId
)
}
// Save the model id for the conversation so all future input is directed to it.
if (changeActiveModel) {
state.ConversationModelState.set(model.clRunner.configModelId)
state.ConversationModelState.set(model.clRunner.modelId)
}
const turnContext = state.turnContext

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

@ -3,51 +3,32 @@
* Licensed under the MIT License.
*/
import * as BB from 'botbuilder'
import * as express from 'express'
import getRouter from './http/router'
import { CLRunner, EntityDetectionCallback, OnSessionStartCallback, OnSessionEndCallback, ICallbackInput } from './CLRunner'
import { CLOptions } from './CLOptions'
import { CLState } from './Memory/CLState'
import { CLDebug } from './CLDebug'
import { CLClient } from './CLClient'
import { CLRecognizerResult } from './CLRecognizeResult'
import { CLModelOptions } from '.'
import CLStateFactory from './Memory/CLStateFactory'
import { CLOptions } from './CLOptions'
import { CLModelOptions } from './CLModelOptions'
import { ILogStorage } from './Memory/ILogStorage'
/**
* Main CL class used by Bot
*/
export class ConversationLearner {
public static options: CLOptions | null = null
public static clClient: CLClient
public static logStorage: ILogStorage
public static models: ConversationLearner[] = []
public clRunner: CLRunner
private stateFactory: CLStateFactory
public static Init(options: CLOptions, stateStorage?: BB.Storage, logStorage?: ILogStorage): express.Router {
ConversationLearner.options = options
try {
this.clClient = new CLClient(options)
CLState.Init(stateStorage)
// Is developer providing their own log storage
if (logStorage) {
this.logStorage = logStorage
}
} catch (error) {
CLDebug.Error(error, 'Conversation Learner Initialization')
}
return getRouter(this.clClient, options)
}
constructor(modelId: string | undefined, modelOptions?: Partial<CLModelOptions>) {
if (!ConversationLearner.options) {
throw new Error("Init() must be called on ConversationLearner before instances are created")
}
this.clRunner = CLRunner.Create(modelId, ConversationLearner.clClient, modelOptions)
constructor(
stateFactory: CLStateFactory,
client: CLClient,
options: CLOptions,
modelId: string | undefined,
modelOptions?: CLModelOptions,
logStorage?: ILogStorage,
) {
this.stateFactory = stateFactory
this.clRunner = CLRunner.Create(stateFactory, client, options, modelId, logStorage, modelOptions)
ConversationLearner.models.push(this)
}
@ -57,9 +38,9 @@ export class ConversationLearner {
// If there is more than one model in use for running bot we need to check which model is active for conversation
// This check avoids doing work for normal singe model model bots
if (ConversationLearner.models.length > 1) {
const context = CLState.GetFromContext(turnContext)
const context = this.stateFactory.getFromContext(turnContext)
const activeModelIdForConversation = await context.ConversationModelState.get<string>()
const model = ConversationLearner.models.find(m => m.clRunner.configModelId === activeModelIdForConversation)
const model = ConversationLearner.models.find(m => m.clRunner.modelId === activeModelIdForConversation)
if (model) {
activeModel = model
}

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

@ -0,0 +1,50 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as BB from 'botbuilder'
import * as express from 'express'
import { ConversationLearner } from './ConversationLearner'
import { CLOptions } from './CLOptions'
import { CLClient } from './CLClient'
import getRouter from './http/router'
import CLStateFactory from './Memory/CLStateFactory'
import { ILogStorage } from './Memory/ILogStorage'
import { CLModelOptions } from './CLModelOptions'
/**
* Conversation Learner Factory. Produces instances that all use the same storage, client, and options.
* Alternative which ConversationLearner.Init() which set the statics but this created temporal coupling (need to call Init before constructor)
*/
export default class ConversationLearnerFactory {
private storageFactory: CLStateFactory
private client: CLClient
private logStorage: ILogStorage | undefined
private options: CLOptions
sdkRouter: express.Router
constructor(
options: CLOptions,
bbStorage: BB.Storage = new BB.MemoryStorage(),
logStorage?: ILogStorage,
) {
this.storageFactory = new CLStateFactory(bbStorage)
this.logStorage = logStorage
this.options = options
this.client = new CLClient(options)
this.sdkRouter = getRouter(this.client, this.storageFactory, options, logStorage)
}
create(modelId?: string, modelOptions?: CLModelOptions) {
return new ConversationLearner(
this.storageFactory,
this.client,
this.options,
modelId,
modelOptions,
this.logStorage
)
}
}

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

@ -3,7 +3,6 @@
* Licensed under the MIT License.
*/
import * as BB from 'botbuilder'
import { ConversationLearner } from '../ConversationLearner'
import { CLStorage } from './CLStorage'
import { AppBase } from '@conversationlearner/models'
import { SessionStartFlags } from '../CLRunner'
@ -72,7 +71,11 @@ export class BotState {
private readonly getKey: GetKey
private readonly conversationReferenceToConversationIdMapper: ConvIdMapper
constructor(storage: CLStorage, getKey: GetKey, conversationReferenceToConvIdMapper: ConvIdMapper = BotState.DefaultConversationIdMapper) {
constructor(
storage: CLStorage,
getKey: GetKey,
conversationReferenceToConvIdMapper: ConvIdMapper = BotState.DefaultConversationIdMapper
) {
this.storage = storage
this.getKey = getKey
this.conversationReferenceToConversationIdMapper = conversationReferenceToConvIdMapper
@ -276,8 +279,9 @@ export class BotState {
conversation: { id: conversationId },
channelId: 'emulator',
// TODO: Refactor away from static coupling. BotState needs to have access to options object through constructor
// Doesn't seemed to be used, so hard-code to default port for now.
// tslint:disable-next-line:no-http-string
serviceUrl: `http://127.0.0.1:${ConversationLearner.options!.botPort}`
serviceUrl: `http://127.0.0.1:${3978}`
} as Partial<BB.ConversationReference>
this.SetConversationReference(conversationReference)
}

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

@ -4,12 +4,9 @@
*/
import * as BB from 'botbuilder'
import * as CLM from '@conversationlearner/models'
import * as Utils from '../Utils'
import { CLDebug } from '../CLDebug'
import { EntityState } from './EntityState'
import { BotState } from './BotState'
import { InProcessMessageState as MessageState } from './InProcessMessageState'
import { CLStorage } from './CLStorage'
import { BrowserSlotState } from './BrowserSlot'
/**
@ -25,7 +22,6 @@ import { BrowserSlotState } from './BrowserSlot'
* - Example: SetApp (which affects BotState, EntityState, and MessageState)
*/
export class CLState {
private static bbStorage: BB.Storage
public readonly turnContext?: BB.TurnContext
BotState: BotState
@ -34,14 +30,14 @@ export class CLState {
ConversationModelState: MessageState
BrowserSlotState: BrowserSlotState
private constructor(
constructor(
botState: BotState,
entityState: EntityState,
messageState: MessageState,
conversationModelState: MessageState,
browserSlotState: BrowserSlotState,
turnContext?: BB.TurnContext,
) {
turnContext?: BB.TurnContext) {
this.BotState = botState
this.EntityState = entityState
this.MessageState = messageState
@ -51,69 +47,11 @@ export class CLState {
this.turnContext = turnContext
}
public static Init(storage?: BB.Storage): void {
// If memory storage not defined use disk storage
if (!storage) {
CLDebug.Log('Storage not defined. Defaulting to in-memory storage.')
storage = new BB.MemoryStorage()
}
CLState.bbStorage = storage
}
public static Get(key: string, modelId: string = '', turnContext?: BB.TurnContext): CLState {
const storage = new CLStorage(CLState.bbStorage)
// Used for state shared through lifetime of conversation (conversationId)
const keyPrefix = Utils.getSha256Hash(key)
// Used for state shared between models within a conversation (Dispatcher has multiple models per conversation)
const modelUniqueKeyPrefix = Utils.getSha256Hash(`${modelId}${key}`)
const botState = new BotState(storage, (datakey) => `${modelUniqueKeyPrefix}_BOTSTATE_${datakey}`)
const entityState = new EntityState(storage, () => `${modelUniqueKeyPrefix}_ENTITYSTATE`)
const messageState = new MessageState(storage, () => `${keyPrefix}_MESSAGE_MUTEX`)
const conversationModelState = new MessageState(storage, () => `${keyPrefix}_CONVERSATION_MODEL`)
const browserSlotState = new BrowserSlotState(storage, () => `BROWSER_SLOTS`)
return new CLState(
botState,
entityState,
messageState,
conversationModelState,
browserSlotState,
turnContext,
)
}
public static GetFromContext(turnContext: BB.TurnContext, modelId: string = ''): CLState {
const conversationReference = BB.TurnContext.getConversationReference(turnContext.activity)
const user = conversationReference.user
let keyPrefix: string
const isRunningInUI = Utils.isRunningInClUI(turnContext)
if (isRunningInUI) {
if (!user) {
throw new Error(`Attempted to initialize state, but cannot get state key because current request did not have 'from'/user specified`)
}
if (!user.id) {
throw new Error(`Attempted to initialize state, but user.id was not provided which is required for use as state key.`)
}
// User ID is the browser slot assigned to the UI
keyPrefix = user.id
} else {
// CLState uses conversation Id as the prefix key for all the objects kept in CLStorage when bot is not running against CL UI
if (!conversationReference.conversation || !conversationReference.conversation.id) {
throw new Error(`Attempted to initialize state, but conversationReference.conversation.id was not provided which is required for use as state key.`)
}
keyPrefix = conversationReference.conversation.id
}
return CLState.Get(keyPrefix, !isRunningInUI ? modelId : undefined, turnContext)
}
public async SetAppAsync(app: CLM.AppBase | null): Promise<void> {
const curApp = await this.BotState.GetApp()
await this.BotState.SetAppAsync(app)
await this.MessageState.remove()
await this.ConversationModelState.remove()
if (!app || !curApp || curApp.appId !== app.appId) {
await this.EntityState.ClearAsync()

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

@ -0,0 +1,76 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as BB from 'botbuilder'
import { CLStorage } from './CLStorage'
import { CLState } from './CLState'
import * as Utils from '../Utils'
import { BotState } from './BotState'
import { EntityState } from './EntityState'
import { InProcessMessageState as MessageState } from './InProcessMessageState'
import { BrowserSlotState } from './BrowserSlot'
/**
* Conversation Learner State Factory.
*
* Produces instances that all use the same BotBuilder storage provider.
*/
export class CLStateFactory {
private bbStorage: BB.Storage
constructor(bbStorage: BB.Storage = new BB.MemoryStorage()) {
this.bbStorage = bbStorage
}
get(key: string, modelId: string = '', turnContext?: BB.TurnContext): CLState {
const storage = new CLStorage(this.bbStorage)
// Used for state shared through lifetime of conversation (conversationId)
const keyPrefix = Utils.getSha256Hash(key)
// Used for state shared between models within a conversation (Dispatcher has multiple models per conversation)
const modelUniqueKeyPrefix = Utils.getSha256Hash(`${modelId}${key}`)
const botState = new BotState(storage, (datakey) => `${modelUniqueKeyPrefix}_BOTSTATE_${datakey}`)
const entityState = new EntityState(storage, () => `${modelUniqueKeyPrefix}_ENTITYSTATE`)
const messageState = new MessageState(storage, () => `${keyPrefix}_MESSAGE_MUTEX`)
const conversationModelState = new MessageState(storage, () => `${keyPrefix}_CONVERSATION_MODEL`)
const browserSlotState = new BrowserSlotState(storage, () => `BROWSER_SLOTS`)
return new CLState(
botState,
entityState,
messageState,
conversationModelState,
browserSlotState,
turnContext,
)
}
getFromContext(turnContext: BB.TurnContext, modelId: string = ''): CLState {
const conversationReference = BB.TurnContext.getConversationReference(turnContext.activity)
const user = conversationReference.user
let keyPrefix: string
if (Utils.isRunningInClUI(turnContext)) {
if (!user) {
throw new Error(`Attempted to initialize state, but cannot get state key because current request did not have 'from'/user specified`)
}
if (!user.id) {
throw new Error(`Attempted to initialize state, but user.id was not provided which is required for use as state key.`)
}
// User ID is the browser slot assigned to the UI
keyPrefix = user.id
} else {
// CLState uses conversation Id as the prefix key for all the objects kept in CLStorage when bot is not running against CL UI
if (!conversationReference.conversation || !conversationReference.conversation.id) {
throw new Error(`Attempted to initialize state, but conversationReference.conversation.id was not provided which is required for use as state key.`)
}
keyPrefix = conversationReference.conversation.id
}
return this.get(keyPrefix, modelId, turnContext)
}
}
export default CLStateFactory

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

@ -5,7 +5,7 @@
import { CLStorage } from './CLStorage'
import { CLDebug } from '../CLDebug'
import { Memory, FilledEntity, MemoryValue, FilledEntityMap } from '@conversationlearner/models'
import { ClientMemoryManager } from '..'
import { ClientMemoryManager } from './ClientMemoryManager'
const NEGATIVE_PREFIX = '~'

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

@ -10,7 +10,7 @@ export interface LogQueryResult {
}
/**
* Interface for generating custom LogStorage implimentations
* Interface for generating custom LogStorage implementations
*/
export declare class ILogStorage {
@ -29,7 +29,7 @@ export declare class ILogStorage {
*/
GetMany(appId: string, packageIds: string[], continuationToken?: string, pageSize?: number): Promise<LogQueryResult>
/** Replace and exisiting log dialog */
/** Replace and existing log dialog */
Replace(appId: string, logDialog: CLM.LogDialog): Promise<void>
/** Delete a log dialog in storage */

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

@ -7,6 +7,7 @@ import * as BB from 'botbuilder'
import * as CLM from '@conversationlearner/models'
import { CLState } from './CLState'
import { InputQueue } from '../Memory/InputQueue'
import CLStateFactory from './CLStateFactory'
// InputQueue requires tiny delay between inputs or InputQueue mutex won't fishing setting
const mutexDelay = 50
@ -35,8 +36,8 @@ function delay(ms: number) {
// Simulate AddInput function in CLRunner
async function addInput(state: CLState, message: string, conversationId: string) {
const activity = makeActivity(message, conversationId)
let addInputPromise = util.promisify(InputQueue.AddInput)
let isReady = await addInputPromise(state.MessageState, activity)
const addInputPromise = util.promisify(InputQueue.AddInput)
const isReady = await addInputPromise(state.MessageState, activity)
if (isReady) {
// Handle input with a delay to simulate CLRunner ProcessInput
@ -52,16 +53,16 @@ async function addInput(state: CLState, message: string, conversationId: string)
}
describe('InputQueue', () => {
const stateFactory = new CLStateFactory()
beforeEach(() => {
CLState.Init()
responses = {}
})
it('should handle all messages in a single queue', async () => {
const conversationId = CLM.ModelUtils.generateGUID()
const clState = CLState.Get(conversationId)
const clState = stateFactory.get(conversationId)
const inputs = ["A", "B", "C", "D", "E"]
// Need longer jest timeout for this test
@ -87,8 +88,8 @@ describe('InputQueue', () => {
const conversation1Id = CLM.ModelUtils.generateGUID()
const conversation2Id = CLM.ModelUtils.generateGUID()
const clState1 = CLState.Get(conversation1Id)
const clState2 = CLState.Get(conversation2Id)
const clState1 = stateFactory.get(conversation1Id)
const clState2 = stateFactory.get(conversation2Id)
const inputs1 = ["A", "B", "C", "D", "E"]
const inputs2 = ["1", "2", "3", "4", "5"]

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

@ -6,6 +6,7 @@ import * as supertest from 'supertest'
import * as express from 'express'
import router from './router'
import { ICLClientOptions, CLClient } from '../CLClient'
import CLStateFactory from '../Memory/CLStateFactory'
describe('Test SDK router', () => {
const options: ICLClientOptions = {
@ -13,8 +14,10 @@ describe('Test SDK router', () => {
APIM_SUBSCRIPTION_KEY: undefined,
LUIS_AUTHORING_KEY: undefined
}
const clClient = new CLClient(options)
const sdkRouter = router(clClient, options)
const client = new CLClient(options)
const stateFactory = new CLStateFactory()
const sdkRouter = router(client, stateFactory, options, undefined)
const app = express()
app.use(sdkRouter)

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

@ -19,12 +19,12 @@ import getAppDefinitionChange from '../upgrade'
import { CLDebug } from '../CLDebug'
import { CLClient, ICLClientOptions } from '../CLClient'
import { CLRunner, SessionStartFlags } from '../CLRunner'
import { ConversationLearner } from '../ConversationLearner'
import { CLState } from '../Memory/CLState'
import { CLStateFactory } from '../Memory/CLStateFactory'
import { CLRecognizerResult } from '../CLRecognizeResult'
import { TemplateProvider } from '../TemplateProvider'
import { CLStrings } from '../CLStrings'
import { UIMode } from '../Memory/BotState'
import { ILogStorage } from '../Memory/ILogStorage'
// Extract error text from HTML error
export const HTML2Error = (htmlText: string): string => {
@ -122,7 +122,20 @@ const getBanner = (source: string): Promise<CLM.Banner | null> => {
})
}
export const getRouter = (client: CLClient, options: ICLClientOptions): express.Router => {
/**
* Create Express.Router using given options (LUIS_AUTHORING_KEY, etc)
*
* Requests that need to manipulate the Bot such as operations from UI on train dialogs must be intercepted, deconstructed, then forwarded using the ClClient
* Requests that do NOT need to manipulate the Bot can flow through uninterrupted and go through HttpProxy middleware.
*
* @param options Options for Conversation Learner client
*/
export const getRouter = (
client: CLClient,
stateFactory: CLStateFactory,
options: ICLClientOptions,
logStorage: ILogStorage | undefined,
): express.Router => {
const router = express.Router({ caseSensitive: false })
router.use(cors())
router.use(bodyParser.json({
@ -144,7 +157,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const key = getMemoryKey(req)
const app: CLM.AppBase = req.body
const state = CLState.Get(key)
const state = stateFactory.get(key)
await state.SetAppAsync(app)
res.sendStatus(200)
} catch (error) {
@ -157,7 +170,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
try {
const key = getMemoryKey(req)
const { conversationId, userName } = getQuery(req)
const state = CLState.Get(key)
const state = stateFactory.get(key)
await state.BotState.CreateConversationReference(userName, key, conversationId)
res.sendStatus(200)
} catch (error) {
@ -177,10 +190,10 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
// TODO: This is code smell, this is using internal knowledge that BrowserState full key is static and will be the same regardless of key
// It makes BrowserState accessed in consistent way other states. Would be more natural as static object but need to share underlying storage.
const state = CLState.Get('')
const state = stateFactory.get('')
// Generate id
const browserSlotId = await state.BrowserSlotState.get(browserId)
const key = ConversationLearner.options!.LUIS_AUTHORING_KEY!
const key = options.LUIS_AUTHORING_KEY
const hashedKey = key ? crypto.createHash('sha256').update(key).digest('hex') : ""
const id = `${browserSlotId}-${hashedKey}`
@ -235,7 +248,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
res.send(app)
// Initialize memory
CLState.Get(key).SetAppAsync(app)
stateFactory.get(key).SetAppAsync(app)
} catch (error) {
HandleError(res, error)
}
@ -250,7 +263,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
await client.ArchiveApp(appId)
// Did I delete my loaded app, if so clear my state
const state = CLState.Get(key)
const state = stateFactory.get(key)
const app = await state.BotState.GetApp()
if (app?.appId === appId) {
await state.SetAppAsync(null)
@ -272,7 +285,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const apps = await client.GetApps(query)
// Get lookup table for which apps packages are being edited
const state = CLState.Get(key)
const state = stateFactory.get(key)
const activeApps = await state.BotState.GetEditingPackages()
const uiAppList = { appList: apps, activeApps: activeApps } as CLM.UIAppList
@ -375,7 +388,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
}
}
const state = CLState.Get(key)
const state = stateFactory.get(key)
const updatedPackageVersions = await state.BotState.SetEditingPackage(appId, packageId)
res.send(updatedPackageVersions)
@ -493,7 +506,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const { appId, logDialogId, turnIndex } = req.params
const userInput: CLM.UserInput = req.body
const extractResponse = await client.LogDialogExtract(appId, logDialogId, turnIndex, userInput)
const state = CLState.Get(key)
const state = stateFactory.get(key)
const memories = await state.EntityState.DumpMemory()
const uiExtractResponse: CLM.UIExtractResponse = { extractResponse, memories }
@ -508,8 +521,8 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
try {
let logDialog
if (ConversationLearner.logStorage) {
logDialog = await ConversationLearner.logStorage.Get(appId, logDialogId)
if (logStorage) {
logDialog = await logStorage.Get(appId, logDialogId)
}
else {
logDialog = await client.GetLogDialog(appId, logDialogId)
@ -529,13 +542,15 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
if (typeof packageIds === "string") {
packageIds = [packageIds]
}
let logQueryResult: CLM.LogQueryResult
if (ConversationLearner.logStorage) {
logQueryResult = await ConversationLearner.logStorage.GetMany(appId, packageIds, continuationToken, maxPageSize)
if (logStorage) {
logQueryResult = await logStorage.GetMany(appId, packageIds, continuationToken, maxPageSize)
}
else {
logQueryResult = await client.GetLogDialogs(appId, packageIds, continuationToken, maxPageSize)
}
res.send(logQueryResult)
} catch (error) {
HandleError(res, error)
@ -547,8 +562,8 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const { appId, logDialogId } = req.params
try {
if (ConversationLearner.logStorage) {
await ConversationLearner.logStorage.Delete(appId, logDialogId)
if (logStorage) {
await logStorage.Delete(appId, logDialogId)
}
else {
await client.DeleteLogDialog(appId, logDialogId)
@ -567,8 +582,8 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
logDialogIds = [logDialogIds]
}
try {
if (ConversationLearner.logStorage) {
ConversationLearner.logStorage.DeleteMany(appId, logDialogIds)
if (logStorage) {
logStorage.DeleteMany(appId, logDialogIds)
}
else {
await client.DeleteLogDialogs(appId, logDialogIds)
@ -592,7 +607,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const userInput: CLM.UserInput = req.body
const extractResponse = await client.TrainDialogExtract(appId, trainDialogId, turnIndex, userInput)
const state = CLState.Get(key)
const state = stateFactory.get(key)
const memories = await state.EntityState.DumpMemory()
const uiExtractResponse: CLM.UIExtractResponse = { extractResponse, memories }
res.send(uiExtractResponse)
@ -613,7 +628,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
trainDialog.rounds = trainDialog.rounds.slice(0, turnIndex)
// Get activities and replay to put bot into last round
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
const teachWithActivities = await clRunner.GetActivities(trainDialog, userName, userId, state)
if (!teachWithActivities) {
@ -649,7 +664,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const clRunner = CLRunner.GetRunnerForUI(appId)
validateBot(req, clRunner.botChecksum())
const state = CLState.Get(key)
const state = stateFactory.get(key)
// Clear memory when running Log from UI
state.EntityState.ClearAsync()
@ -667,7 +682,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
router.put('/app/:appId/session', async (req, res, next) => {
try {
const key = getMemoryKey(req)
const state = CLState.Get(key)
const state = stateFactory.get(key)
const conversationId = await state.BotState.GetConversationId()
// If conversation is empty
if (!conversationId) {
@ -698,7 +713,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const { appId } = req.params
// Session may be a replacement for an expired one
const state = CLState.Get(key)
const state = stateFactory.get(key)
const sessionId = await state.BotState.GetSessionIdAsync()
// May have already been closed
@ -732,7 +747,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
validateBot(req, clRunner.botChecksum())
const state = CLState.Get(key)
const state = stateFactory.get(key)
const createTeachParams: CLM.CreateTeachParams = {
contextDialog: [],
@ -756,7 +771,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
try {
const key = getMemoryKey(req)
// Update Memory
const state = CLState.Get(key)
const state = stateFactory.get(key)
await state.EntityState.ClearAsync()
res.sendStatus(200)
@ -780,7 +795,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const userInput: CLM.UserInput = req.body.userInput
// Get activities and replay to put bot into last round
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
@ -851,7 +866,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const key = getMemoryKey(req)
const appId = req.params.appId
const trainDialog: CLM.TrainDialog = req.body
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
// Replay the TrainDialog logic (API calls and EntityDetectionCallback)
@ -902,7 +917,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const appId = req.params.appId
const trainDialog: CLM.TrainDialog = req.body.trainDialog
const userInput: CLM.UserInput = req.body.userInput
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
// Replay the TrainDialog logic (API calls and EntityDetectionCallback)
@ -937,7 +952,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const key = getMemoryKey(req)
const appId = req.params.appId
const trainDialog: CLM.TrainDialog = req.body
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
validateBot(req, clRunner.botChecksum())
@ -971,7 +986,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
}
const extractResponse = await client.TeachExtract(appId, teachId, userInput, excludeConflictCheckId)
const state = CLState.Get(key)
const state = stateFactory.get(key)
const memories = await state.EntityState.DumpMemory()
const uiExtractResponse: CLM.UIExtractResponse = { extractResponse, memories }
res.send(uiExtractResponse)
@ -994,7 +1009,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const key = getMemoryKey(req)
const { appId, teachId } = req.params
const uiScoreInput: CLM.UIScoreInput = req.body
const state = CLState.Get(key)
const state = stateFactory.get(key)
// There will be no extraction step if performing a 2nd scorer round after a non-terminal action
if (uiScoreInput.trainExtractorStep) {
@ -1094,7 +1109,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const key = getMemoryKey(req)
const { appId, teachId } = req.params
const scoreInput: CLM.ScoreInput = req.body
const state = CLState.Get(key)
const state = stateFactory.get(key)
// Get new score response re-using scoreInput from previous score request
const scoreResponse = await client.TeachScore(appId, teachId, scoreInput)
@ -1128,7 +1143,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
}
delete uiTrainScorerStep.trainScorerStep.scoredAction
const state = CLState.Get(key)
const state = stateFactory.get(key)
// Now send the trained intent
const intent = {
@ -1187,7 +1202,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const response = await client.EndTeach(appId, teachId, save)
res.send(response)
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
clRunner.EndSessionAsync(state, CLM.SessionEndState.OPEN)
} catch (error) {
@ -1207,7 +1222,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const markdown = useMarkdown === "true"
const trainDialog: CLM.TrainDialog = req.body
const state = CLState.Get(key)
const state = stateFactory.get(key)
const clRunner = CLRunner.GetRunnerForUI(appId)
validateBot(req, clRunner.botChecksum())
@ -1243,7 +1258,7 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
const app = await client.GetApp(appId)
// TestId allows us to run multiple validations in parallel
const state = CLState.Get(testId)
const state = stateFactory.get(testId)
// Need to set app as not using default key
await state.SetAppAsync(app)
@ -1334,8 +1349,8 @@ export const getRouter = (client: CLClient, options: ICLClientOptions): express.
CLDebug.Error(error.message)
// Delete log dialog
if (ConversationLearner.logStorage) {
await ConversationLearner.logStorage.Delete(appId, logDialogId)
if (logStorage) {
await logStorage.Delete(appId, logDialogId)
}
else {
await client.DeleteLogDialog(appId, logDialogId)

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

@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ConversationLearner } from './ConversationLearner'
import ConversationLearnerFactory from './ConversationLearnerFactory'
import { CLOptions } from './CLOptions'
import { CLModelOptions } from './CLModelOptions'
import { ClientMemoryManager, ReadOnlyClientMemoryManager } from './Memory/ClientMemoryManager'
@ -16,8 +16,8 @@ import { CosmosLogStorage } from './CosmosLogStorage'
export {
uiRouter,
ConversationLearner,
CLOptions as ICLOptions,
ConversationLearnerFactory,
CLOptions,
CLModelOptions,
ClientMemoryManager,
ReadOnlyClientMemoryManager,