UNPKG

85.8 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: ActionInputType
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 | 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 // Initizize 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 === undefined || 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 default:
934 throw new Error(`Could not find matching renderer for action type: ${clRecognizeResult.scoredAction.actionType}`)
935 }
936
937 // Convert string actions to activities
938 if (typeof actionResult.response === 'string') {
939 actionResult.response = BB.MessageFactory.text(actionResult.response)
940 }
941 if (actionResult.response && typeof actionResult.response !== 'string' && clData) {
942 actionResult.response.channelData = {
943 ...actionResult.response.channelData, clData: { ...clData, replayError: replayError || undefined }
944 }
945 }
946
947 // If action wasn't terminal loop through Conversation Learner again after a short delay
948 if (!clRecognizeResult.inTeach && !clRecognizeResult.scoredAction.isTerminal) {
949 if (app === null) {
950 app = await clRecognizeResult.memory.BotState.GetApp()
951 }
952 if (!app) {
953 throw new Error(`Attempted to get current app before app was set.`)
954 }
955
956 if (!conversationReference.conversation) {
957 throw new Error(`Attempted to get session by conversation id, but user was not defined on current conversation`)
958 }
959
960 if (sessionId == null) {
961 sessionId = await clRecognizeResult.memory.BotState.GetSessionIdAndSetConversationId(conversationReference.conversation.id)
962 }
963 if (!sessionId) {
964 throw new Error(`Attempted to get session by conversation id: ${conversationReference.conversation.id} but session was not found`)
965 }
966
967 // send the current response to user before score for the next turn
968 if (actionResult.response != null) {
969 await this.SendMessage(clRecognizeResult.memory, actionResult.response)
970 }
971 await delay(100)
972
973 let bestAction = await this.Score(
974 app.appId,
975 sessionId,
976 clRecognizeResult.memory,
977 '',
978 [],
979 clRecognizeResult.clEntities,
980 clRecognizeResult.inTeach,
981 true
982 )
983
984 clRecognizeResult.scoredAction = bestAction
985 actionResult = await this.TakeActionAsync(conversationReference, clRecognizeResult, clData)
986 }
987 return actionResult
988 }
989
990 public async SendIntent(intent: CLRecognizerResult, clData: CLM.CLChannelData | null = null): Promise<IActionResult | undefined> {
991
992 let conversationReference = await intent.memory.BotState.GetConversationReverence();
993
994 if (!conversationReference) {
995 CLDebug.Error('Missing ConversationReference')
996 return
997 }
998 if (!this.adapter) {
999 CLDebug.Error('Missing Adapter')
1000 return
1001 }
1002
1003 const actionResult = await this.TakeActionAsync(conversationReference, intent, clData)
1004
1005 if (actionResult.response != null) {
1006 const context = await this.GetTurnContext(intent.memory)
1007 await context.sendActivity(actionResult.response)
1008 }
1009
1010 return actionResult
1011 }
1012
1013 private async SendMessage(memory: CLMemory, message: string | Partial<BB.Activity>, incomingActivityId?: string | undefined): Promise<void> {
1014
1015 // If requested, pop incoming activity from message queue
1016 if (incomingActivityId) {
1017 await InputQueue.MessageHandled(memory.BotState, incomingActivityId);
1018 }
1019
1020 let conversationReference = await memory.BotState.GetConversationReverence()
1021 if (!conversationReference) {
1022 CLDebug.Error('Missing ConversationReference')
1023 return
1024 }
1025
1026 if (!this.adapter) {
1027 CLDebug.Error(`Attempted to send message before adapter was assigned`)
1028 return
1029 }
1030 const context = await this.GetTurnContext(memory)
1031 await context.sendActivity(message)
1032 }
1033
1034 // TODO: This issue arises because we only save non-null non-empty argument values on the actions
1035 // which means callback may accept more arguments than is actually available on the action.arguments
1036 // To me, it seems it would make more sense to always have these be same length, but perhaps there is
1037 // dependency on action not being defined somewhere else in the application like ActionCreatorEditor
1038 private GetRenderedArguments(fnArgs: string[], actionArgs: CLM.ActionArgument[], filledEntityMap: CLM.FilledEntityMap): string[] {
1039 const missingEntityNames: string[] = []
1040 const renderedArgumentValues = fnArgs.map(param => {
1041 const argument = actionArgs.find(arg => arg.parameter === param)
1042 if (!argument) {
1043 return ''
1044 }
1045
1046 try {
1047 return argument.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap))
1048 }
1049 catch (error) {
1050 missingEntityNames.push(param)
1051 return ''
1052 }
1053 }, missingEntityNames)
1054
1055 if (missingEntityNames.length > 0) {
1056 throw new Error(`Missing Entity value(s) for ${missingEntityNames.join(', ')}`)
1057 }
1058
1059 return renderedArgumentValues
1060 }
1061
1062 public async TakeAPIAction(apiAction: CLM.ApiAction, filledEntityMap: CLM.FilledEntityMap, clMemory: CLMemory, allEntities: CLM.EntityBase[], inTeach: boolean, actionInput: IActionInput): Promise<IActionResult> {
1063 // Extract API name and args
1064 const callback = this.callbacks[apiAction.name]
1065 if (!callback) {
1066 return {
1067 logicResult: undefined,
1068 response: `ERROR: API callback with name "${apiAction.name}" is not defined`
1069 }
1070 }
1071
1072 try {
1073 // Invoke Logic part of callback
1074 const renderedLogicArgumentValues = this.GetRenderedArguments(callback.logicArguments, apiAction.logicArguments, filledEntityMap)
1075 const memoryManager = await this.CreateMemoryManagerAsync(clMemory, allEntities)
1076 let replayError: CLM.ReplayError | null = null
1077
1078 // If we're only doing the render part, used stored values
1079 // This happens when replaying dialog to recreated action outputs
1080 let logicResult: CLM.LogicResult = { logicValue: undefined, changedFilledEntities: [] }
1081 if (actionInput.type === ActionInputType.RENDER_ONLY) {
1082 let storedResult = (actionInput as IActionInputLogic).logicResult
1083 logicResult = storedResult || logicResult
1084
1085 // Logic result holds delta from before after logic callback, use it to update memory
1086 memoryManager.curMemories.UpdateFilledEntities(logicResult.changedFilledEntities, allEntities)
1087
1088 // Update memory with changes from logic callback
1089 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
1090 }
1091 else {
1092 try {
1093 // create a copy of the map before calling into logic api
1094 // the copy of map is created because the passed infilledEntityMap contains "filledEntities by Id" too
1095 // and this causes issues when calculating changedFilledEntities.
1096 const entityMapBeforeCall = new CLM.FilledEntityMap(await clMemory.BotMemory.FilledEntityMap())
1097 // Store logic callback value
1098 const logicObject = await callback.logic(memoryManager, ...renderedLogicArgumentValues)
1099 logicResult.logicValue = JSON.stringify(logicObject)
1100 // Update memory with changes from logic callback
1101 await clMemory.BotMemory.RestoreFromMemoryManagerAsync(memoryManager)
1102 // Store changes to filled entities
1103 logicResult.changedFilledEntities = CLM.ModelUtils.changedFilledEntities(entityMapBeforeCall, memoryManager.curMemories)
1104 }
1105 catch (error) {
1106 let botAPIError: CLM.LogicAPIError = { APIError: error.stack || error.message || error }
1107 logicResult.logicValue = JSON.stringify(botAPIError)
1108 replayError = new CLM.ReplayErrorAPIException()
1109 }
1110 }
1111
1112 // Render the action unless only doing logic part
1113 if (actionInput.type === ActionInputType.LOGIC_ONLY) {
1114 return {
1115 logicResult,
1116 response: null,
1117 replayError: replayError || undefined
1118 }
1119 }
1120 else {
1121 let response: Partial<BB.Activity> | string | null = null
1122 let logicAPIError = Utils.GetLogicAPIError(logicResult)
1123
1124 // If there was an api Error show card to user
1125 if (logicAPIError) {
1126 const title = `Exception hit in Bot's API Callback: '${apiAction.name}'`
1127 response = this.RenderErrorCard(title, logicAPIError.APIError)
1128 }
1129 else if (logicResult.logicValue && !callback.render) {
1130 const title = `Malformed API Callback: '${apiAction.name}'`
1131 response = this.RenderErrorCard(title, "Logic portion of callback returns a value, but no Render portion defined")
1132 replayError = new CLM.ReplayErrorAPIMalformed()
1133 }
1134 else {
1135 // Invoke Render part of callback
1136 const renderedRenderArgumentValues = this.GetRenderedArguments(callback.renderArguments, apiAction.renderArguments, filledEntityMap)
1137
1138 const readOnlyMemoryManager = await this.CreateReadOnlyMemoryManagerAsync(clMemory, allEntities)
1139
1140 let logicObject = logicResult.logicValue ? JSON.parse(logicResult.logicValue) : undefined
1141 if (callback.render) {
1142 response = await callback.render(logicObject, readOnlyMemoryManager, ...renderedRenderArgumentValues)
1143 }
1144
1145 if (response && !Utils.IsCardValid(response)) {
1146 const title = `Malformed API Callback '${apiAction.name}'`
1147 const error = `Return value in Render function must be a string or BotBuilder Activity`
1148 response = this.RenderErrorCard(title, error)
1149 replayError = new CLM.ReplayErrorAPIBadCard()
1150 }
1151
1152 // If response is empty, but we're in teach session return a placeholder card in WebChat so they can click it to edit
1153 // Otherwise return the response as is.
1154 if (!response && inTeach) {
1155 response = this.RenderAPICard(callback, renderedLogicArgumentValues)
1156 }
1157 }
1158 return {
1159 logicResult,
1160 response,
1161 replayError: replayError || undefined
1162 }
1163 }
1164 }
1165 catch (err) {
1166 const title = `Exception hit in Bot's API Callback: '${apiAction.name}'`
1167 const message = this.RenderErrorCard(title, err.stack || err.message || "")
1168 const replayError = new CLM.ReplayErrorAPIException()
1169 return {
1170 logicResult: undefined,
1171 response: message,
1172 replayError
1173 }
1174 }
1175 }
1176
1177 public async TakeTextAction(textAction: CLM.TextAction, filledEntityMap: CLM.FilledEntityMap): Promise<Partial<BB.Activity> | string> {
1178 return Promise.resolve(textAction.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap)))
1179 }
1180
1181 public async TakeCardAction(cardAction: CLM.CardAction, filledEntityMap: CLM.FilledEntityMap): Promise<Partial<BB.Activity> | string> {
1182 try {
1183 const entityDisplayValues = CLM.getEntityDisplayValueMap(filledEntityMap)
1184 const renderedArguments = cardAction.renderArguments(entityDisplayValues)
1185
1186 const missingEntities = renderedArguments.filter(ra => ra.value === null);
1187 if (missingEntities.length > 0) {
1188 return `ERROR: Missing Entity value(s) for ${missingEntities.map(me => me.parameter).join(', ')}`;
1189 }
1190
1191 const form = await TemplateProvider.RenderTemplate(cardAction.templateName, renderedArguments)
1192
1193 if (form == null) {
1194 return CLDebug.Error(`Missing Template: ${cardAction.templateName}`)
1195 }
1196 const attachment = BB.CardFactory.adaptiveCard(form)
1197 const message = BB.MessageFactory.attachment(attachment)
1198 message.text = undefined
1199 return message
1200 } catch (error) {
1201 let msg = CLDebug.Error(error, 'Failed to Render Template')
1202 return msg
1203 }
1204 }
1205
1206 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> {
1207
1208 // Get any context from the action
1209 let content = sessionAction.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap))
1210
1211 // If inTeach, show something to user in WebChat so they can edit
1212 if (inTeach) {
1213 let payload = sessionAction.renderValue(CLM.getEntityDisplayValueMap(filledEntityMap))
1214 let card = {
1215 type: "AdaptiveCard",
1216 version: "1.0",
1217 body: [
1218 {
1219 type: "TextBlock",
1220 text: `EndSession: *${payload}*`
1221 }
1222 ]
1223 }
1224 const attachment = BB.CardFactory.adaptiveCard(card)
1225 const message = BB.MessageFactory.attachment(attachment)
1226 return message
1227 }
1228 // If I'm not in Teach end session.
1229 // (In Teach EndSession is handled in ScoreFeedback to keep session alive for TeachScoreFeedback)
1230 else {
1231 // End the current session (if in replay will be no sessionId or app)
1232 if (app && sessionId) {
1233 await this.clClient.EndSession(app.appId, sessionId)
1234 await this.EndSessionAsync(clMemory, CLM.SessionEndState.COMPLETED, content);
1235 }
1236 }
1237 return null
1238 }
1239
1240 // Returns true if Action is available given Entities in Memory
1241 public isActionAvailable(action: CLM.ActionBase, filledEntities: CLM.FilledEntity[]): boolean {
1242
1243 for (let entityId of action.requiredEntities) {
1244 let found = filledEntities.find(e => e.entityId == entityId);
1245 if (!found || found.values.length === 0) {
1246 return false;
1247 }
1248 }
1249 for (let entityId of action.negativeEntities) {
1250 let found = filledEntities.find(e => e.entityId == entityId);
1251 if (found && found.values.length > 0) {
1252 return false;
1253 }
1254 }
1255 return true;
1256 }
1257
1258 // Convert list of filled entities into a filled entity map lookup table
1259 private CreateFilledEntityMap(filledEntities: CLM.FilledEntity[], entityList: CLM.EntityList): CLM.FilledEntityMap {
1260 let filledEntityMap = new CLM.FilledEntityMap()
1261 for (let filledEntity of filledEntities) {
1262 let entity = entityList.entities.find(e => e.entityId == filledEntity.entityId)
1263 if (entity) {
1264 filledEntityMap.map[entity.entityName] = filledEntity
1265 filledEntityMap.map[entity.entityId] = filledEntity
1266 }
1267 }
1268 return filledEntityMap
1269 }
1270
1271 /**
1272 * Identify any validation issues
1273 * Missing Entities
1274 * Missing Actions
1275 * Unavailable Actions
1276 */
1277 public DialogValidationErrors(trainDialog: CLM.TrainDialog, entities: CLM.EntityBase[], actions: CLM.ActionBase[]): string[] {
1278
1279 let validationErrors: string[] = [];
1280
1281 for (let round of trainDialog.rounds) {
1282 let userText = round.extractorStep.textVariations[0].text;
1283 let filledEntities = round.scorerSteps[0] && round.scorerSteps[0].input ? round.scorerSteps[0].input.filledEntities : []
1284
1285 // Check that entities exist
1286 for (let filledEntity of filledEntities) {
1287 if (!entities.find(e => e.entityId == filledEntity.entityId)) {
1288 validationErrors.push(`Missing Entity for "${CLM.filledEntityValueAsString(filledEntity)}"`);
1289 }
1290 }
1291
1292 for (let scorerStep of round.scorerSteps) {
1293 let labelAction = scorerStep.labelAction
1294
1295 // Check that action exists
1296 let selectedAction = actions.find(a => a.actionId == labelAction)
1297 if (!selectedAction) {
1298 validationErrors.push(`Missing Action response for "${userText}"`);
1299 }
1300 else {
1301 // Check action availability
1302 if (!this.isActionAvailable(selectedAction, scorerStep.input.filledEntities)) {
1303 validationErrors.push(`Selected Action in unavailable in response to "${userText}"`);
1304 }
1305 }
1306 }
1307 }
1308 // Make errors unique using Set operator
1309 validationErrors = [...new Set(validationErrors)]
1310 return validationErrors;
1311 }
1312
1313 /** Return a list of trainDialogs that are invalid for the given set of entities and actions */
1314 public validateTrainDialogs(appDefinition: CLM.AppDefinition): string[] {
1315 let invalidTrainDialogIds = [];
1316 for (let trainDialog of appDefinition.trainDialogs) {
1317 // Ignore train dialogs that are already invalid
1318 if (trainDialog.validity !== CLM.Validity.INVALID) {
1319 let validationErrors = this.DialogValidationErrors(trainDialog, appDefinition.entities, appDefinition.actions);
1320 if (validationErrors.length > 0) {
1321 invalidTrainDialogIds.push(trainDialog.trainDialogId);
1322 }
1323 }
1324 }
1325 return invalidTrainDialogIds;
1326 }
1327
1328 /** Populate prebuilt information in predicted entities given filled entity array */
1329 private PopulatePrebuilts(predictedEntities: CLM.PredictedEntity[], filledEntities: CLM.FilledEntity[]) {
1330 for (let pe of predictedEntities) {
1331 let filledEnt = filledEntities.find(fe => fe.entityId === pe.entityId);
1332 if (filledEnt) {
1333 let value = filledEnt.values.find(v => v.userText === pe.entityText)
1334 if (value) {
1335 pe.resolution = value.resolution;
1336 if (value.builtinType) {
1337 pe.builtinType = value.builtinType;
1338 }
1339 }
1340 }
1341 }
1342 }
1343
1344 /**
1345 * Provide empty FilledEntity for any missing entities so they can still be rendered
1346 */
1347 private PopulateMissingFilledEntities(action: CLM.ActionBase, filledEntityMap: CLM.FilledEntityMap, allEntities: CLM.EntityBase[], bidirectional: boolean): string[] {
1348 // For backwards compatibiliity need to check requieredEntities too. In new version all in requiredEntitiesFromPayload
1349 const allRequiredEntities = [...action.requiredEntities, ...action.requiredEntitiesFromPayload]
1350 let missingEntities: string[] = []
1351
1352 allRequiredEntities.forEach((entityId: string) => {
1353 let entity = allEntities.find(e => e.entityId === entityId)
1354 if (entity) {
1355 if (!filledEntityMap.map[entity.entityName]) {
1356 // Add an empty filledEntity if requried and has no values
1357 let filledEntity = {
1358 entityId: entityId,
1359 values: []
1360 } as CLM.FilledEntity
1361 filledEntityMap.map[entity.entityId] = filledEntity
1362 if (bidirectional) {
1363 filledEntityMap.map[entity.entityName] = filledEntity
1364 }
1365 missingEntities.push(entity.entityName)
1366 }
1367 else {
1368 const filledEntity = filledEntityMap.map[entity.entityName]
1369 if (filledEntity && filledEntity.values.length == 0) {
1370 missingEntities.push(entity.entityName)
1371 }
1372 }
1373 } else {
1374 throw new Error(`ENTITY ${entityId} DOES NOT EXIST`)
1375 }
1376 })
1377 return missingEntities
1378 }
1379
1380 /**
1381 * Initialize memory for replay
1382 */
1383 private async InitReplayMemory(clMemory: CLMemory, trainDialog: CLM.TrainDialog, allEntities: CLM.EntityBase[]) {
1384
1385 // Reset the memory
1386 await clMemory.BotMemory.ClearAsync()
1387
1388 // Call start sesssion for initial entities
1389 await this.CheckSessionStartCallback(clMemory, allEntities);
1390 let startSessionEntities = await clMemory.BotMemory.FilledEntitiesAsync()
1391 startSessionEntities = [...trainDialog.initialFilledEntities || [], ...startSessionEntities]
1392
1393 let map = CLM.FilledEntityMap.FromFilledEntities(startSessionEntities, allEntities)
1394 await clMemory.BotMemory.RestoreFromMapAsync(map)
1395 }
1396
1397 /**
1398 * Replay a TrainDialog, calling EntityDetection callback and API Logic,
1399 * recalculating FilledEntities along the way
1400 */
1401 public async ReplayTrainDialogLogic(trainDialog: CLM.TrainDialog, clMemory: CLMemory, cleanse: boolean): Promise<CLM.TrainDialog> {
1402
1403 if (!trainDialog || !trainDialog.rounds) {
1404 return trainDialog
1405 }
1406
1407 // Copy train dialog
1408 let newTrainDialog: CLM.TrainDialog = JSON.parse(JSON.stringify(trainDialog))
1409
1410 let entities: CLM.EntityBase[] = trainDialog.definitions ? trainDialog.definitions.entities : []
1411 let actions: CLM.ActionBase[] = trainDialog.definitions ? trainDialog.definitions.actions : []
1412 let entityList: CLM.EntityList = { entities }
1413
1414 await this.InitReplayMemory(clMemory, newTrainDialog, entities)
1415
1416 for (let round of newTrainDialog.rounds) {
1417
1418 // Call entity detection callback with first text Variation
1419 let textVariation = round.extractorStep.textVariations[0]
1420 let predictedEntities = CLM.ModelUtils.ToPredictedEntities(textVariation.labelEntities)
1421
1422 // Call EntityDetectionCallback and populate filledEntities with the result
1423 let scoreInput: CLM.ScoreInput
1424 let botAPIError: CLM.LogicAPIError | null = null
1425 try {
1426 scoreInput = await this.CallEntityDetectionCallback(textVariation.text, predictedEntities, clMemory, entities)
1427 }
1428 catch (err) {
1429 // Hit exception in Bot's Entity Detection Callback
1430 // Use existing memory before callback
1431 const filledEntities = await clMemory.BotMemory.FilledEntitiesAsync()
1432 scoreInput = {
1433 filledEntities,
1434 context: {},
1435 maskedActions: []
1436 }
1437
1438 // Create error to show to user
1439 let errMessage = `${CLStrings.ENTITYCALLBACK_EXCEPTION} ${err.message}`
1440 botAPIError = { APIError: errMessage }
1441 }
1442
1443 // Use scorer step to populate pre-built data (when)
1444 if (round.scorerSteps && round.scorerSteps.length > 0) {
1445 this.PopulatePrebuilts(predictedEntities, scoreInput.filledEntities)
1446 round.scorerSteps[0].input.filledEntities = scoreInput.filledEntities
1447
1448 // Go through each scorer step
1449 for (let [scoreIndex, scorerStep] of round.scorerSteps.entries()) {
1450
1451 let curAction = actions.filter((a: CLM.ActionBase) => a.actionId === scorerStep.labelAction)[0]
1452 if (curAction) {
1453
1454 let filledEntityMap = await clMemory.BotMemory.FilledEntityMap()
1455
1456 // Provide empty FilledEntity for missing entities
1457 if (!cleanse) {
1458 this.PopulateMissingFilledEntities(curAction, filledEntityMap, entities, false)
1459 }
1460
1461 round.scorerSteps[scoreIndex].input.filledEntities = filledEntityMap.FilledEntities()
1462
1463 // Run logic part of APIAction to update the FilledEntities
1464 if (curAction.actionType === CLM.ActionTypes.API_LOCAL) {
1465 const apiAction = new CLM.ApiAction(curAction)
1466 const actionInput: IActionInput = {
1467 type: ActionInputType.LOGIC_ONLY
1468 }
1469 // Calculate and store new logic result
1470 const filledIdMap = filledEntityMap.EntityMapToIdMap()
1471 let actionResult = await this.TakeAPIAction(apiAction, filledIdMap, clMemory, entityList.entities, true, actionInput)
1472 round.scorerSteps[scoreIndex].logicResult = actionResult.logicResult
1473 } else if (curAction.actionType === CLM.ActionTypes.END_SESSION) {
1474 const sessionAction = new CLM.SessionAction(curAction)
1475 await this.TakeSessionAction(sessionAction, filledEntityMap, true, clMemory, null, null)
1476 }
1477 }
1478
1479 // If ran into API error inject into first scorer step so it gets displayed to the user
1480 if (botAPIError && scoreIndex === 0) {
1481 round.scorerSteps[scoreIndex].logicResult = { logicValue: JSON.stringify(botAPIError), changedFilledEntities: [] }
1482 }
1483 }
1484 }
1485 else {
1486 // Otherwise create a dummy scorer step with the filled entities
1487 const scorerStep: CLM.TrainScorerStep = {
1488 input: {
1489 filledEntities: await clMemory.BotMemory.FilledEntitiesAsync(),
1490 context: {},
1491 maskedActions: []
1492 },
1493 labelAction: undefined,
1494 logicResult: undefined,
1495 scoredAction: undefined
1496 }
1497 if (!round.scorerSteps) {
1498 round.scorerSteps = []
1499 }
1500 round.scorerSteps.push(scorerStep)
1501 }
1502 }
1503
1504 // When editing, may need to run Scorer or Extrator on TrainDialog with invalid rounds
1505 //This cleans up the TrainDialog removing bad data so the extractor can run
1506 if (cleanse) {
1507 // Remove rounds with two user inputs in a row (they'll have a dummy scorer round)
1508 newTrainDialog.rounds = newTrainDialog.rounds.filter(r => {
1509 return !r.scorerSteps[0] || r.scorerSteps[0].labelAction != undefined
1510 })
1511
1512 }
1513 return newTrainDialog
1514 }
1515
1516 /**
1517 * Get Activities generated by trainDialog.
1518 * Return any errors in TrainDialog
1519 * NOTE: Will set bot memory to state at end of history
1520 */
1521 public async GetHistory(trainDialog: CLM.TrainDialog, userName: string, userId: string, clMemory: CLMemory): Promise<CLM.TeachWithHistory | null> {
1522
1523 let entities: CLM.EntityBase[] = trainDialog.definitions ? trainDialog.definitions.entities : []
1524 let actions: CLM.ActionBase[] = trainDialog.definitions ? trainDialog.definitions.actions : []
1525 let entityList: CLM.EntityList = { entities }
1526 let prevMemories: CLM.Memory[] = []
1527
1528 if (!trainDialog || !trainDialog.rounds) {
1529 return null
1530 }
1531
1532 await this.InitReplayMemory(clMemory, trainDialog, entities)
1533
1534 let excludedEntities = entities.filter(e => e.doNotMemorize).map(e => e.entityId)
1535 let activities: Partial<BB.Activity>[] = []
1536 let replayError: CLM.ReplayError | null = null
1537 let replayErrors: CLM.ReplayError[] = [];
1538 let curAction = null
1539
1540 for (let [roundNum, round] of trainDialog.rounds.entries()) {
1541 let filledEntities = round.scorerSteps[0] && round.scorerSteps[0].input ? round.scorerSteps[0].input.filledEntities : []
1542
1543 // VALIDATION
1544 replayError = null
1545
1546 // Check that non-multivalue isn't labelled twice
1547 for (let tv of round.extractorStep.textVariations) {
1548 let usedEntities: string[] = []
1549 for (let labelEntity of tv.labelEntities) {
1550 // If already used, make sure it's multi-value
1551 if (usedEntities.find(e => e === labelEntity.entityId)) {
1552 let entity = entities.find(e => e.entityId == labelEntity.entityId)
1553 if (entity && !entity.isMultivalue
1554 && (entity.entityType === CLM.EntityType.LUIS || entity.entityType === CLM.EntityType.LOCAL)) {
1555 replayError = replayError || new CLM.EntityUnexpectedMultivalue(entity.entityName)
1556 replayErrors.push(replayError);
1557 }
1558 }
1559 // Otherwise add to list of used entities
1560 else {
1561 usedEntities.push(labelEntity.entityId)
1562 }
1563 }
1564 }
1565
1566 // Check that entities exist in text variations
1567 for (let tv of round.extractorStep.textVariations) {
1568 for (let labelEntity of tv.labelEntities) {
1569 if (!entities.find(e => e.entityId == labelEntity.entityId)) {
1570 replayError = new CLM.ReplayErrorEntityUndefined(labelEntity.entityId)
1571 replayErrors.push()
1572 }
1573 }
1574 }
1575
1576 // Check that entities exist in filled entities
1577 for (let filledEntity of filledEntities) {
1578 if (!entities.find(e => e.entityId == filledEntity.entityId)) {
1579 replayError = new CLM.ReplayErrorEntityUndefined(CLM.filledEntityValueAsString(filledEntity))
1580 replayErrors.push()
1581 }
1582 }
1583
1584 // Check for double user inputs
1585 if (roundNum != trainDialog.rounds.length - 1 &&
1586 (round.scorerSteps.length === 0 || !round.scorerSteps[0].labelAction)) {
1587 replayError = new CLM.ReplayErrorTwoUserInputs()
1588 replayErrors.push(replayError)
1589 }
1590
1591 // Check for user input when previous action wasn't wait
1592 if (curAction && !curAction.isTerminal) {
1593 replayError = new CLM.ReplayErrorInputAfterNonWait()
1594 replayErrors.push(replayError)
1595 }
1596
1597 // Generate activity. Add markdown to highlight labelled entities
1598 let userText = CLM.ModelUtils.textVariationToMarkdown(round.extractorStep.textVariations[0], excludedEntities)
1599 let userActivity: Partial<BB.Activity> = CLM.ModelUtils.InputToActivity(userText, userName, userId, roundNum)
1600 userActivity.channelData.clData.replayError = replayError
1601 userActivity.channelData.clData.activityIndex = activities.length
1602 userActivity.textFormat = 'markdown'
1603 activities.push(userActivity)
1604
1605 // Save memory before this step (used to show changes in UI)
1606 prevMemories = await clMemory.BotMemory.DumpMemory()
1607
1608 let textVariation = round.extractorStep.textVariations[0]
1609 let predictedEntities = CLM.ModelUtils.ToPredictedEntities(textVariation.labelEntities)
1610
1611 // Use scorer step to populate pre-built data (when)
1612 if (round.scorerSteps.length > 0) {
1613 this.PopulatePrebuilts(predictedEntities, round.scorerSteps[0].input.filledEntities)
1614 }
1615
1616 for (let [scoreIndex, scorerStep] of round.scorerSteps.entries()) {
1617
1618 let labelAction = scorerStep.labelAction
1619
1620 // Scorer rounds w/o labelActions may exist to store extraction result for rendering
1621 if (labelAction) {
1622
1623 let scoreFilledEntities = scorerStep.input.filledEntities
1624
1625 // VALIDATION
1626 replayError = null
1627
1628 // Check that action exists
1629 let selectedAction = actions.find(a => a.actionId == labelAction)
1630 if (!selectedAction) {
1631 replayError = new CLM.ReplayErrorActionUndefined(userText)
1632 replayErrors.push(replayError);
1633 }
1634 else {
1635 // Check action availability
1636 if (!this.isActionAvailable(selectedAction, scoreFilledEntities)) {
1637 replayError = new CLM.ReplayErrorActionUnavailable(userText)
1638 replayErrors.push(replayError);
1639 }
1640 }
1641
1642 // Check that action (if not first) is after a wait action
1643 if (scoreIndex > 0) {
1644 const lastScoredAction = round.scorerSteps[scoreIndex - 1].labelAction
1645 let lastAction = actions.find(a => a.actionId == lastScoredAction)
1646 if (lastAction && lastAction.isTerminal) {
1647 replayError = new CLM.ReplayErrorActionAfterWait()
1648 replayErrors.push(replayError);
1649 }
1650 }
1651
1652 // Generate bot response
1653 curAction = actions.filter((a: CLM.ActionBase) => a.actionId === labelAction)[0]
1654 let botResponse: IActionResult
1655
1656 // Check for exceptions on API call (specificaly EntityDetectionCallback)
1657 let logicAPIError = Utils.GetLogicAPIError(scorerStep.logicResult)
1658 if (logicAPIError) {
1659 replayError = new CLM.ReplayErrorAPIException()
1660 replayErrors.push(replayError);
1661
1662 let actionName = ""
1663 if (curAction.actionType === CLM.ActionTypes.API_LOCAL) {
1664 const apiAction = new CLM.ApiAction(curAction)
1665 actionName = `${apiAction.name}`
1666 }
1667 const title = `Exception hit in Bot's API Callback:${actionName}`;
1668 const response = this.RenderErrorCard(title, logicAPIError.APIError);
1669
1670 botResponse = {
1671 logicResult: undefined,
1672 response
1673 }
1674 }
1675 else if (!curAction) {
1676 botResponse = {
1677 logicResult: undefined,
1678 response: CLDebug.Error(`Can't find Action Id ${labelAction}`)
1679 }
1680 }
1681 else {
1682
1683 // Create map with names and ids
1684 let filledEntityMap = this.CreateFilledEntityMap(scoreFilledEntities, entityList)
1685
1686 // Fill in missing entities with a warning
1687 const missingEntities = this.PopulateMissingFilledEntities(curAction, filledEntityMap, entities, true)
1688
1689 // Entity required for Action isn't filled in
1690 if (missingEntities.length > 0) {
1691 replayError = replayError || new CLM.ReplayErrorEntityEmpty(missingEntities)
1692 replayErrors.push(replayError);
1693 }
1694
1695 // Set memory from map with names only (since not calling APIs)
1696 let memoryMap = CLM.FilledEntityMap.FromFilledEntities(scoreFilledEntities, entities)
1697 await clMemory.BotMemory.RestoreFromMapAsync(memoryMap)
1698
1699 if (curAction.actionType === CLM.ActionTypes.CARD) {
1700 const cardAction = new CLM.CardAction(curAction)
1701 botResponse = {
1702 logicResult: undefined,
1703 response: await this.TakeCardAction(cardAction, filledEntityMap)
1704 }
1705 } else if (curAction.actionType === CLM.ActionTypes.API_LOCAL) {
1706 const apiAction = new CLM.ApiAction(curAction)
1707 const actionInput: IActionInput = {
1708 type: ActionInputType.RENDER_ONLY,
1709 logicResult: scorerStep.logicResult
1710 }
1711
1712 botResponse = await this.TakeAPIAction(apiAction, filledEntityMap, clMemory, entityList.entities, true, actionInput)
1713
1714 if (!this.callbacks[apiAction.name]) {
1715 replayError = new CLM.ReplayErrorAPIUndefined(apiAction.name)
1716 replayErrors.push(replayError)
1717 }
1718 else if (botResponse.replayError) {
1719 replayError = botResponse.replayError
1720 replayErrors.push(botResponse.replayError)
1721 }
1722 } else if (curAction.actionType === CLM.ActionTypes.TEXT) {
1723 const textAction = new CLM.TextAction(curAction)
1724 try {
1725 botResponse = {
1726 logicResult: undefined,
1727 response: await this.TakeTextAction(textAction, filledEntityMap)
1728 }
1729 }
1730 catch (error) {
1731 // Payload is invalid
1732 replayError = new CLM.ReplayErrorEntityUndefined("")
1733 replayErrors.push(replayError);
1734 botResponse = {
1735 logicResult: undefined,
1736 response: JSON.parse(textAction.payload).text // Show raw text
1737 }
1738 }
1739 } else if (curAction.actionType === CLM.ActionTypes.END_SESSION) {
1740 const sessionAction = new CLM.SessionAction(curAction)
1741 botResponse = {
1742 logicResult: undefined,
1743 response: await this.TakeSessionAction(sessionAction, filledEntityMap, true, clMemory, null, null)
1744 }
1745 }
1746 else {
1747 throw new Error(`Cannot construct bot response for unknown action type: ${curAction.actionType}`)
1748 }
1749 }
1750
1751 let validWaitAction
1752 if (curAction && !curAction.isTerminal) {
1753 if (round.scorerSteps.length === scoreIndex + 1) {
1754 validWaitAction = false
1755 }
1756 else {
1757 validWaitAction = true
1758 }
1759 }
1760
1761 let clBotData: CLM.CLChannelData = {
1762 senderType: CLM.SenderType.Bot,
1763 roundIndex: roundNum,
1764 scoreIndex,
1765 validWaitAction: validWaitAction,
1766 replayError,
1767 activityIndex: activities.length
1768 }
1769
1770 let botActivity: Partial<BB.Activity> | null = null
1771 let botAccount = { id: `BOT-${userId}`, name: CLM.CL_USER_NAME_ID, role: BB.RoleTypes.Bot, aadObjectId: '' }
1772 if (typeof botResponse.response == 'string') {
1773 botActivity = {
1774 id: CLM.ModelUtils.generateGUID(),
1775 from: botAccount,
1776 type: 'message',
1777 text: botResponse.response,
1778 channelData: { clData: clBotData }
1779 }
1780 } else if (botResponse) {
1781 botActivity = botResponse.response as BB.Activity
1782 botActivity.id = CLM.ModelUtils.generateGUID()
1783 botActivity.from = botAccount
1784 botActivity.channelData = { clData: clBotData }
1785 }
1786
1787 if (botActivity) {
1788 activities.push(botActivity)
1789 }
1790 }
1791 }
1792 }
1793
1794 let memories = await clMemory.BotMemory.DumpMemory()
1795
1796 let hasRounds = trainDialog.rounds.length > 0;
1797 let hasScorerRound = (hasRounds && trainDialog.rounds[trainDialog.rounds.length - 1].scorerSteps.length > 0)
1798 let dialogMode = CLM.DialogMode.Scorer;
1799
1800 // If I have no rounds, I'm waiting for input
1801 if (!hasRounds) {
1802 dialogMode = CLM.DialogMode.Wait;
1803 }
1804 else if (curAction) {
1805 // If last action is session end
1806 if (curAction.actionType === CLM.ActionTypes.END_SESSION) {
1807 dialogMode = CLM.DialogMode.EndSession;
1808 }
1809 // If I have a scorer round, wait
1810 else if (curAction.isTerminal && hasScorerRound) {
1811 dialogMode = CLM.DialogMode.Wait;
1812 }
1813 }
1814
1815 // Calculate last extract response from text variations
1816 let uiScoreInput: CLM.UIScoreInput | undefined;
1817
1818 if (hasRounds) {
1819 // Note: Could potentially just send back extractorStep and calculate extractResponse on other end
1820 let textVariations = trainDialog.rounds[trainDialog.rounds.length - 1].extractorStep.textVariations;
1821 let extractResponses = CLM.ModelUtils.ToExtractResponses(textVariations);
1822 let trainExtractorStep = trainDialog.rounds[trainDialog.rounds.length - 1].extractorStep;
1823
1824 uiScoreInput = {
1825 trainExtractorStep: trainExtractorStep,
1826 extractResponse: extractResponses[0]
1827 } as CLM.UIScoreInput
1828 }
1829
1830 // Make errors unique using Set operator
1831 replayErrors = [...new Set(replayErrors)]
1832
1833 let teachWithHistory: CLM.TeachWithHistory = {
1834 teach: undefined,
1835 scoreInput: undefined,
1836 scoreResponse: undefined,
1837 uiScoreInput: uiScoreInput,
1838 extractResponse: undefined,
1839 lastAction: curAction,
1840 history: activities,
1841 memories: memories,
1842 prevMemories: prevMemories,
1843 dialogMode: dialogMode,
1844 replayErrors: replayErrors
1845 }
1846 return teachWithHistory
1847 }
1848
1849 // Generate a card to show for an API action w/o output
1850 private RenderAPICard(callback: CLM.Callback, args: string[]): Partial<BB.Activity> {
1851
1852 let card = {
1853 type: "AdaptiveCard",
1854 version: "1.0",
1855 body: [
1856 {
1857 type: "Container",
1858 items: [
1859 {
1860 type: "TextBlock",
1861 text: `${callback.name}(${args.join(',')})`,
1862 wrap: true
1863 }
1864 ]
1865 }]
1866 }
1867
1868 const attachment = BB.CardFactory.adaptiveCard(card)
1869 const message = BB.MessageFactory.attachment(attachment)
1870 message.text = "API Call:"
1871 return message;
1872 }
1873
1874 // Generate a card to show for an API action w/o output
1875 private RenderErrorCard(title: string, error: string): Partial<BB.Activity> {
1876 let card = {
1877 type: "AdaptiveCard",
1878 version: "1.0",
1879 body: [
1880 {
1881 type: "Container",
1882 items: [
1883 {
1884 type: "TextBlock",
1885 text: error,
1886 wrap: true
1887 }
1888 ]
1889 }]
1890 }
1891 const attachment = BB.CardFactory.adaptiveCard(card)
1892 const message = BB.MessageFactory.attachment(attachment)
1893 message.text = title
1894 return message;
1895 }
1896
1897}
\No newline at end of file