@servicetitan/titan-chatbot-api 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,14 +67,14 @@ describe('[ChatbotUiBackendStore]', () => {
67
67
 
68
68
  test('should subscribe on external events', () => {
69
69
  store.subscribe();
70
- expect(spyOn).toHaveBeenCalledTimes(7);
71
- expect(spyOff).toHaveBeenCalledTimes(7);
70
+ expect(spyOn).toHaveBeenCalledTimes(8);
71
+ expect(spyOff).toHaveBeenCalledTimes(8);
72
72
  });
73
73
 
74
74
  test('should unsubscribe from external events', () => {
75
75
  store.unsubscribe();
76
76
  expect(chatUiStore.on).toHaveBeenCalledTimes(0);
77
- expect(chatUiStore.off).toHaveBeenCalledTimes(7);
77
+ expect(chatUiStore.off).toHaveBeenCalledTimes(8);
78
78
  });
79
79
  });
80
80
 
@@ -259,6 +259,70 @@ describe('[ChatbotUiBackendStore]', () => {
259
259
  });
260
260
  });
261
261
 
262
+ describe('with session start', () => {
263
+ const createSession = (id: number) =>
264
+ ModelsMocks.mockSession({
265
+ id,
266
+ data: { custom: 'data' },
267
+ });
268
+
269
+ test('should start session', async () => {
270
+ chatbotApi.postSession.mockResolvedValue(createSession(1));
271
+ const { session } = await runChatUiEventListener(store.handleSessionStart, [
272
+ { custom: 'data' },
273
+ ]);
274
+
275
+ expect(session).toEqual(createSession(1));
276
+ expect(chatbotApi.postSession).toHaveBeenCalledWith(
277
+ { custom: 'data' },
278
+ expect.any(AbortSignal)
279
+ );
280
+ });
281
+
282
+ test('should handle multiple calls', async () => {
283
+ chatbotApi.postSession.mockResolvedValue(createSession(1));
284
+ const p1 = runChatUiEventListener(store.handleSessionStart, [{ custom: 'data' }]);
285
+ const p2 = runChatUiEventListener(store.handleSessionStart, [{ custom: 'data' }]);
286
+
287
+ const [r1, r2] = await Promise.all([p1, p2]);
288
+
289
+ expect(r1.session).toEqual(createSession(1));
290
+ expect(r2.session).toEqual(createSession(1));
291
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(1);
292
+ expect(chatbotApi.postSession).toHaveBeenCalledWith(
293
+ { custom: 'data' },
294
+ expect.any(AbortSignal)
295
+ );
296
+ });
297
+
298
+ test('should handle forced calls', async () => {
299
+ chatbotApi.postSession.mockResolvedValue(createSession(1));
300
+ const { session } = await runChatUiEventListener(store.handleSessionStart, [
301
+ { custom: 'data' },
302
+ ]);
303
+
304
+ chatbotApi.postSession.mockResolvedValue(createSession(2));
305
+ const { session: session2 } = await runChatUiEventListener(
306
+ store.handleSessionStart,
307
+ [{ custom: 'data' }]
308
+ );
309
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(1);
310
+ expect(session?.id).toEqual(1);
311
+ expect(session2?.id).toEqual(1);
312
+ expect(session === session2).toBe(true);
313
+
314
+ // Make api call return another session object and ensure that after
315
+ chatbotApi.postSession.mockResolvedValue(createSession(3));
316
+ const { session: session3 } = await runChatUiEventListener(
317
+ store.handleSessionStart,
318
+ [{ custom: 'data' }, true]
319
+ );
320
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(2);
321
+ expect(session3?.id).toEqual(3);
322
+ expect(session === session3).toBe(false);
323
+ });
324
+ });
325
+
262
326
  describe('with session feedback', () => {
263
327
  const createSessionFeedback = () =>
264
328
  ModelsMocks.mockFeedback({
@@ -57,6 +57,27 @@ describe('[ChatbotUiStore]', () => {
57
57
  });
58
58
 
59
59
  describe('with chat events', () => {
60
+ test('should startSession', async () => {
61
+ mockEmit();
62
+ mockEventEmitter.emit.mockImplementation(
63
+ async (eventName: string, resolve: (session: Models.ISession) => void) => {
64
+ await Promise.resolve();
65
+ resolve(ModelsMocks.mockSession());
66
+ }
67
+ );
68
+ const sessionData = ModelsMocks.mockSession({ id: 1 });
69
+ const session = await store.startSession(sessionData, true);
70
+ expect(session).toBeDefined();
71
+ expect(session?.id).toBe(1);
72
+ expect(mockEventEmitter.emit).toHaveBeenCalledWith(
73
+ ChatbotUiEvent.eventChatbotSessionStart,
74
+ expect.any(Function),
75
+ expect.any(Function),
76
+ sessionData,
77
+ true
78
+ );
79
+ });
80
+
60
81
  test('should sendSessionFeedback', async () => {
61
82
  mockEmit();
62
83
  const feedback = new Models.Feedback({
@@ -59,6 +59,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
59
59
  this.chatUiStore.on(ChatUiEvent.eventRestart, this.handleRestart);
60
60
  this.chatUiStore.on(ChatUiEvent.eventMessageSend, this.handleMessageSend);
61
61
  this.chatUiStore.on(ChatUiEvent.eventMessageSendRetry, this.handleMessageSendRetry);
62
+ this.chatUiStore.on(ChatbotUiEvent.eventChatbotSessionStart, this.handleSessionStart);
62
63
  this.chatUiStore.on(ChatbotUiEvent.eventChatbotSessionFeedback, this.handleSessionFeedback);
63
64
  this.chatUiStore.on(ChatbotUiEvent.eventChatbotMessageFeedback, this.handleMessageFeedback);
64
65
  }
@@ -68,6 +69,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
68
69
  this.chatUiStore.off(ChatUiEvent.eventRestart, this.handleRestart);
69
70
  this.chatUiStore.off(ChatUiEvent.eventMessageSend, this.handleMessageSend);
70
71
  this.chatUiStore.off(ChatUiEvent.eventMessageSendRetry, this.handleMessageSendRetry);
72
+ this.chatUiStore.off(ChatbotUiEvent.eventChatbotSessionStart, this.handleSessionStart);
71
73
  this.chatUiStore.off(
72
74
  ChatbotUiEvent.eventChatbotSessionFeedback,
73
75
  this.handleSessionFeedback
@@ -119,6 +121,15 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
119
121
  resolve();
120
122
  };
121
123
 
124
+ handleSessionStart: ChatUiEventListener<{
125
+ session?: Models.ISession;
126
+ }> = async (resolve, _, sessionData?: Models.ISession, forceRecreate?: boolean) => {
127
+ const session = await this.startSession(sessionData, forceRecreate);
128
+ resolve({
129
+ session,
130
+ });
131
+ };
132
+
122
133
  handleSessionFeedback: ChatUiEventListener<{
123
134
  state: Models.ChatbotFeedbackState;
124
135
  feedback?: Models.Feedback;
@@ -136,7 +147,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
136
147
  recoverStrategy: false,
137
148
  });
138
149
  }
139
- await this.ensureSession();
150
+ await this.startSession();
140
151
  runInAction(() => {
141
152
  feedback.sessionId = this.session!.id;
142
153
  });
@@ -174,7 +185,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
174
185
  recoverStrategy: false,
175
186
  });
176
187
  }
177
- await this.ensureSession();
188
+ await this.startSession();
178
189
  runInAction(() => {
179
190
  feedback.sessionId = this.session!.id;
180
191
  });
@@ -265,7 +276,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
265
276
 
266
277
  private async sendMessage(messageModel: ChatMessageModelText, selections?: Models.Selections) {
267
278
  try {
268
- await this.ensureSession();
279
+ await this.startSession();
269
280
  this.chatUiStore.setAgentTyping(true);
270
281
  const answer = await withTimeout(
271
282
  this.api(
@@ -314,12 +325,20 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
314
325
  this.session = session;
315
326
  }
316
327
 
317
- private async ensureSession() {
328
+ private async startSession(sessionData?: Models.ISession, forceRecreate = false) {
329
+ if (forceRecreate) {
330
+ this.reset();
331
+ this.sessionPromise = undefined;
332
+ }
318
333
  if (this.session) {
319
- return;
334
+ return this.session;
320
335
  }
321
336
  if (!this.sessionPromise) {
322
- this.sessionPromise = this.api('postSession', ModelsUtils.createNewSessionModel());
337
+ this.sessionPromise = this.api(
338
+ 'postSession',
339
+ ModelsUtils.createNewSessionModel(sessionData),
340
+ this.abortController.signal
341
+ );
323
342
  const session = await this.sessionPromise;
324
343
  this.setSession(session);
325
344
  this.withSaveState(() => {
@@ -331,6 +350,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
331
350
  } finally {
332
351
  this.sessionPromise = undefined;
333
352
  }
353
+ return this.session;
334
354
  }
335
355
 
336
356
  private async api<
@@ -8,6 +8,7 @@ import { FilterStore } from './filter.store';
8
8
  export const CHATBOT_UI_STORE_TOKEN = symbolToken<IChatbotUiStore>('CHATBOT_UI_STORE_TOKEN');
9
9
 
10
10
  export enum ChatbotUiEvent {
11
+ eventChatbotSessionStart = 'eventChatbotSessionStart',
11
12
  eventChatbotSessionFeedback = 'eventChatbotSessionFeedback',
12
13
  eventChatbotMessageFeedback = 'eventChatbotMessageFeedback',
13
14
  }
@@ -16,13 +17,17 @@ export interface IChatbotUiStore extends IChatUiStore<ChatbotCustomizations> {
16
17
  filterStore: FilterStore;
17
18
  settings?: Models.IFrontendModel;
18
19
  setFilters: (filterOptions: Models.IFrontendModel) => void;
20
+ startSession: (
21
+ sessionData?: Models.ISession,
22
+ forceRecreate?: boolean
23
+ ) => Promise<undefined | Models.Session>;
19
24
  sendSessionFeedback: (
20
25
  feedback: Models.IFeedback
21
- ) => Promise<void | Models.ChatbotFeedbackState>;
26
+ ) => Promise<undefined | Models.ChatbotFeedbackState>;
22
27
  sendMessageFeedback: (
23
28
  message: Models.IBotMessageWithFeedback,
24
29
  messageFeedback: Models.IFeedback
25
- ) => Promise<void | Models.ChatbotFeedbackState>;
30
+ ) => Promise<undefined | Models.ChatbotFeedbackState>;
26
31
  }
27
32
 
28
33
  @injectable()
@@ -58,6 +63,16 @@ export class ChatbotUiStore extends ChatUiStore<ChatbotCustomizations> implement
58
63
  this.filterStore.initFilters(filterOptions);
59
64
  }
60
65
 
66
+ @action
67
+ async startSession(sessionData?: Models.ISession, forceRecreate?: boolean) {
68
+ const result = await this.emitAsync<Models.Session>(
69
+ ChatbotUiEvent.eventChatbotSessionStart,
70
+ sessionData,
71
+ forceRecreate
72
+ );
73
+ return result;
74
+ }
75
+
61
76
  @action
62
77
  async sendSessionFeedback(feedback: Models.Feedback) {
63
78
  feedback.messageId = undefined; // Ensure messageId is not set for session feedback