UNPKG

90 kBPlain TextView Raw
1/**
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License.
4 */
5import * as BB from 'botbuilder'
6import { CLMemory } from './CLMemory'
7import { BotMemory } from './Memory/BotMemory'
8import { CLDebug } from './CLDebug'
9import { CLClient } from './CLClient'
10import { CLStrings } from './CLStrings'
11import { TemplateProvider } from './TemplateProvider'
12import * as CLM from '@conversationlearner/models'
13import * as Utils from './Utils'
14import { ReadOnlyClientMemoryManager, ClientMemoryManager } from './Memory/ClientMemoryManager'
15import { CLRecognizerResult } from './CLRecognizeResult'
16import { ConversationLearner } from './ConversationLearner'
17import { InputQueue } from './Memory/InputQueue'
18import * as util from 'util'
19import { UIMode } from './Memory/BotState'
20
21interface RunnerLookup {
22 [appId: string]: CLRunner
23}
24
25const delay = util.promisify(setTimeout)
26
27export enum SessionStartFlags {
28 NONE = 0,
29 /* Start a teaching session */
30 IN_TEACH = 1 << 0,
31 /* Session is an edit and continue with existing turns */
32 IS_EDIT_CONTINUE = 1 << 1
33}
34
35export interface InternalCallback<T> extends CLM.Callback, ICallback<T> {
36}
37
38/**
39 * Processes messages received from the user. Called by the dialog system.
40 */
41export type EntityDetectionCallback = (text: string, memoryManager: ClientMemoryManager) => Promise<void>
42
43/**
44 * Called at session start.
45 * Allows bot to set initial entities before conversation begins
46 */
47export type OnSessionStartCallback = (context: BB.TurnContext, memoryManager: ClientMemoryManager) => Promise<void>
48
49/**
50 * Called when Session ends.
51 * If not implemented all entity values will be cleared.
52 * If implemented, developer may return a list of entities to preserve for the next session
53 * as well as store them in the Bot State
54 */
55export type OnSessionEndCallback = (context: BB.TurnContext, memoryManager: ClientMemoryManager, sessionEndState: CLM.SessionEndState, data: string | undefined) => Promise<string[] | void>
56
57/**
58 * Called when the associated action in your bot is sent.
59 * Common use cases are to call external APIs to gather data and save into entities for usage later.
60 */
61export type LogicCallback<T> = (memoryManager: ClientMemoryManager, ...args: string[]) => Promise<T | void>
62// tslint:disable-next-line:no-empty
63export const defaultLogicCallback = async () => { }
64/**
65 * Called when the associated action in your bot is sent AND during dialog replay.
66 * Common use cases are to construct text or card messages based on current entity values.
67 */
68export type RenderCallback<T> = (logicResult: T, memoryManager: ReadOnlyClientMemoryManager, ...args: string[]) => Promise<Partial<BB.Activity> | string>
69
70export interface ICallbackInput<T> {
71 name: string
72 logic?: LogicCallback<T>
73 render?: RenderCallback<T>
74}
75
76interface ICallback<T> {
77 name: string
78 logic: LogicCallback<T>
79 render: RenderCallback<T> | undefined
80}
81
82enum ActionInputType {
83 LOGIC_ONLY = "LOGIC_ONLY",
84 RENDER_ONLY = "RENDER_ONLY",
85 LOGIC_AND_RENDER = "LOGIC_AND_RENDER"
86}
87
88interface IActionInputLogic {
89 type: ActionInputType.RENDER_ONLY
90 logicResult: CLM.LogicResult | undefined
91}
92interface IActionInputRenderOnly {
93 type: Exclude<ActionInputType, ActionInputType.RENDER_ONLY>
94}
95
96type IActionInput = IActionInputRenderOnly | IActionInputLogic
97
98export interface IActionResult {
99 logicResult: CLM.LogicResult | undefined
100 response: Partial<BB.Activity> | string | null
101 replayError?: CLM.ReplayError
102}
103
104export type CallbackMap = { [name: string]: InternalCallback<any> }
105
106export class CLRunner {
107
108 /* Lookup table for CLRunners. One CLRunner per CL Model */
109 private static Runners: RunnerLookup = {}
110 private static UIRunner: CLRunner
111
112 public clClient: CLClient
113 public adapter: BB.BotAdapter | undefined
114 // Used to detect changes in API callbacks / Templates when bot reloaded and UI running
115 private checksum: string | null = null
116
117 /* Model Id passed in from configuration. Used when not running in Conversation Learner UI */
118 private configModelId: string | undefined;
119 private maxTimeout: number | undefined; // TODO: Move timeout to app settings
120
121 /* Mapping between user defined API names and functions */
122 public callbacks: CallbackMap = {}
123
124 public static Create(configModelId: string | undefined, maxTimeout: number | undefined, client: CLClient): CLRunner {
125
126 // Ok to not provide modelId when just running in training UI.
127 // If not, Use UI_RUNNER_APPID const as lookup value
128 let newRunner = new CLRunner(configModelId, maxTimeout, client);
129 CLRunner.Runners[configModelId || Utils.UI_RUNNER_APPID] = newRunner;
130
131 // Bot can define multiple CLs. Always run UI on first CL defined in the bot
132 if (!CLRunner.UIRunner) {
133 CLRunner.UIRunner = newRunner;
134 }
135
136 return newRunner;
137 }
138
139 // Get CLRunner for the UI
140 public static GetRunnerForUI(appId?: string): CLRunner {
141
142 // Runner with the appId may not exist if running training UI, if so use the UI Runner
143 if (!appId || !CLRunner.Runners[appId]) {
144 if (CLRunner.UIRunner) {
145 return CLRunner.UIRunner;
146 } else {
147 throw new Error(`Not in UI and requested CLRunner that doesn't exist: ${appId}`)
148 }
149 }
150 return CLRunner.Runners[appId];
151 }
152
153 private constructor(configModelId: string | undefined, maxTimeout: number | undefined, client: CLClient) {
154 this.configModelId = configModelId
155 this.maxTimeout = maxTimeout
156 this.clClient = client
157 }
158
159 public botChecksum(): string {
160 // Create bot checksum is doesn't already exist
161 if (!this.checksum) {
162 const callbacks = Object.values(this.callbacks).map(this.convertInternalCallbackToCallback)
163 const templates = TemplateProvider.GetTemplates()
164 this.checksum = Utils.botChecksum(callbacks, templates)
165 }
166 return this.checksum
167 }
168
169 public convertInternalCallbackToCallback = <T>(c: InternalCallback<T>): CLM.Callback => {
170 const { logic, render, ...callback } = c
171 return callback
172 }
173
174 public async onTurn(turnContext: BB.TurnContext, next: (result: CLRecognizerResult | null) => Promise<void>): Promise<void> {
175 const recognizerResult = await this.recognize(turnContext, true);
176 return next(recognizerResult)
177 }
178
179 public async recognize(turnContext: BB.TurnContext, force?: boolean): Promise<CLRecognizerResult | null> {
180 // Add input to queue
181 const res = await this.AddInput(turnContext);
182 return res
183 }
184
185 public async InTrainingUI(turnContext: BB.TurnContext): Promise<boolean> {
186 if (turnContext.activity.from && turnContext.activity.from.name === Utils.CL_DEVELOPER) {
187 let clMemory = await CLMemory.InitMemory(turnContext)
188 let app = await clMemory.BotState.GetApp()
189 // If no app selected in UI or no app set in config, or they don't match return true
190 if (!app || !this.configModelId || app.appId !== this.configModelId) {
191 return true
192 }
193 }
194 return false
195 }
196
197 // Allows Bot developer to start a new Session with initial parameters (never in Teach)
198 public async BotStartSession(turnContext: BB.TurnContext): Promise<void> {
199
200 // Set adapter / conversation reference even if from field not set
201 let conversationReference = BB.TurnContext.getConversationReference(turnContext.activity);
202 this.SetAdapter(turnContext.adapter, conversationReference);
203
204 const activity = turnContext.activity
205 if (activity.from === undefined || activity.id == undefined) {
206 return;
207 }
208
209 try {
210 let app = await this.GetRunningApp(turnContext, false);
211 let clMemory = await CLMemory.InitMemory(turnContext)
212
213 if (app) {
214 let packageId = (app.livePackageId || app.devPackageId)
215 if (packageId) {
216 const sessionCreateParams: CLM.SessionCreateParams = {
217 saveToLog: app.metadata.isLoggingOn !== false,
218 packageId: packageId,
219 initialFilledEntities: []
220 }
221 await this.StartSessionAsync(clMemory, activity.conversation.id, app.appId, SessionStartFlags.NONE, sessionCreateParams)
222 }
223 }
224 }
225 catch (error) {
226 CLDebug.Error(error)
227 }
228 }
229
230 public SetAdapter(adapter: BB.BotAdapter, conversationReference: Partial<BB.ConversationReference>) {
231 this.adapter = adapter
232 CLDebug.InitLogger(adapter, conversationReference)
233 }
234
235 // Add input to queue. Allows CL to handle out-of-order messages
236 private async AddInput(turnContext: BB.TurnContext): Promise<CLRecognizerResult | null> {
237
238 // Set adapter / conversation reference even if from field not set
239 let conversationReference = BB.TurnContext.getConversationReference(turnContext.activity);
240 this.SetAdapter(turnContext.adapter, conversationReference);
241
242 // ConversationUpdate messages are not processed by ConversationLearner
243 // They should be handled in the general bot code
244 if (turnContext.activity.type == "conversationUpdate") {
245 CLDebug.Verbose(`Ignoring Conversation update... +${JSON.stringify(turnContext.activity.membersAdded)} -${JSON.stringify(turnContext.activity.membersRemoved)}`);
246 return null
247 }
248
249 if (turnContext.activity.from === undefined || turnContext.activity.id == undefined) {
250 return null;
251 }
252
253 let clMemory = await CLMemory.InitMemory(turnContext)
254 let botState = clMemory.BotState;
255
256 // If I'm in teach or edit mode process message right away
257 let uiMode = await botState.getUIMode();
258 if (uiMode !== UIMode.NONE) {
259 return await this.ProcessInput(turnContext);
260 }
261
262 // Otherwise I have to queue up messages as user may input them faster than bot responds
263 else {
264 let addInputPromise = util.promisify(InputQueue.AddInput);
265 let isReady = await addInputPromise(botState, turnContext.activity, conversationReference);
266
267 if (isReady) {
268 let intents = await this.ProcessInput(turnContext);
269 return intents;
270 }
271 // Message has expired
272 return null;
273 }
274 }
275
276 public async StartSessionAsync(clMemory: CLMemory, conversationId: string | BB.ConversationReference | null, appId: string, sessionStartFlags: SessionStartFlags, createParams: CLM.SessionCreateParams | CLM.CreateTeachParams): Promise<CLM.Teach | CLM.Session> {
277
278 const inTeach = ((sessionStartFlags & SessionStartFlags.IN_TEACH) > 0)
279 let entityList = await this.clClient.GetEntities(appId)
280
281 // If not continuing an edited session, call endSession
282 if (!(sessionStartFlags && SessionStartFlags.IS_EDIT_CONTINUE)) {
283 // Default callback will clear the bot memory.
284 // END_SESSION action was never triggered, so SessionEndState.OPEN
285 await this.CheckSessionEndCallback(clMemory, entityList.entities, CLM.SessionEndState.OPEN);
286 }
287
288 // check that this works = should it be inside edit continue above
289 // Check if StartSession call is required
290 await this.CheckSessionStartCallback(clMemory, entityList.entities);
291 let startSessionEntities = await clMemory.BotMemory.FilledEntitiesAsync()
292 startSessionEntities = [...createParams.initialFilledEntities || [], ...startSessionEntities]
293
294 const filledEntityMap = CLM.FilledEntityMap.FromFilledEntities(startSessionEntities, entityList.entities)
295 await clMemory.BotMemory.RestoreFromMapAsync(filledEntityMap)
296
297 // Start the new session
298 let sessionId: string
299 let logDialogId: string | null
300 let startResponse: CLM.Teach | CLM.Session
301 if (inTeach) {
302 const teachResponse = await this.clClient.StartTeach(appId, createParams as CLM.CreateTeachParams)
303 startResponse = CLM.ModelUtils.ToTeach(teachResponse)
304 sessionId = teachResponse.teachId
305 logDialogId = null
306 }
307 else {
308 startResponse = await this.clClient.StartSession(appId, createParams as CLM.SessionCreateParams)
309 sessionId = startResponse.sessionId
310 logDialogId = startResponse.logDialogId
311 }
312
313 // Initialize Bot State
314 await clMemory.BotState.InitSessionAsync(sessionId, logDialogId, conversationId, sessionStartFlags)
315
316 CLDebug.Verbose(`Started Session: ${sessionId} - ${conversationId}`)
317 return startResponse
318 }
319
320 // Get the currently running app
321 private async GetRunningApp(turnContext: BB.TurnContext, inEditingUI: boolean): Promise<CLM.AppBase | null> {
322
323 let clMemory = await CLMemory.InitMemory(turnContext)
324 let app = await clMemory.BotState.GetApp()
325
326 if (app) {
327 // If I'm not in the editing UI, always use app specified by options
328 if (!inEditingUI && this.configModelId && this.configModelId != app.appId) {
329 // Use config value
330 CLDebug.Log(`Switching to app specified in config: ${this.configModelId}`)
331 app = await this.clClient.GetApp(this.configModelId)
332 await clMemory.SetAppAsync(app)
333 }
334 }
335 // If I don't have an app, attempt to use one set in config
336 else if (this.configModelId) {
337 CLDebug.Log(`Selecting app specified in config: ${this.configModelId}`)
338 app = await this.clClient.GetApp(this.configModelId)
339 await clMemory.SetAppAsync(app)
340 }
341 return app;
342 }
343
344 // End a teach or log session
345 public async EndSessionAsync(memory: CLMemory, sessionEndState: CLM.SessionEndState, data?: string): Promise<void> {
346
347 let app = await memory.BotState.GetApp()
348
349 if (app) {
350 let entityList = await this.clClient.GetEntities(app.appId)
351
352 // Default callback will clear the bot memory
353 await this.CheckSessionEndCallback(memory, entityList.entities, sessionEndState, data);
354
355 await memory.BotState.EndSessionAsync();
356 }
357 }
358
359 // Process user input
360 private async ProcessInput(turnContext: BB.TurnContext): Promise<CLRecognizerResult | null> {
361 let errComponent = 'ProcessInput'
362 const activity = turnContext.activity
363 const conversationReference = BB.TurnContext.getConversationReference(activity)
364
365 // Validate request
366 if (!activity.from || !activity.from.id) {
367 throw new Error(`Attempted to get current session for user, but user was not defined on bot request.`)
368 }
369
370 try {
371
372 let inEditingUI =
373 conversationReference.user &&
374 conversationReference.user.name === Utils.CL_DEVELOPER || false;
375
376 // Validate setup
377 if (!inEditingUI && !this.configModelId) {
378 let msg = 'Must specify modelId in ConversationLearner constructor when not running bot in Editing UI\n\n'
379 CLDebug.Error(msg)
380 return null
381 }
382
383 if (!ConversationLearner.options || !ConversationLearner.options.LUIS_AUTHORING_KEY) {
384 let msg = 'Options must specify luisAuthoringKey. Set the LUIS_AUTHORING_KEY.\n\n'
385 CLDebug.Error(msg)
386 return null
387 }
388
389 let app = await this.GetRunningApp(turnContext, inEditingUI);
390 let clMemory = await CLMemory.InitMemory(turnContext)
391 let uiMode = await clMemory.BotState.getUIMode()
392
393 if (!app) {
394 let error = "ERROR: AppId 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"
395 await this.SendMessage(clMemory, error, activity.id)
396 return null;
397 }
398
399 let sessionId = await clMemory.BotState.GetSessionIdAndSetConversationId(activity.conversation.id)
400
401 // When UI is active inputs are handled via API calls from the Conversation Learner UI
402 if (uiMode !== UIMode.NONE) {
403 return null
404 }
405
406 // Check for expired session
407 if (sessionId) {
408 const currentTicks = new Date().getTime();
409 let lastActive = await clMemory.BotState.GetLastActive()
410 let passedTicks = currentTicks - lastActive;
411 if (passedTicks > this.maxTimeout!) {
412
413 // Parameters for new session
414 const sessionCreateParams: CLM.SessionCreateParams = {
415 saveToLog: app.metadata.isLoggingOn,
416 initialFilledEntities: []
417 }
418
419 // If I'm running in the editing UI I need to retreive the packageId as
420 // may not be running live package
421 if (inEditingUI) {
422 const result = await this.clClient.GetSession(app.appId, sessionId)
423 sessionCreateParams.packageId = result.packageId
424 }
425
426 // End the current session
427 await this.clClient.EndSession(app.appId, sessionId)
428 await this.EndSessionAsync(clMemory, CLM.SessionEndState.OPEN)
429
430 // If I'm not in the UI, reload the App to get any changes (live package version may have been updated)
431 if (!inEditingUI) {
432
433 if (!this.configModelId) {
434 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"
435 await this.SendMessage(clMemory, error, activity.id)
436 return null
437 }
438
439 app = await this.clClient.GetApp(this.configModelId)
440 await clMemory.SetAppAsync(app)
441
442 if (!app) {
443 let error = "ERROR: Failed to find Model specified by CONVERSATION_LEARNER_MODEL_ID"
444 await this.SendMessage(clMemory, error, activity.id)
445 return null
446 }
447
448 // Update logging state
449 sessionCreateParams.saveToLog = app.metadata.isLoggingOn
450 }
451
452 let conversationId = await clMemory.BotState.GetConversationId()
453
454 // Start a new session
455 let session = await this.StartSessionAsync(clMemory, conversationId, app.appId, SessionStartFlags.NONE, sessionCreateParams) as CLM.Session
456 sessionId = session.sessionId
457 }
458 // Otherwise update last access time
459 else {
460 await clMemory.BotState.SetLastActive(currentTicks);
461 }
462 }
463
464 // Handle any other non-message input
465 if (activity.type !== "message") {
466 await InputQueue.MessageHandled(clMemory.BotState, activity.id);
467 return null;
468 }
469
470 // PackageId: Use live package id if not in editing UI, default to devPackage if no active package set
471 let packageId = (inEditingUI ? await clMemory.BotState.GetEditingPackageForApp(app.appId) : app.livePackageId) || app.devPackageId
472 if (!packageId) {
473 await this.SendMessage(clMemory, "ERROR: No PackageId has been set", activity.id)
474 return null;
475 }
476
477 // If no session for this conversation, create a new one
478 if (!sessionId) {
479 const sessionCreateParams: CLM.SessionCreateParams = {
480 saveToLog: app.metadata.isLoggingOn !== false,
481 packageId: packageId,
482 initialFilledEntities: []
483 }
484 let session = await this.StartSessionAsync(clMemory, activity.conversation.id, app.appId, SessionStartFlags.NONE, sessionCreateParams) as CLM.Session
485 sessionId = session.sessionId
486 }
487
488 // Process any form data
489 let buttonResponse = await this.ProcessFormData(activity, clMemory, app.appId)
490
491 let entities: CLM.EntityBase[] = []
492
493 // Generate result
494 errComponent = 'Extract Entities'
495 let userInput: CLM.UserInput = { text: buttonResponse || activity.text || ' ' }
496 let extractResponse = await this.clClient.SessionExtract(app.appId, sessionId, userInput)
497 entities = extractResponse.definitions.entities
498 errComponent = 'Score Actions'
499 const scoredAction = await this.Score(
500 app.appId,
501 sessionId,
502 clMemory,
503 extractResponse.text,
504 extractResponse.predictedEntities,
505 entities,
506 false
507 )
508 return {
509 scoredAction: scoredAction,
510 clEntities: entities,
511 memory: clMemory,
512 inTeach: false,
513 activity: activity
514 } as CLRecognizerResult
515 } catch (error) {
516 // Try to end the session, so use can potentially recover
517 try {
518 const clMemory = await CLMemory.InitMemory(turnContext)
519 await this.EndSessionAsync(clMemory, CLM.SessionEndState.OPEN)
520 } catch {
521 CLDebug.Log(`Failed to End Session`)
522 }
523
524 CLDebug.Error(error, errComponent)
525 return null
526 }
527 }
528
529 private async ProcessFormData(request: BB.Activity, clMemory: CLMemory, appId: string): Promise<string | null> {
530 const data = request.value as FormData
531 if (data) {
532 // Get list of all entities
533 let entityList = await this.clClient.GetEntities(appId)
534
535 // For each form entry
536 for (let entityName of Object.keys(data)) {
537 // Reserved parameter
538 if (entityName == 'submit') {
539 continue
540 }
541
542 // Find the entity
543 let entity = entityList.entities.find((e: CLM.EntityBase) => e.entityName == entityName)
544
545 // If it exists, set it
546 if (entity) {
547 await clMemory.BotMemory.RememberEntity(entity.entityName, entity.entityId, data[entityName], entity.isMultivalue)
548 }
549 }
550
551 // If submit type return as a response
552 if (data['submit']) {
553 return data['submit']
554 }
555 }
556 return null
557 }
558
559 private async Score(
560 appId: string,
561 sessionId: string,
562 memory: CLMemory,
563 text: string,
564 predictedEntities: CLM.PredictedEntity[],
565 allEntities: CLM.EntityBase[],
566 inTeach: boolean,
567 skipEntityDetectionCallBack: boolean = false
568 ): Promise<CLM.ScoredAction> {
569 // Call LUIS callback
570 let scoreInput = await this.CallEntityDetectionCallback(text, predictedEntities, memory, allEntities, skipEntityDetectionCallBack)
571
572 // Call the scorer
573 let scoreResponse = null
574 if (inTeach) {
575 scoreResponse = await this.clClient.TeachScore(appId, sessionId, scoreInput)
576 } else {
577 scoreResponse = await this.clClient.SessionScore(appId, sessionId, scoreInput)
578 }
579
580 // Get best action
581 let bestAction = scoreResponse.scoredActions[0]
582
583 // Return the action
584 return bestAction
585 }
586
587 //-------------------------------------------
588 // Optional callback than runs after LUIS but before Conversation Learner. Allows Bot to substitute entities
589 public entityDetectionCallback: EntityDetectionCallback | undefined
590
591 // Optional callback than runs before a new chat session starts. Allows Bot to set initial entities
592 public onSessionStartCallback: OnSessionStartCallback | undefined
593
594 // Optional callback than runs when a session ends. Allows Bot set and/or preserve memories after session end
595 public onSessionEndCallback: OnSessionEndCallback | undefined
596
597 public AddCallback<T>(
598 callbackInput: ICallbackInput<T>
599 ) {
600 if (typeof callbackInput.name !== "string" || callbackInput.name.trim().length === 0) {
601 throw new Error(`You attempted to add callback but did not provide a valid name. Name must be non-empty string.`)
602 }
603
604 if (!callbackInput.logic && !callbackInput.render) {
605 throw new Error(`You attempted to add callback by name: ${callbackInput.name} but did not provide a logic or render function. You must provide at least one of them.`)
606 }
607
608 const callback: InternalCallback<T> = {
609 name: callbackInput.name,
610 logic: defaultLogicCallback,
611 logicArguments: [],
612 isLogicFunctionProvided: false,
613 render: undefined,
614 renderArguments: [],
615 isRenderFunctionProvided: false
616 }
617
618 if (callbackInput.logic) {
619 callback.logic = callbackInput.logic
620 callback.logicArguments = this.GetArguments(callbackInput.logic, 1)
621 callback.isLogicFunctionProvided = true
622 }
623
624 if (callbackInput.render) {
625 callback.render = callbackInput.render
626 callback.renderArguments = this.GetArguments(callbackInput.render, 2)
627 callback.isRenderFunctionProvided = true
628 }
629
630 this.callbacks[callbackInput.name] = callback
631 }
632
633 private GetArguments(func: Function, skip: number = 0): string[] {
634 const STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/gm
635 const ARGUMENT_NAMES = /([^\s,]+)/g
636
637 const fnStr = func.toString().replace(STRIP_COMMENTS, '')
638 const argumentNames = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES) || []
639 return argumentNames.filter((_, i) => i >= skip)
640 }
641
642 private async ProcessPredictedEntities(text: string, memory: BotMemory, predictedEntities: CLM.PredictedEntity[], allEntities: CLM.EntityBase[]): Promise<void> {
643
644 const predictedEntitiesWithType = predictedEntities.map(pe => {
645 let entity = allEntities.find(e => e.entityId == pe.entityId)
646 if (entity) {
647 return { entityType: entity.entityType, ...pe }
648 } else {
649 return { entityType: null, ...pe }
650 }
651 })
652
653 // Update entities in my memory
654 for (let predictedEntity of predictedEntities) {
655 let entity = allEntities.find(e => e.entityId == predictedEntity.entityId)
656 if (!entity) {
657 CLDebug.Error(`Could not find entity by id: ${predictedEntity.entityId}`)
658 return;
659 }
660
661 // Update resolution for entities with resolver type
662 if (entity.resolverType !== undefined
663 && entity.resolverType !== null
664 && (!predictedEntity.resolution || Object.keys(predictedEntity.resolution).length === 0)) {
665 const builtInEntity = predictedEntitiesWithType.find(pe => pe.startCharIndex >= predictedEntity.startCharIndex
666 && pe.endCharIndex <= predictedEntity.endCharIndex
667 && pe.entityType === (<any>entity).resolverType)
668 if (builtInEntity) {
669 predictedEntity.resolution = builtInEntity.resolution
670 predictedEntity.builtinType = builtInEntity.builtinType
671 }
672 }
673
674 // If negative entity will have a positive counter entity
675 if (entity.positiveId) {
676 await memory.ForgetEntity(entity.entityName, predictedEntity.entityText, entity.isMultivalue)
677 } else if (!entity.doNotMemorize) {
678 await memory.RememberEntity(
679 entity.entityName,
680 entity.entityId,
681 predictedEntity.entityText,
682 entity.isMultivalue,
683 predictedEntity.builtinType,
684 predictedEntity.resolution
685 )
686 }
687 }
688 }
689
690 public async CallEntityDetectionCallback(text: string, predictedEntities: CLM.PredictedEntity[], clMemory: CLMemory, allEntities: CLM.EntityBase[], skipEntityDetectionCallBack: boolean = false): Promise<CLM.ScoreInput> {
691
692 // Entities before processing
693 let prevMemories = new CLM.FilledEntityMap(await clMemory.BotMemory.FilledEntityMap());
694
695 // Update memory with predicted entities
696 await this.ProcessPredictedEntities(text, clMemory.BotMemory, predictedEntities, allEntities)
697
698 // If bot has callback and callback should not be skipped, call it
699 if (this.entityDetectionCallback && !skipEntityDetectionCallBack) {
700 let memoryManager = await this.CreateMemoryManagerAsync(clMemory, allEntities, prevMemories)
701
702 await this.entityDetectionCallback(text, memoryManager)
703
704 // Update Memory
705 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
706 }
707
708 // Get entities from my memory
709 const filledEntities = await clMemory.BotMemory.FilledEntitiesAsync()
710
711 let scoreInput: CLM.ScoreInput = {
712 filledEntities,
713 context: {},
714 maskedActions: []
715 }
716 return scoreInput
717 }
718
719 private async CreateMemoryManagerAsync(clMemory: CLMemory, allEntities: CLM.EntityBase[], prevMemories?: CLM.FilledEntityMap): Promise<ClientMemoryManager> {
720 let sessionInfo = await clMemory.BotState.SessionInfoAsync()
721 let curMemories = new CLM.FilledEntityMap(await clMemory.BotMemory.FilledEntityMap());
722 if (!prevMemories) {
723 prevMemories = curMemories;
724 }
725 return new ClientMemoryManager(prevMemories, curMemories, allEntities, sessionInfo);
726 }
727
728 private async CreateReadOnlyMemoryManagerAsync(clMemory: CLMemory, allEntities: CLM.EntityBase[], prevMemories?: CLM.FilledEntityMap): Promise<ReadOnlyClientMemoryManager> {
729 let sessionInfo = await clMemory.BotState.SessionInfoAsync()
730 let curMemories = new CLM.FilledEntityMap(await clMemory.BotMemory.FilledEntityMap());
731 if (!prevMemories) {
732 prevMemories = curMemories;
733 }
734 return new ReadOnlyClientMemoryManager(prevMemories, curMemories, allEntities, sessionInfo);
735 }
736
737 private async GetTurnContext(clMemory: CLMemory): Promise<BB.TurnContext> {
738
739 const getTurnContextForConversationReference = (conversationRef: Partial<BB.ConversationReference>, activity?: Partial<BB.Activity>): BB.TurnContext => {
740 if (!this.adapter) {
741 CLDebug.Error('Missing Adapter')
742 throw new Error('Adapter is missing!')
743 }
744
745 if (!activity) {
746 activity = <BB.Activity>{ type: BB.ActivityTypes.Message }
747 }
748 const incomingActivity = BB.TurnContext.applyConversationReference(activity, conversationRef, true)
749 return new BB.TurnContext(this.adapter, incomingActivity)
750 }
751
752 // Get conversation ref, so I can generate context and send it back to bot dev
753 let conversationReference = await clMemory.BotState.GetConversationReverence()
754 if (!conversationReference) {
755 throw new Error('Missing ConversationReference')
756 }
757
758 let context = clMemory.TurnContext
759 if (context === undefined) {
760 context = getTurnContextForConversationReference(conversationReference)
761 }
762 return context
763 }
764
765 // Call session start callback, set memory and return list of filled entities coming from callback
766 protected async CheckSessionStartCallback(clMemory: CLMemory, entities: CLM.EntityBase[]): Promise<void> {
767
768 // If bot has callback, call it
769 if (this.onSessionStartCallback && this.adapter) {
770 let memoryManager = await this.CreateMemoryManagerAsync(clMemory, entities)
771
772 // Get conversation ref, so I can generate context and send it back to bot dev
773 let conversationReference = await clMemory.BotState.GetConversationReverence()
774 if (!conversationReference) {
775 CLDebug.Error('Missing ConversationReference')
776 return
777 }
778
779 const context = await this.GetTurnContext(clMemory)
780 if (this.onSessionStartCallback) {
781 try {
782 await this.onSessionStartCallback(context, memoryManager)
783 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
784 }
785 catch (err) {
786 const text = "Exception hit in Bot's OnSessionStartCallback"
787 const message = BB.MessageFactory.text(text)
788 const replayError = new CLM.ReplayErrorAPIException()
789 message.channelData = { clData: { replayError } }
790
791 await this.SendMessage(clMemory, message)
792 CLDebug.Log(err);
793 }
794 }
795 }
796 }
797
798 protected async CheckSessionEndCallback(clMemory: CLMemory, entities: CLM.EntityBase[], sessionEndState: CLM.SessionEndState, data?: string): Promise<void> {
799
800 // If onEndSession hasn't been called yet, call it
801 let needEndSession = await clMemory.BotState.GetNeedSessionEndCall();
802
803 if (needEndSession) {
804
805 // If bot has callback, call it to determine which entities to clear / edit
806 if (this.onSessionEndCallback && this.adapter) {
807
808 let memoryManager = await this.CreateMemoryManagerAsync(clMemory, entities)
809
810 // Get conversation ref, so I can generate context and send it back to bot dev
811 let conversationReference = await clMemory.BotState.GetConversationReverence()
812 if (!conversationReference) {
813 CLDebug.Error('Missing ConversationReference')
814 return
815 }
816
817 const context = await this.GetTurnContext(clMemory)
818 try {
819 let saveEntities = this.onSessionEndCallback
820 ? await this.onSessionEndCallback(context, memoryManager, sessionEndState, data)
821 : undefined
822
823 await clMemory.BotMemory.ClearAsync(saveEntities)
824 }
825 catch (err) {
826 const text = "Exception hit in Bot's OnSessionEndCallback"
827 const message = BB.MessageFactory.text(text)
828 const replayError = new CLM.ReplayErrorAPIException()
829 message.channelData = { clData: { replayError } }
830
831 await this.SendMessage(clMemory, message)
832 CLDebug.Log(err);
833 }
834 }
835 // Otherwise just clear the memory
836 else {
837 await clMemory.BotMemory.ClearAsync()
838 }
839 await clMemory.BotState.SetNeedSessionEndCall(false);
840 }
841 }
842
843 public async TakeActionAsync(conversationReference: Partial<BB.ConversationReference>, clRecognizeResult: CLRecognizerResult, clData: CLM.CLChannelData | null): Promise<IActionResult> {
844 // Get filled entities from memory
845 let filledEntityMap = await clRecognizeResult.memory.BotMemory.FilledEntityMap()
846 filledEntityMap = Utils.addEntitiesById(filledEntityMap)
847
848 // If the action was terminal, free up the mutex allowing queued messages to be processed
849 // Activity won't be present if running in training as messages aren't queued
850 if (clRecognizeResult.scoredAction.isTerminal && clRecognizeResult.activity) {
851 await InputQueue.MessageHandled(clRecognizeResult.memory.BotState, clRecognizeResult.activity.id);
852 }
853
854 if (!conversationReference.conversation) {
855 throw new Error(`ConversationReference contains no conversation`)
856 }
857
858 let actionResult: IActionResult
859 let app: CLM.AppBase | null = null
860 let sessionId: string | null = null
861 let replayError: CLM.ReplayError | null = null
862 const inTeach = clData !== null
863 switch (clRecognizeResult.scoredAction.actionType) {
864 case CLM.ActionTypes.TEXT: {
865 // This is hack to allow ScoredAction to be accepted as ActionBase
866 // TODO: Remove extra properties from ScoredAction so it only had actionId and up service to return actions definitions of scored/unscored actions
867 // so UI can link the two together instead of having "partial" actions being incorrectly treated as full actions
868 const textAction = new CLM.TextAction(clRecognizeResult.scoredAction as any)
869 const response = await this.TakeTextAction(textAction, filledEntityMap)
870 actionResult = {
871 logicResult: undefined,
872 response
873 }
874 break
875 }
876 case CLM.ActionTypes.API_LOCAL: {
877 const apiAction = new CLM.ApiAction(clRecognizeResult.scoredAction as any)
878 actionResult = await this.TakeAPIAction(
879 apiAction,
880 filledEntityMap,
881 clRecognizeResult.memory,
882 clRecognizeResult.clEntities,
883 inTeach,
884 {
885 type: ActionInputType.LOGIC_AND_RENDER
886 }
887 )
888
889 if (inTeach) {
890 if (actionResult.replayError) {
891 replayError = actionResult.replayError
892 }
893 }
894 else {
895 app = await clRecognizeResult.memory.BotState.GetApp()
896 if (!app) {
897 throw new Error(`Attempted to get current app before app was set.`)
898 }
899 if (app.metadata.isLoggingOn !== false && actionResult && actionResult.logicResult !== undefined) {
900 if (!conversationReference.conversation) {
901 throw new Error(`Attempted to get session by conversation id, but user was not defined on current conversation`)
902 }
903
904 sessionId = await clRecognizeResult.memory.BotState.GetSessionIdAndSetConversationId(conversationReference.conversation.id)
905 if (!sessionId) {
906 throw new Error(`Attempted to get session by conversation id: ${conversationReference.conversation.id} but session was not found`)
907 }
908 await this.clClient.SessionLogicResult(app.appId, sessionId, apiAction.actionId, actionResult);
909 }
910 }
911 break
912 }
913 case CLM.ActionTypes.CARD: {
914 const cardAction = new CLM.CardAction(clRecognizeResult.scoredAction as any)
915 const response = await this.TakeCardAction(cardAction, filledEntityMap)
916 actionResult = {
917 logicResult: undefined,
918 response
919 }
920 break
921 }
922 case CLM.ActionTypes.END_SESSION: {
923 app = await clRecognizeResult.memory.BotState.GetApp()
924 const sessionAction = new CLM.SessionAction(clRecognizeResult.scoredAction as any)
925 sessionId = await clRecognizeResult.memory.BotState.GetSessionIdAndSetConversationId(conversationReference.conversation.id)
926 const response = await this.TakeSessionAction(sessionAction, filledEntityMap, inTeach, clRecognizeResult.memory, sessionId, app);
927 actionResult = {
928 logicResult: undefined,
929 response
930 }
931 break
932 }
933 case CLM.ActionTypes.SET_ENTITY: {
934 // TODO: Schema refactor
935 // scored actions aren't actions and only have payload instead of strongly typed values
936 const setEntityAction = new CLM.SetEntityAction(clRecognizeResult.scoredAction as any)
937 actionResult = await this.TakeSetEntityAction(
938 setEntityAction,
939 filledEntityMap,
940 clRecognizeResult.memory,
941 clRecognizeResult.clEntities,
942 inTeach
943 )
944 break
945 }
946 default:
947 throw new Error(`Could not find matching renderer for action type: ${clRecognizeResult.scoredAction.actionType}`)
948 }
949
950 // Convert string actions to activities
951 if (typeof actionResult.response === 'string') {
952 actionResult.response = BB.MessageFactory.text(actionResult.response)
953 }
954 if (actionResult.response && typeof actionResult.response !== 'string' && clData) {
955 actionResult.response.channelData = {
956 ...actionResult.response.channelData, clData: { ...clData, replayError: replayError || undefined }
957 }
958 }
959
960 // If action wasn't terminal loop through Conversation Learner again after a short delay
961 if (!clRecognizeResult.inTeach && !clRecognizeResult.scoredAction.isTerminal) {
962 if (app === null) {
963 app = await clRecognizeResult.memory.BotState.GetApp()
964 }
965 if (!app) {
966 throw new Error(`Attempted to get current app before app was set.`)
967 }
968
969 if (!conversationReference.conversation) {
970 throw new Error(`Attempted to get session by conversation id, but user was not defined on current conversation`)
971 }
972
973 if (sessionId == null) {
974 sessionId = await clRecognizeResult.memory.BotState.GetSessionIdAndSetConversationId(conversationReference.conversation.id)
975 }
976 if (!sessionId) {
977 throw new Error(`Attempted to get session by conversation id: ${conversationReference.conversation.id} but session was not found`)
978 }
979
980 // send the current response to user before score for the next turn
981 if (actionResult.response != null) {
982 await this.SendMessage(clRecognizeResult.memory, actionResult.response)
983 }
984 await delay(100)
985
986 let bestAction = await this.Score(
987 app.appId,
988 sessionId,
989 clRecognizeResult.memory,
990 '',
991 [],
992 clRecognizeResult.clEntities,
993 clRecognizeResult.inTeach,
994 true
995 )
996
997 clRecognizeResult.scoredAction = bestAction
998 actionResult = await this.TakeActionAsync(conversationReference, clRecognizeResult, clData)
999 }
1000 return actionResult
1001 }
1002
1003 public async SendIntent(intent: CLRecognizerResult, clData: CLM.CLChannelData | null = null): Promise<IActionResult | undefined> {
1004
1005 let conversationReference = await intent.memory.BotState.GetConversationReverence();
1006
1007 if (!conversationReference) {
1008 CLDebug.Error('Missing ConversationReference')
1009 return
1010 }
1011 if (!this.adapter) {
1012 CLDebug.Error('Missing Adapter')
1013 return
1014 }
1015
1016 const actionResult = await this.TakeActionAsync(conversationReference, intent, clData)
1017
1018 if (actionResult.response != null) {
1019 const context = await this.GetTurnContext(intent.memory)
1020 await context.sendActivity(actionResult.response)
1021 }
1022
1023 return actionResult
1024 }
1025
1026 private async SendMessage(memory: CLMemory, message: string | Partial<BB.Activity>, incomingActivityId?: string | undefined): Promise<void> {
1027
1028 // If requested, pop incoming activity from message queue
1029 if (incomingActivityId) {
1030 await InputQueue.MessageHandled(memory.BotState, incomingActivityId);
1031 }
1032
1033 let conversationReference = await memory.BotState.GetConversationReverence()
1034 if (!conversationReference) {
1035 CLDebug.Error('Missing ConversationReference')
1036 return
1037 }
1038
1039 if (!this.adapter) {
1040 CLDebug.Error(`Attempted to send message before adapter was assigned`)
1041 return
1042 }
1043 const context = await this.GetTurnContext(memory)
1044 await context.sendActivity(message)
1045 }
1046
1047 // TODO: This issue arises because we only save non-null non-empty argument values on the actions
1048 // which means callback may accept more arguments than is actually available on the action.arguments
1049 // To me, it seems it would make more sense to always have these be same length, but perhaps there is
1050 // dependency on action not being defined somewhere else in the application like ActionCreatorEditor
1051 private GetRenderedArguments(fnArgs: string[], actionArgs: CLM.ActionArgument[], filledEntityMap: CLM.FilledEntityMap): string[] {
1052 const missingEntityNames: string[] = []
1053 const renderedArgumentValues = fnArgs.map(param => {
1054 const argument = actionArgs.find(arg => arg.parameter === param)
1055 if (!argument) {
1056 return ''
1057 }
1058
1059 try {
1060 return argument.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap))
1061 }
1062 catch (error) {
1063 missingEntityNames.push(param)
1064 return ''
1065 }
1066 }, missingEntityNames)
1067
1068 if (missingEntityNames.length > 0) {
1069 throw new Error(`Missing Entity value(s) for ${missingEntityNames.join(', ')}`)
1070 }
1071
1072 return renderedArgumentValues
1073 }
1074
1075 public async TakeSetEntityAction(action: CLM.SetEntityAction, filledEntityMap: CLM.FilledEntityMap, clMemory: CLMemory, allEntities: CLM.EntityBase[], inTeach: boolean): Promise<IActionResult> {
1076 try {
1077 let replayError: CLM.ReplayError | undefined
1078 let response: Partial<BB.Activity> | string | null = null
1079
1080 const entity = allEntities.find(e => e.entityId === action.entityId)
1081 if (!entity) {
1082 throw new Error(`Set Entity Action: ${action.actionId} could not find the referenced entity with id: ${action.entityId}`)
1083 }
1084
1085 if (entity.entityType !== CLM.EntityType.ENUM) {
1086 throw new Error(`Set Entity Action: ${action.actionId} referenced entity ${entity.entityName} but it is not an ENUM. Please update the action to reference the correct entity.`)
1087 }
1088
1089 const enumValueObj = (entity.enumValues && entity.enumValues.find(ev => ev.enumValueId === action.enumValueId))
1090 if (!enumValueObj) {
1091 throw new Error(`Set Entity Action: ${action.actionId} which sets: ${entity.entityName} could not find the value with id: ${action.enumValueId}`)
1092 }
1093
1094 // TODO: Is there more efficient way to do this, like editing memory directly?
1095 const memoryManager = await this.CreateMemoryManagerAsync(clMemory, allEntities)
1096 memoryManager.Set(entity.entityName, enumValueObj.enumValue)
1097 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
1098
1099 if (inTeach) {
1100 response = this.RenderSetEntityCard(entity.entityName, enumValueObj.enumValue)
1101 }
1102
1103 return {
1104 logicResult: undefined,
1105 response,
1106 replayError: replayError || undefined
1107 }
1108 }
1109 catch (e) {
1110 const error: Error = e
1111 const title = error.message || `Exception hit when calling Set Entity Action: '${action.actionId}'`
1112 const message = this.RenderErrorCard(title, error.stack || error.message || "")
1113 const replayError = new CLM.ReplaySetEntityException()
1114 return {
1115 logicResult: undefined,
1116 response: message,
1117 replayError
1118 }
1119 }
1120 }
1121
1122 public async TakeAPIAction(apiAction: CLM.ApiAction, filledEntityMap: CLM.FilledEntityMap, clMemory: CLMemory, allEntities: CLM.EntityBase[], inTeach: boolean, actionInput: IActionInput): Promise<IActionResult> {
1123 // Extract API name and args
1124 const callback = this.callbacks[apiAction.name]
1125 if (!callback) {
1126 return {
1127 logicResult: undefined,
1128 response: `ERROR: API callback with name "${apiAction.name}" is not defined`
1129 }
1130 }
1131
1132 try {
1133 // Invoke Logic part of callback
1134 const renderedLogicArgumentValues = this.GetRenderedArguments(callback.logicArguments, apiAction.logicArguments, filledEntityMap)
1135 const memoryManager = await this.CreateMemoryManagerAsync(clMemory, allEntities)
1136 let replayError: CLM.ReplayError | null = null
1137
1138 // If we're only doing the render part, used stored values
1139 // This happens when replaying dialog to recreated action outputs
1140 let logicResult: CLM.LogicResult = { logicValue: undefined, changedFilledEntities: [] }
1141 if (actionInput.type === ActionInputType.RENDER_ONLY) {
1142 logicResult = actionInput.logicResult || logicResult
1143
1144 // Logic result holds delta from before after logic callback, use it to update memory
1145 memoryManager.curMemories.UpdateFilledEntities(logicResult.changedFilledEntities, allEntities)
1146
1147 // Update memory with changes from logic callback
1148 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
1149 }
1150 else {
1151 try {
1152 // create a copy of the map before calling into logic api
1153 // the copy of map is created because the passed infilledEntityMap contains "filledEntities by Id" too
1154 // and this causes issues when calculating changedFilledEntities.
1155 const entityMapBeforeCall = new CLM.FilledEntityMap(await clMemory.BotMemory.FilledEntityMap())
1156 // Store logic callback value
1157 const logicObject = await callback.logic(memoryManager, ...renderedLogicArgumentValues)
1158 logicResult.logicValue = JSON.stringify(logicObject)
1159 // Update memory with changes from logic callback
1160 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
1161 // Store changes to filled entities
1162 logicResult.changedFilledEntities = CLM.ModelUtils.changedFilledEntities(entityMapBeforeCall, memoryManager.curMemories)
1163 }
1164 catch (error) {
1165 let botAPIError: CLM.LogicAPIError = { APIError: error.stack || error.message || error }
1166 logicResult.logicValue = JSON.stringify(botAPIError)
1167 replayError = new CLM.ReplayErrorAPIException()
1168 }
1169 }
1170
1171 // Render the action unless only doing logic part
1172 if (actionInput.type === ActionInputType.LOGIC_ONLY) {
1173 return {
1174 logicResult,
1175 response: null,
1176 replayError: replayError || undefined
1177 }
1178 }
1179 else {
1180 let response: Partial<BB.Activity> | string | null = null
1181 let logicAPIError = Utils.GetLogicAPIError(logicResult)
1182
1183 // If there was an api Error show card to user
1184 if (logicAPIError) {
1185 const title = `Exception hit in Bot's API Callback: '${apiAction.name}'`
1186 response = this.RenderErrorCard(title, logicAPIError.APIError)
1187 }
1188 else if (logicResult.logicValue && !callback.render) {
1189 const title = `Malformed API Callback: '${apiAction.name}'`
1190 response = this.RenderErrorCard(title, "Logic portion of callback returns a value, but no Render portion defined")
1191 replayError = new CLM.ReplayErrorAPIMalformed()
1192 }
1193 else {
1194 // Invoke Render part of callback
1195 const renderedRenderArgumentValues = this.GetRenderedArguments(callback.renderArguments, apiAction.renderArguments, filledEntityMap)
1196
1197 const readOnlyMemoryManager = await this.CreateReadOnlyMemoryManagerAsync(clMemory, allEntities)
1198
1199 let logicObject = logicResult.logicValue ? JSON.parse(logicResult.logicValue) : undefined
1200 if (callback.render) {
1201 response = await callback.render(logicObject, readOnlyMemoryManager, ...renderedRenderArgumentValues)
1202 }
1203
1204 if (response && !Utils.IsCardValid(response)) {
1205 const title = `Malformed API Callback '${apiAction.name}'`
1206 const error = `Return value in Render function must be a string or BotBuilder Activity`
1207 response = this.RenderErrorCard(title, error)
1208 replayError = new CLM.ReplayErrorAPIBadCard()
1209 }
1210
1211 // If response is empty, but we're in teach session return a placeholder card in WebChat so they can click it to edit
1212 // Otherwise return the response as is.
1213 if (!response && inTeach) {
1214 response = this.RenderAPICard(callback, renderedLogicArgumentValues)
1215 }
1216 }
1217 return {
1218 logicResult,
1219 response,
1220 replayError: replayError || undefined
1221 }
1222 }
1223 }
1224 catch (err) {
1225 const title = `Exception hit in Bot's API Callback: '${apiAction.name}'`
1226 const message = this.RenderErrorCard(title, err.stack || err.message || "")
1227 const replayError = new CLM.ReplayErrorAPIException()
1228 return {
1229 logicResult: undefined,
1230 response: message,
1231 replayError
1232 }
1233 }
1234 }
1235
1236 public async TakeTextAction(textAction: CLM.TextAction, filledEntityMap: CLM.FilledEntityMap): Promise<Partial<BB.Activity> | string> {
1237 return Promise.resolve(textAction.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap)))
1238 }
1239
1240 public async TakeCardAction(cardAction: CLM.CardAction, filledEntityMap: CLM.FilledEntityMap): Promise<Partial<BB.Activity> | string> {
1241 try {
1242 const entityDisplayValues = CLM.getEntityDisplayValueMap(filledEntityMap)
1243 const renderedArguments = cardAction.renderArguments(entityDisplayValues)
1244
1245 const missingEntities = renderedArguments.filter(ra => ra.value === null);
1246 if (missingEntities.length > 0) {
1247 return `ERROR: Missing Entity value(s) for ${missingEntities.map(me => me.parameter).join(', ')}`;
1248 }
1249
1250 const form = await TemplateProvider.RenderTemplate(cardAction.templateName, renderedArguments)
1251
1252 if (form == null) {
1253 return CLDebug.Error(`Missing Template: ${cardAction.templateName}`)
1254 }
1255 const attachment = BB.CardFactory.adaptiveCard(form)
1256 const message = BB.MessageFactory.attachment(attachment)
1257 message.text = undefined
1258 return message
1259 } catch (error) {
1260 let msg = CLDebug.Error(error, 'Failed to Render Template')
1261 return msg
1262 }
1263 }
1264
1265 private async TakeSessionAction(sessionAction: CLM.SessionAction, filledEntityMap: CLM.FilledEntityMap, inTeach: boolean, clMemory: CLMemory, sessionId: string | null, app: CLM.AppBase | null): Promise<Partial<BB.Activity> | null> {
1266
1267 // Get any context from the action
1268 let content = sessionAction.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap))
1269
1270 // If inTeach, show something to user in WebChat so they can edit
1271 if (inTeach) {
1272 let payload = sessionAction.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap))
1273 let card = {
1274 type: "AdaptiveCard",
1275 version: "1.0",
1276 body: [
1277 {
1278 type: "TextBlock",
1279 text: `EndSession: *${payload}*`
1280 }
1281 ]
1282 }
1283 const attachment = BB.CardFactory.adaptiveCard(card)
1284 const message = BB.MessageFactory.attachment(attachment)
1285 return message
1286 }
1287 // If I'm not in Teach end session.
1288 // (In Teach EndSession is handled in ScoreFeedback to keep session alive for TeachScoreFeedback)
1289 else {
1290 // End the current session (if in replay will be no sessionId or app)
1291 if (app && sessionId) {
1292 await this.clClient.EndSession(app.appId, sessionId)
1293 await this.EndSessionAsync(clMemory, CLM.SessionEndState.COMPLETED, content);
1294 }
1295 }
1296 return null
1297 }
1298
1299 // Returns true if Action is available given Entities in Memory
1300 public isActionAvailable(action: CLM.ActionBase, filledEntities: CLM.FilledEntity[]): boolean {
1301
1302 for (let entityId of action.requiredEntities) {
1303 let found = filledEntities.find(e => e.entityId == entityId);
1304 if (!found || found.values.length === 0) {
1305 return false;
1306 }
1307 }
1308 for (let entityId of action.negativeEntities) {
1309 let found = filledEntities.find(e => e.entityId == entityId);
1310 if (found && found.values.length > 0) {
1311 return false;
1312 }
1313 }
1314 return true;
1315 }
1316
1317 // Convert list of filled entities into a filled entity map lookup table
1318 private CreateFilledEntityMap(filledEntities: CLM.FilledEntity[], entityList: CLM.EntityList): CLM.FilledEntityMap {
1319 let filledEntityMap = new CLM.FilledEntityMap()
1320 for (let filledEntity of filledEntities) {
1321 let entity = entityList.entities.find(e => e.entityId == filledEntity.entityId)
1322 if (entity) {
1323 filledEntityMap.map[entity.entityName] = filledEntity
1324 filledEntityMap.map[entity.entityId] = filledEntity
1325 }
1326 }
1327 return filledEntityMap
1328 }
1329
1330 /**
1331 * Identify any validation issues
1332 * Missing Entities
1333 * Missing Actions
1334 * Unavailable Actions
1335 */
1336 public DialogValidationErrors(trainDialog: CLM.TrainDialog, entities: CLM.EntityBase[], actions: CLM.ActionBase[]): string[] {
1337
1338 let validationErrors: string[] = [];
1339
1340 for (let round of trainDialog.rounds) {
1341 let userText = round.extractorStep.textVariations[0].text;
1342 let filledEntities = round.scorerSteps[0] && round.scorerSteps[0].input ? round.scorerSteps[0].input.filledEntities : []
1343
1344 // Check that entities exist
1345 for (let filledEntity of filledEntities) {
1346 if (!entities.find(e => e.entityId == filledEntity.entityId)) {
1347 validationErrors.push(`Missing Entity for "${CLM.filledEntityValueAsString(filledEntity)}"`);
1348 }
1349 }
1350
1351 for (let scorerStep of round.scorerSteps) {
1352 let labelAction = scorerStep.labelAction
1353
1354 // Check that action exists
1355 let selectedAction = actions.find(a => a.actionId == labelAction)
1356 if (!selectedAction) {
1357 validationErrors.push(`Missing Action response for "${userText}"`);
1358 }
1359 else {
1360 // Check action availability
1361 if (!this.isActionAvailable(selectedAction, scorerStep.input.filledEntities)) {
1362 validationErrors.push(`Selected Action in unavailable in response to "${userText}"`);
1363 }
1364 }
1365 }
1366 }
1367 // Make errors unique using Set operator
1368 validationErrors = [...new Set(validationErrors)]
1369 return validationErrors;
1370 }
1371
1372 /** Return a list of trainDialogs that are invalid for the given set of entities and actions */
1373 public validateTrainDialogs(appDefinition: CLM.AppDefinition): string[] {
1374 let invalidTrainDialogIds = [];
1375 for (let trainDialog of appDefinition.trainDialogs) {
1376 // Ignore train dialogs that are already invalid
1377 if (trainDialog.validity !== CLM.Validity.INVALID) {
1378 let validationErrors = this.DialogValidationErrors(trainDialog, appDefinition.entities, appDefinition.actions);
1379 if (validationErrors.length > 0) {
1380 invalidTrainDialogIds.push(trainDialog.trainDialogId);
1381 }
1382 }
1383 }
1384 return invalidTrainDialogIds;
1385 }
1386
1387 /** Populate prebuilt information in predicted entities given filled entity array */
1388 private PopulatePrebuilts(predictedEntities: CLM.PredictedEntity[], filledEntities: CLM.FilledEntity[]) {
1389 for (let pe of predictedEntities) {
1390 let filledEnt = filledEntities.find(fe => fe.entityId === pe.entityId);
1391 if (filledEnt) {
1392 let value = filledEnt.values.find(v => v.userText === pe.entityText)
1393 if (value) {
1394 pe.resolution = value.resolution;
1395 if (value.builtinType) {
1396 pe.builtinType = value.builtinType;
1397 }
1398 }
1399 }
1400 }
1401 }
1402
1403 /**
1404 * Provide empty FilledEntity for any missing entities so they can still be rendered
1405 */
1406 private PopulateMissingFilledEntities(action: CLM.ActionBase, filledEntityMap: CLM.FilledEntityMap, allEntities: CLM.EntityBase[], bidirectional: boolean): string[] {
1407 // For backwards compatibiliity need to check requieredEntities too. In new version all in requiredEntitiesFromPayload
1408 const allRequiredEntities = [...action.requiredEntities, ...action.requiredEntitiesFromPayload]
1409 let missingEntities: string[] = []
1410
1411 allRequiredEntities.forEach((entityId: string) => {
1412 let entity = allEntities.find(e => e.entityId === entityId)
1413 if (entity) {
1414 if (!filledEntityMap.map[entity.entityName]) {
1415 // Add an empty filledEntity if requried and has no values
1416 let filledEntity = {
1417 entityId: entityId,
1418 values: []
1419 } as CLM.FilledEntity
1420 filledEntityMap.map[entity.entityId] = filledEntity
1421 if (bidirectional) {
1422 filledEntityMap.map[entity.entityName] = filledEntity
1423 }
1424 missingEntities.push(entity.entityName)
1425 }
1426 else {
1427 const filledEntity = filledEntityMap.map[entity.entityName]
1428 if (filledEntity && filledEntity.values.length == 0) {
1429 missingEntities.push(entity.entityName)
1430 }
1431 }
1432 } else {
1433 throw new Error(`ENTITY ${entityId} DOES NOT EXIST`)
1434 }
1435 })
1436 return missingEntities
1437 }
1438
1439 /**
1440 * Initialize memory for replay
1441 */
1442 private async InitReplayMemory(clMemory: CLMemory, trainDialog: CLM.TrainDialog, allEntities: CLM.EntityBase[]) {
1443
1444 // Reset the memory
1445 await clMemory.BotMemory.ClearAsync()
1446
1447 // Call start sesssion for initial entities
1448 await this.CheckSessionStartCallback(clMemory, allEntities);
1449 let startSessionEntities = await clMemory.BotMemory.FilledEntitiesAsync()
1450 startSessionEntities = [...trainDialog.initialFilledEntities || [], ...startSessionEntities]
1451
1452 let map = CLM.FilledEntityMap.FromFilledEntities(startSessionEntities, allEntities)
1453 await clMemory.BotMemory.RestoreFromMapAsync(map)
1454 }
1455
1456 /**
1457 * Replay a TrainDialog, calling EntityDetection callback and API Logic,
1458 * recalculating FilledEntities along the way
1459 */
1460 public async ReplayTrainDialogLogic(trainDialog: CLM.TrainDialog, clMemory: CLMemory, cleanse: boolean): Promise<CLM.TrainDialog> {
1461
1462 if (!trainDialog || !trainDialog.rounds) {
1463 return trainDialog
1464 }
1465
1466 // Copy train dialog
1467 let newTrainDialog: CLM.TrainDialog = JSON.parse(JSON.stringify(trainDialog))
1468
1469 let entities: CLM.EntityBase[] = trainDialog.definitions ? trainDialog.definitions.entities : []
1470 let actions: CLM.ActionBase[] = trainDialog.definitions ? trainDialog.definitions.actions : []
1471 let entityList: CLM.EntityList = { entities }
1472
1473 await this.InitReplayMemory(clMemory, newTrainDialog, entities)
1474
1475 for (let round of newTrainDialog.rounds) {
1476
1477 // Call entity detection callback with first text Variation
1478 let textVariation = round.extractorStep.textVariations[0]
1479 let predictedEntities = CLM.ModelUtils.ToPredictedEntities(textVariation.labelEntities)
1480
1481 // Call EntityDetectionCallback and populate filledEntities with the result
1482 let scoreInput: CLM.ScoreInput
1483 let botAPIError: CLM.LogicAPIError | null = null
1484 try {
1485 scoreInput = await this.CallEntityDetectionCallback(textVariation.text, predictedEntities, clMemory, entities)
1486 }
1487 catch (err) {
1488 // Hit exception in Bot's Entity Detection Callback
1489 // Use existing memory before callback
1490 const filledEntities = await clMemory.BotMemory.FilledEntitiesAsync()
1491 scoreInput = {
1492 filledEntities,
1493 context: {},
1494 maskedActions: []
1495 }
1496
1497 // Create error to show to user
1498 let errMessage = `${CLStrings.ENTITYCALLBACK_EXCEPTION} ${err.message}`
1499 botAPIError = { APIError: errMessage }
1500 }
1501
1502 // Use scorer step to populate pre-built data (when)
1503 if (round.scorerSteps && round.scorerSteps.length > 0) {
1504 this.PopulatePrebuilts(predictedEntities, scoreInput.filledEntities)
1505 round.scorerSteps[0].input.filledEntities = scoreInput.filledEntities
1506
1507 // Go through each scorer step
1508 for (let [scoreIndex, scorerStep] of round.scorerSteps.entries()) {
1509
1510 let curAction = actions.filter((a: CLM.ActionBase) => a.actionId === scorerStep.labelAction)[0]
1511 if (curAction) {
1512
1513 let filledEntityMap = await clMemory.BotMemory.FilledEntityMap()
1514
1515 // Provide empty FilledEntity for missing entities
1516 if (!cleanse) {
1517 this.PopulateMissingFilledEntities(curAction, filledEntityMap, entities, false)
1518 }
1519
1520 round.scorerSteps[scoreIndex].input.filledEntities = filledEntityMap.FilledEntities()
1521
1522 // Run logic part of APIAction to update the FilledEntities
1523 if (curAction.actionType === CLM.ActionTypes.API_LOCAL) {
1524 const apiAction = new CLM.ApiAction(curAction)
1525 const actionInput: IActionInput = {
1526 type: ActionInputType.LOGIC_ONLY
1527 }
1528 // Calculate and store new logic result
1529 const filledIdMap = filledEntityMap.EntityMapToIdMap()
1530 let actionResult = await this.TakeAPIAction(apiAction, filledIdMap, clMemory, entityList.entities, true, actionInput)
1531 round.scorerSteps[scoreIndex].logicResult = actionResult.logicResult
1532 } else if (curAction.actionType === CLM.ActionTypes.END_SESSION) {
1533 const sessionAction = new CLM.SessionAction(curAction)
1534 await this.TakeSessionAction(sessionAction, filledEntityMap, true, clMemory, null, null)
1535 } else if (curAction.actionType === CLM.ActionTypes.SET_ENTITY) {
1536 const setEntityAction = new CLM.SetEntityAction(curAction)
1537 await this.TakeSetEntityAction(setEntityAction, filledEntityMap, clMemory, entityList.entities, true)
1538 }
1539 }
1540
1541 // If ran into API error inject into first scorer step so it gets displayed to the user
1542 if (botAPIError && scoreIndex === 0) {
1543 round.scorerSteps[scoreIndex].logicResult = { logicValue: JSON.stringify(botAPIError), changedFilledEntities: [] }
1544 }
1545 }
1546 }
1547 else {
1548 // Otherwise create a dummy scorer step with the filled entities
1549 const scorerStep: CLM.TrainScorerStep = {
1550 input: {
1551 filledEntities: await clMemory.BotMemory.FilledEntitiesAsync(),
1552 context: {},
1553 maskedActions: []
1554 },
1555 labelAction: undefined,
1556 logicResult: undefined,
1557 scoredAction: undefined
1558 }
1559 if (!round.scorerSteps) {
1560 round.scorerSteps = []
1561 }
1562 round.scorerSteps.push(scorerStep)
1563 }
1564 }
1565
1566 // When editing, may need to run Scorer or Extrator on TrainDialog with invalid rounds
1567 //This cleans up the TrainDialog removing bad data so the extractor can run
1568 if (cleanse) {
1569 // Remove rounds with two user inputs in a row (they'll have a dummy scorer round)
1570 newTrainDialog.rounds = newTrainDialog.rounds.filter(r => {
1571 return !r.scorerSteps[0] || r.scorerSteps[0].labelAction != undefined
1572 })
1573
1574 }
1575 return newTrainDialog
1576 }
1577
1578 /**
1579 * Get Activities generated by trainDialog.
1580 * Return any errors in TrainDialog
1581 * NOTE: Will set bot memory to state at end of history
1582 */
1583 public async GetHistory(trainDialog: CLM.TrainDialog, userName: string, userId: string, clMemory: CLMemory): Promise<CLM.TeachWithHistory | null> {
1584
1585 let entities: CLM.EntityBase[] = trainDialog.definitions ? trainDialog.definitions.entities : []
1586 let actions: CLM.ActionBase[] = trainDialog.definitions ? trainDialog.definitions.actions : []
1587 let entityList: CLM.EntityList = { entities }
1588 let prevMemories: CLM.Memory[] = []
1589
1590 if (!trainDialog || !trainDialog.rounds) {
1591 return null
1592 }
1593
1594 await this.InitReplayMemory(clMemory, trainDialog, entities)
1595
1596 let excludedEntities = entities.filter(e => e.doNotMemorize).map(e => e.entityId)
1597 let activities: Partial<BB.Activity>[] = []
1598 let replayError: CLM.ReplayError | null = null
1599 let replayErrors: CLM.ReplayError[] = [];
1600 let curAction = null
1601
1602 for (let [roundNum, round] of trainDialog.rounds.entries()) {
1603 let filledEntities = round.scorerSteps[0] && round.scorerSteps[0].input ? round.scorerSteps[0].input.filledEntities : []
1604
1605 // VALIDATION
1606 replayError = null
1607
1608 // Check that non-multivalue isn't labelled twice
1609 for (let tv of round.extractorStep.textVariations) {
1610 let usedEntities: string[] = []
1611 for (let labelEntity of tv.labelEntities) {
1612 // If already used, make sure it's multi-value
1613 if (usedEntities.find(e => e === labelEntity.entityId)) {
1614 let entity = entities.find(e => e.entityId == labelEntity.entityId)
1615 if (entity && !entity.isMultivalue
1616 && (entity.entityType === CLM.EntityType.LUIS || entity.entityType === CLM.EntityType.LOCAL)) {
1617 replayError = replayError || new CLM.EntityUnexpectedMultivalue(entity.entityName)
1618 replayErrors.push(replayError);
1619 }
1620 }
1621 // Otherwise add to list of used entities
1622 else {
1623 usedEntities.push(labelEntity.entityId)
1624 }
1625 }
1626 }
1627
1628 // Check that entities exist in text variations
1629 for (let tv of round.extractorStep.textVariations) {
1630 for (let labelEntity of tv.labelEntities) {
1631 if (!entities.find(e => e.entityId == labelEntity.entityId)) {
1632 replayError = new CLM.ReplayErrorEntityUndefined(labelEntity.entityId)
1633 replayErrors.push()
1634 }
1635 }
1636 }
1637
1638 // Check that entities exist in filled entities
1639 for (let filledEntity of filledEntities) {
1640 if (!entities.find(e => e.entityId == filledEntity.entityId)) {
1641 replayError = new CLM.ReplayErrorEntityUndefined(CLM.filledEntityValueAsString(filledEntity))
1642 replayErrors.push()
1643 }
1644 }
1645
1646 // Check for double user inputs
1647 if (roundNum != trainDialog.rounds.length - 1 &&
1648 (round.scorerSteps.length === 0 || !round.scorerSteps[0].labelAction)) {
1649 replayError = new CLM.ReplayErrorTwoUserInputs()
1650 replayErrors.push(replayError)
1651 }
1652
1653 // Check for user input when previous action wasn't wait
1654 if (curAction && !curAction.isTerminal) {
1655 replayError = new CLM.ReplayErrorInputAfterNonWait()
1656 replayErrors.push(replayError)
1657 }
1658
1659 // Generate activity. Add markdown to highlight labelled entities
1660 let userText = CLM.ModelUtils.textVariationToMarkdown(round.extractorStep.textVariations[0], excludedEntities)
1661 let userActivity: Partial<BB.Activity> = CLM.ModelUtils.InputToActivity(userText, userName, userId, roundNum)
1662 userActivity.channelData.clData.replayError = replayError
1663 userActivity.channelData.clData.activityIndex = activities.length
1664 userActivity.textFormat = 'markdown'
1665 activities.push(userActivity)
1666
1667 // Save memory before this step (used to show changes in UI)
1668 prevMemories = await clMemory.BotMemory.DumpMemory()
1669
1670 let textVariation = round.extractorStep.textVariations[0]
1671 let predictedEntities = CLM.ModelUtils.ToPredictedEntities(textVariation.labelEntities)
1672
1673 // Use scorer step to populate pre-built data (when)
1674 if (round.scorerSteps.length > 0) {
1675 this.PopulatePrebuilts(predictedEntities, round.scorerSteps[0].input.filledEntities)
1676 }
1677
1678 for (let [scoreIndex, scorerStep] of round.scorerSteps.entries()) {
1679
1680 let labelAction = scorerStep.labelAction
1681
1682 // Scorer rounds w/o labelActions may exist to store extraction result for rendering
1683 if (labelAction) {
1684
1685 let scoreFilledEntities = scorerStep.input.filledEntities
1686
1687 // VALIDATION
1688 replayError = null
1689
1690 // Check that action exists
1691 let selectedAction = actions.find(a => a.actionId == labelAction)
1692 if (!selectedAction) {
1693 replayError = new CLM.ReplayErrorActionUndefined(userText)
1694 replayErrors.push(replayError);
1695 }
1696 else {
1697 // Check action availability
1698 if (!this.isActionAvailable(selectedAction, scoreFilledEntities)) {
1699 replayError = new CLM.ReplayErrorActionUnavailable(userText)
1700 replayErrors.push(replayError);
1701 }
1702 }
1703
1704 // Check that action (if not first) is after a wait action
1705 if (scoreIndex > 0) {
1706 const lastScoredAction = round.scorerSteps[scoreIndex - 1].labelAction
1707 let lastAction = actions.find(a => a.actionId == lastScoredAction)
1708 if (lastAction && lastAction.isTerminal) {
1709 replayError = new CLM.ReplayErrorActionAfterWait()
1710 replayErrors.push(replayError);
1711 }
1712 }
1713
1714 // Generate bot response
1715 curAction = actions.filter((a: CLM.ActionBase) => a.actionId === labelAction)[0]
1716 let botResponse: IActionResult
1717
1718 // Check for exceptions on API call (specificaly EntityDetectionCallback)
1719 let logicAPIError = Utils.GetLogicAPIError(scorerStep.logicResult)
1720 if (logicAPIError) {
1721 replayError = new CLM.ReplayErrorAPIException()
1722 replayErrors.push(replayError);
1723
1724 let actionName = ""
1725 if (curAction.actionType === CLM.ActionTypes.API_LOCAL) {
1726 const apiAction = new CLM.ApiAction(curAction)
1727 actionName = `${apiAction.name}`
1728 }
1729 const title = `Exception hit in Bot's API Callback:${actionName}`;
1730 const response = this.RenderErrorCard(title, logicAPIError.APIError);
1731
1732 botResponse = {
1733 logicResult: undefined,
1734 response
1735 }
1736 }
1737 else if (!curAction) {
1738 botResponse = {
1739 logicResult: undefined,
1740 response: CLDebug.Error(`Can't find Action Id ${labelAction}`)
1741 }
1742 }
1743 else {
1744
1745 // Create map with names and ids
1746 let filledEntityMap = this.CreateFilledEntityMap(scoreFilledEntities, entityList)
1747
1748 // Fill in missing entities with a warning
1749 const missingEntities = this.PopulateMissingFilledEntities(curAction, filledEntityMap, entities, true)
1750
1751 // Entity required for Action isn't filled in
1752 if (missingEntities.length > 0) {
1753 replayError = replayError || new CLM.ReplayErrorEntityEmpty(missingEntities)
1754 replayErrors.push(replayError);
1755 }
1756
1757 // Set memory from map with names only (since not calling APIs)
1758 let memoryMap = CLM.FilledEntityMap.FromFilledEntities(scoreFilledEntities, entities)
1759 await clMemory.BotMemory.RestoreFromMapAsync(memoryMap)
1760
1761 if (curAction.actionType === CLM.ActionTypes.CARD) {
1762 const cardAction = new CLM.CardAction(curAction)
1763 botResponse = {
1764 logicResult: undefined,
1765 response: await this.TakeCardAction(cardAction, filledEntityMap)
1766 }
1767 } else if (curAction.actionType === CLM.ActionTypes.API_LOCAL) {
1768 const apiAction = new CLM.ApiAction(curAction)
1769 const actionInput: IActionInput = {
1770 type: ActionInputType.RENDER_ONLY,
1771 logicResult: scorerStep.logicResult
1772 }
1773
1774 botResponse = await this.TakeAPIAction(apiAction, filledEntityMap, clMemory, entityList.entities, true, actionInput)
1775
1776 if (!this.callbacks[apiAction.name]) {
1777 replayError = new CLM.ReplayErrorAPIUndefined(apiAction.name)
1778 replayErrors.push(replayError)
1779 }
1780 else if (botResponse.replayError) {
1781 replayError = botResponse.replayError
1782 replayErrors.push(botResponse.replayError)
1783 }
1784 } else if (curAction.actionType === CLM.ActionTypes.TEXT) {
1785 const textAction = new CLM.TextAction(curAction)
1786 try {
1787 botResponse = {
1788 logicResult: undefined,
1789 response: await this.TakeTextAction(textAction, filledEntityMap)
1790 }
1791 }
1792 catch (error) {
1793 // Payload is invalid
1794 replayError = new CLM.ReplayErrorEntityUndefined("")
1795 replayErrors.push(replayError);
1796 botResponse = {
1797 logicResult: undefined,
1798 response: JSON.parse(textAction.payload).text // Show raw text
1799 }
1800 }
1801 } else if (curAction.actionType === CLM.ActionTypes.END_SESSION) {
1802 const sessionAction = new CLM.SessionAction(curAction)
1803 botResponse = {
1804 logicResult: undefined,
1805 response: await this.TakeSessionAction(sessionAction, filledEntityMap, true, clMemory, null, null)
1806 }
1807 } else if (curAction.actionType === CLM.ActionTypes.SET_ENTITY) {
1808 const setEntityAction = new CLM.SetEntityAction(curAction)
1809 botResponse = await this.TakeSetEntityAction(setEntityAction, filledEntityMap, clMemory, entityList.entities, true)
1810 }
1811 else {
1812 throw new Error(`Cannot construct bot response for unknown action type: ${curAction.actionType}`)
1813 }
1814 }
1815
1816 let validWaitAction
1817 if (curAction && !curAction.isTerminal) {
1818 if (round.scorerSteps.length === scoreIndex + 1) {
1819 validWaitAction = false
1820 }
1821 else {
1822 validWaitAction = true
1823 }
1824 }
1825
1826 let clBotData: CLM.CLChannelData = {
1827 senderType: CLM.SenderType.Bot,
1828 roundIndex: roundNum,
1829 scoreIndex,
1830 validWaitAction: validWaitAction,
1831 replayError,
1832 activityIndex: activities.length
1833 }
1834
1835 let botActivity: Partial<BB.Activity> | null = null
1836 let botAccount = { id: `BOT-${userId}`, name: CLM.CL_USER_NAME_ID, role: BB.RoleTypes.Bot, aadObjectId: '' }
1837 if (typeof botResponse.response == 'string') {
1838 botActivity = {
1839 id: CLM.ModelUtils.generateGUID(),
1840 from: botAccount,
1841 type: 'message',
1842 text: botResponse.response,
1843 channelData: { clData: clBotData }
1844 }
1845 } else if (botResponse) {
1846 botActivity = botResponse.response as BB.Activity
1847 botActivity.id = CLM.ModelUtils.generateGUID()
1848 botActivity.from = botAccount
1849 botActivity.channelData = { clData: clBotData }
1850 }
1851
1852 if (botActivity) {
1853 activities.push(botActivity)
1854 }
1855 }
1856 }
1857 }
1858
1859 let memories = await clMemory.BotMemory.DumpMemory()
1860
1861 let hasRounds = trainDialog.rounds.length > 0;
1862 let hasScorerRound = (hasRounds && trainDialog.rounds[trainDialog.rounds.length - 1].scorerSteps.length > 0)
1863 let dialogMode = CLM.DialogMode.Scorer;
1864
1865 // If I have no rounds, I'm waiting for input
1866 if (!hasRounds) {
1867 dialogMode = CLM.DialogMode.Wait;
1868 }
1869 else if (curAction) {
1870 // If last action is session end
1871 if (curAction.actionType === CLM.ActionTypes.END_SESSION) {
1872 dialogMode = CLM.DialogMode.EndSession;
1873 }
1874 // If I have a scorer round, wait
1875 else if (curAction.isTerminal && hasScorerRound) {
1876 dialogMode = CLM.DialogMode.Wait;
1877 }
1878 }
1879
1880 // Calculate last extract response from text variations
1881 let uiScoreInput: CLM.UIScoreInput | undefined;
1882
1883 if (hasRounds) {
1884 // Note: Could potentially just send back extractorStep and calculate extractResponse on other end
1885 let textVariations = trainDialog.rounds[trainDialog.rounds.length - 1].extractorStep.textVariations;
1886 let extractResponses = CLM.ModelUtils.ToExtractResponses(textVariations);
1887 let trainExtractorStep = trainDialog.rounds[trainDialog.rounds.length - 1].extractorStep;
1888
1889 uiScoreInput = {
1890 trainExtractorStep: trainExtractorStep,
1891 extractResponse: extractResponses[0]
1892 } as CLM.UIScoreInput
1893 }
1894
1895 // Make errors unique using Set operator
1896 replayErrors = [...new Set(replayErrors)]
1897
1898 let teachWithHistory: CLM.TeachWithHistory = {
1899 teach: undefined,
1900 scoreInput: undefined,
1901 scoreResponse: undefined,
1902 uiScoreInput: uiScoreInput,
1903 extractResponse: undefined,
1904 lastAction: curAction,
1905 history: activities,
1906 memories: memories,
1907 prevMemories: prevMemories,
1908 dialogMode: dialogMode,
1909 replayErrors: replayErrors
1910 }
1911 return teachWithHistory
1912 }
1913
1914 private RenderSetEntityCard(name: string, value: string): Partial<BB.Activity> {
1915 const card = {
1916 type: "AdaptiveCard",
1917 version: "1.0",
1918 body: [
1919 {
1920 type: "Container",
1921 items: [
1922 {
1923 type: "TextBlock",
1924 text: `memory.Set(${name}, ${value})`,
1925 wrap: true
1926 }
1927 ]
1928 }]
1929 }
1930
1931 const attachment = BB.CardFactory.adaptiveCard(card)
1932 const message = BB.MessageFactory.attachment(attachment)
1933 message.text = "Set Entity:"
1934 return message;
1935 }
1936
1937 // Generate a card to show for an API action w/o output
1938 private RenderAPICard(callback: CLM.Callback, args: string[]): Partial<BB.Activity> {
1939
1940 let card = {
1941 type: "AdaptiveCard",
1942 version: "1.0",
1943 body: [
1944 {
1945 type: "Container",
1946 items: [
1947 {
1948 type: "TextBlock",
1949 text: `${callback.name}(${args.join(',')})`,
1950 wrap: true
1951 }
1952 ]
1953 }]
1954 }
1955
1956 const attachment = BB.CardFactory.adaptiveCard(card)
1957 const message = BB.MessageFactory.attachment(attachment)
1958 message.text = "API Call:"
1959 return message;
1960 }
1961
1962 // Generate a card to show for an API action w/o output
1963 private RenderErrorCard(title: string, error: string): Partial<BB.Activity> {
1964 let card = {
1965 type: "AdaptiveCard",
1966 version: "1.0",
1967 body: [
1968 {
1969 type: "Container",
1970 items: [
1971 {
1972 type: "TextBlock",
1973 text: error,
1974 wrap: true
1975 }
1976 ]
1977 }]
1978 }
1979 const attachment = BB.CardFactory.adaptiveCard(card)
1980 const message = BB.MessageFactory.attachment(attachment)
1981 message.text = title
1982 return message;
1983 }
1984
1985}
\No newline at end of file