@servicetitan/titan-chatbot-api 6.0.0 → 6.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.
- package/CHANGELOG.md +13 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.test.js +98 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.test.js.map +1 -1
- package/dist/stores/__tests__/chatbot-ui.store.test.js +9 -0
- package/dist/stores/__tests__/chatbot-ui.store.test.js.map +1 -1
- package/dist/stores/chatbot-ui-backend.store.d.ts +1 -1
- package/dist/stores/chatbot-ui-backend.store.d.ts.map +1 -1
- package/dist/stores/chatbot-ui-backend.store.js +12 -6
- package/dist/stores/chatbot-ui-backend.store.js.map +1 -1
- package/dist/stores/chatbot-ui.store.d.ts +10 -5
- package/dist/stores/chatbot-ui.store.d.ts.map +1 -1
- package/dist/stores/chatbot-ui.store.js +2 -2
- package/dist/stores/chatbot-ui.store.js.map +1 -1
- package/package.json +3 -3
- package/src/stores/__tests__/chatbot-ui-backend.store.test.ts +177 -0
- package/src/stores/__tests__/chatbot-ui.store.test.ts +18 -0
- package/src/stores/chatbot-ui-backend.store.ts +17 -7
- package/src/stores/chatbot-ui.store.ts +15 -9
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -318,6 +318,183 @@ describe('[ChatbotUiBackendStore]', () => {
|
|
|
318
318
|
});
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
test('should use per-request timeoutMs when provided', async () => {
|
|
322
|
+
mockChatbotSession();
|
|
323
|
+
chatbotApi.postMessage.mockImplementation(
|
|
324
|
+
() =>
|
|
325
|
+
new Promise(resolve =>
|
|
326
|
+
setTimeout(
|
|
327
|
+
() =>
|
|
328
|
+
resolve(ModelsMocks.mockBotMessage({ answer: 'late answer' })),
|
|
329
|
+
200
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
await runStore();
|
|
335
|
+
await chatUiStore.sendMessageText('user question');
|
|
336
|
+
const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
337
|
+
|
|
338
|
+
const p = runChatUiEventListener(store.handleMessageSend, [message, undefined, 50]);
|
|
339
|
+
await jest.advanceTimersByTimeAsync(50);
|
|
340
|
+
await p;
|
|
341
|
+
|
|
342
|
+
expect(chatUiStore.isError).toBe(true);
|
|
343
|
+
expect(log.error).toHaveBeenCalledWith(
|
|
344
|
+
expect.objectContaining({
|
|
345
|
+
code: 'TitanChatbot_FailedToSendMessage',
|
|
346
|
+
message: 'Failed to send message',
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('should fall back to global timeout when no per-request timeoutMs', async () => {
|
|
352
|
+
mockChatbotSession();
|
|
353
|
+
chatbotApi.postMessage.mockImplementation(
|
|
354
|
+
() =>
|
|
355
|
+
new Promise(resolve =>
|
|
356
|
+
setTimeout(
|
|
357
|
+
() =>
|
|
358
|
+
resolve(ModelsMocks.mockBotMessage({ answer: 'late answer' })),
|
|
359
|
+
200
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
chatUiStore.setCustomizationContext({ timeouts: { chatbotRequestTimeoutMs: 100 } });
|
|
364
|
+
|
|
365
|
+
await runStore();
|
|
366
|
+
await chatUiStore.sendMessageText('user question');
|
|
367
|
+
const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
368
|
+
|
|
369
|
+
// No per-request timeoutMs — should use global (100ms)
|
|
370
|
+
const p = runChatUiEventListener(store.handleMessageSend, [message, undefined]);
|
|
371
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
372
|
+
await p;
|
|
373
|
+
|
|
374
|
+
expect(chatUiStore.isError).toBe(true);
|
|
375
|
+
expect(log.error).toHaveBeenCalledWith(
|
|
376
|
+
expect.objectContaining({ code: 'TitanChatbot_FailedToSendMessage' })
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('should fall back to DEFAULT timeout when neither per-request nor global timeout is set', async () => {
|
|
381
|
+
mockChatbotSession();
|
|
382
|
+
chatbotApi.postMessage.mockImplementation(
|
|
383
|
+
() =>
|
|
384
|
+
new Promise(resolve =>
|
|
385
|
+
setTimeout(
|
|
386
|
+
() =>
|
|
387
|
+
resolve(ModelsMocks.mockBotMessage({ answer: 'late answer' })),
|
|
388
|
+
40000
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
await runStore();
|
|
394
|
+
await chatUiStore.sendMessageText('user question');
|
|
395
|
+
const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
396
|
+
|
|
397
|
+
// No per-request timeoutMs, no global timeout — should use DEFAULT (31000ms)
|
|
398
|
+
const p = runChatUiEventListener(store.handleMessageSend, [message, undefined]);
|
|
399
|
+
await jest.runOnlyPendingTimersAsync();
|
|
400
|
+
await p;
|
|
401
|
+
|
|
402
|
+
expect(chatUiStore.isError).toBe(true);
|
|
403
|
+
expect(log.error).toHaveBeenCalledWith(
|
|
404
|
+
expect.objectContaining({ code: 'TitanChatbot_FailedToSendMessage' })
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('should not abort store-level controller when per-request timeout fires', async () => {
|
|
409
|
+
mockChatbotSession();
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* First message: slow — will time out at 50ms
|
|
413
|
+
* Second message: fast — should succeed despite the first timing out
|
|
414
|
+
*/
|
|
415
|
+
const fastAnswer = ModelsMocks.mockBotMessage({ answer: 'fast answer' });
|
|
416
|
+
|
|
417
|
+
chatbotApi.postMessage
|
|
418
|
+
.mockImplementationOnce(() => new Promise(() => {})) // never resolves — times out
|
|
419
|
+
.mockResolvedValueOnce(fastAnswer);
|
|
420
|
+
|
|
421
|
+
await runStore();
|
|
422
|
+
|
|
423
|
+
await chatUiStore.sendMessageText('slow question');
|
|
424
|
+
const slowMessage = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
425
|
+
|
|
426
|
+
// Start slow message with a short per-request timeout
|
|
427
|
+
const p1 = runChatUiEventListener(store.handleMessageSend, [
|
|
428
|
+
slowMessage,
|
|
429
|
+
undefined,
|
|
430
|
+
50,
|
|
431
|
+
]);
|
|
432
|
+
await jest.advanceTimersByTimeAsync(50);
|
|
433
|
+
await p1;
|
|
434
|
+
|
|
435
|
+
expect(chatUiStore.isError).toBe(true);
|
|
436
|
+
expect(store.abortController.signal.aborted).toBe(false); // store-level signal must remain intact
|
|
437
|
+
|
|
438
|
+
// A subsequent message should still succeed because the store controller was not aborted
|
|
439
|
+
chatUiStore.resetError(ChatRunState.Started);
|
|
440
|
+
await chatUiStore.sendMessageText('fast question');
|
|
441
|
+
const fastMessage = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
442
|
+
await runChatUiEventListener(store.handleMessageSend, [fastMessage]);
|
|
443
|
+
|
|
444
|
+
expect(chatUiStore.isError).toBe(false);
|
|
445
|
+
expect(chatUiStore.messages.at(-1)! as ChatMessageModelText).toMatchObject({
|
|
446
|
+
message: 'fast answer',
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('should preserve per-request timeoutMs on retry', async () => {
|
|
451
|
+
mockChatbotSession();
|
|
452
|
+
chatbotApi.postMessage.mockImplementation(
|
|
453
|
+
() =>
|
|
454
|
+
new Promise(resolve =>
|
|
455
|
+
setTimeout(
|
|
456
|
+
() =>
|
|
457
|
+
resolve(ModelsMocks.mockBotMessage({ answer: 'late answer' })),
|
|
458
|
+
200
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
await runStore();
|
|
464
|
+
await chatUiStore.sendMessageText('user question');
|
|
465
|
+
const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
466
|
+
|
|
467
|
+
// Initial send fails due to per-request timeout; timeoutMs is persisted on the message
|
|
468
|
+
const p1 = runChatUiEventListener(store.handleMessageSend, [
|
|
469
|
+
message,
|
|
470
|
+
undefined,
|
|
471
|
+
50,
|
|
472
|
+
]);
|
|
473
|
+
await jest.advanceTimersByTimeAsync(50);
|
|
474
|
+
await p1;
|
|
475
|
+
expect(chatUiStore.isError).toBe(true);
|
|
476
|
+
|
|
477
|
+
// Retry should reuse the persisted timeoutMs (50ms), not fall back to default (31000ms)
|
|
478
|
+
chatbotApi.postMessage.mockImplementation(
|
|
479
|
+
() =>
|
|
480
|
+
new Promise(resolve =>
|
|
481
|
+
setTimeout(
|
|
482
|
+
() =>
|
|
483
|
+
resolve(ModelsMocks.mockBotMessage({ answer: 'late answer' })),
|
|
484
|
+
200
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
const p2 = runChatUiEventListener(store.handleMessageSendRetry, [message]);
|
|
489
|
+
await jest.advanceTimersByTimeAsync(50);
|
|
490
|
+
await p2;
|
|
491
|
+
|
|
492
|
+
// If retry had fallen back to default 31000ms, it would not have timed out in 50ms
|
|
493
|
+
expect(log.error).toHaveBeenCalledWith(
|
|
494
|
+
expect.objectContaining({ code: 'TitanChatbot_FailedToSendMessage' })
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
321
498
|
test('should send message retry', async () => {
|
|
322
499
|
mockChatbotSession();
|
|
323
500
|
const spyMessage = jest.spyOn(chatbotApi, 'postMessage');
|
|
@@ -153,6 +153,24 @@ describe('[ChatbotUiStore]', () => {
|
|
|
153
153
|
await checkScrollTriggered();
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
test('should sendMessageText with timeoutMs option', async () => {
|
|
157
|
+
mockEmit();
|
|
158
|
+
const messageText = 'Hello, world!';
|
|
159
|
+
await store.sendMessageText(messageText, { timeoutMs: 50 });
|
|
160
|
+
|
|
161
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
162
|
+
ChatUiEvent.eventMessageSend,
|
|
163
|
+
expect.any(Function),
|
|
164
|
+
expect.any(Function),
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
type: 'message',
|
|
167
|
+
message: messageText,
|
|
168
|
+
}),
|
|
169
|
+
store.filterStore.export(),
|
|
170
|
+
50
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
156
174
|
test('should sendMessageText without message', async () => {
|
|
157
175
|
mockEmit();
|
|
158
176
|
await store.sendMessageText('');
|
|
@@ -106,9 +106,10 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
|
|
|
106
106
|
resolve,
|
|
107
107
|
_,
|
|
108
108
|
messageModel: ChatMessageModelText,
|
|
109
|
-
selections: Models.Selections | undefined
|
|
109
|
+
selections: Models.Selections | undefined,
|
|
110
|
+
timeoutMs?: number
|
|
110
111
|
) => {
|
|
111
|
-
await this.sendMessage(messageModel, selections);
|
|
112
|
+
await this.sendMessage(messageModel, selections, timeoutMs);
|
|
112
113
|
resolve();
|
|
113
114
|
};
|
|
114
115
|
|
|
@@ -122,7 +123,8 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
|
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
const selections = messageModel.data?.selections as Models.Selections | undefined;
|
|
125
|
-
|
|
126
|
+
const timeoutMs = messageModel.data?.timeoutMs as number | undefined;
|
|
127
|
+
await this.sendMessage(messageModel as ChatMessageModelText, selections, timeoutMs);
|
|
126
128
|
resolve();
|
|
127
129
|
};
|
|
128
130
|
|
|
@@ -296,9 +298,14 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
|
|
|
296
298
|
|
|
297
299
|
protected async sendMessage(
|
|
298
300
|
messageModel: ChatMessageModelText,
|
|
299
|
-
selections?: Models.Selections
|
|
301
|
+
selections?: Models.Selections,
|
|
302
|
+
timeoutMs?: number
|
|
300
303
|
) {
|
|
301
304
|
const question = messageModel.message;
|
|
305
|
+
const perRequestController = new AbortController();
|
|
306
|
+
const forwardStoreAbort = () =>
|
|
307
|
+
perRequestController.abort(this.abortController.signal.reason);
|
|
308
|
+
this.abortController.signal.addEventListener('abort', forwardStoreAbort, { once: true });
|
|
302
309
|
try {
|
|
303
310
|
await this.startSession();
|
|
304
311
|
this.chatUiStore.setAgentTyping(true);
|
|
@@ -311,11 +318,12 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
|
|
|
311
318
|
experience: Models.Experience.MultiTurn,
|
|
312
319
|
selections,
|
|
313
320
|
}),
|
|
314
|
-
|
|
321
|
+
perRequestController.signal
|
|
315
322
|
),
|
|
316
|
-
|
|
323
|
+
timeoutMs ??
|
|
324
|
+
this.customizations?.timeouts?.chatbotRequestTimeoutMs ??
|
|
317
325
|
DEFAULT_CHATBOT_ASK_BOT_TIMEOUT_MS,
|
|
318
|
-
|
|
326
|
+
perRequestController
|
|
319
327
|
);
|
|
320
328
|
this.withSaveState(() => {
|
|
321
329
|
// Set message state to Delivered and preserve selections and bot answer id to the user question (to have a link between question and answer)
|
|
@@ -330,6 +338,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
|
|
|
330
338
|
this.withSaveState(() => {
|
|
331
339
|
this.chatUiStore.setMessageState(messageModel, ChatMessageState.Failed, {
|
|
332
340
|
selections,
|
|
341
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
333
342
|
});
|
|
334
343
|
});
|
|
335
344
|
if (error instanceof ChatError) {
|
|
@@ -343,6 +352,7 @@ export class ChatbotUiBackendStore extends InitializeStore implements IChatbotUi
|
|
|
343
352
|
);
|
|
344
353
|
}
|
|
345
354
|
} finally {
|
|
355
|
+
this.abortController.signal.removeEventListener('abort', forwardStoreAbort);
|
|
346
356
|
this.chatUiStore.setAgentTyping(false);
|
|
347
357
|
}
|
|
348
358
|
}
|
|
@@ -16,18 +16,19 @@ export enum ChatbotUiEvent {
|
|
|
16
16
|
export interface IChatbotUiStore extends IChatUiStore<ChatbotCustomizations> {
|
|
17
17
|
filterStore: IFilterStore;
|
|
18
18
|
settings?: Models.IFrontendModel;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
sendMessageText(messageText: string, options?: { timeoutMs?: number }): Promise<void>;
|
|
20
|
+
setFilters(filterOptions: Models.IFrontendModel, options?: IFilterStoreInitOptions): void;
|
|
21
|
+
startSession(
|
|
21
22
|
sessionData?: Models.ISession['data'],
|
|
22
23
|
forceRecreate?: boolean
|
|
23
|
-
)
|
|
24
|
-
sendSessionFeedback
|
|
24
|
+
): Promise<undefined | Models.Session>;
|
|
25
|
+
sendSessionFeedback(
|
|
25
26
|
feedback: Models.IFeedback
|
|
26
|
-
)
|
|
27
|
-
sendMessageFeedback
|
|
27
|
+
): Promise<undefined | Models.ChatbotFeedbackState>;
|
|
28
|
+
sendMessageFeedback(
|
|
28
29
|
message: Models.IBotMessageWithFeedback,
|
|
29
30
|
messageFeedback: Models.IFeedback
|
|
30
|
-
)
|
|
31
|
+
): Promise<undefined | Models.ChatbotFeedbackState>;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
@injectable()
|
|
@@ -49,13 +50,18 @@ export class ChatbotUiStore extends ChatUiStore<ChatbotCustomizations> implement
|
|
|
49
50
|
this.triggerScroll();
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
override async sendMessageText(messageText: string) {
|
|
53
|
+
override async sendMessageText(messageText: string, options?: { timeoutMs?: number }) {
|
|
53
54
|
await this.restartTimers();
|
|
54
55
|
if (messageText.trim() === '') {
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
const messageModel = this.addMessage(false, messageText);
|
|
58
|
-
await this.emitAsync(
|
|
59
|
+
await this.emitAsync(
|
|
60
|
+
ChatUiEvent.eventMessageSend,
|
|
61
|
+
messageModel,
|
|
62
|
+
this.filterStore.export(),
|
|
63
|
+
...(options?.timeoutMs !== undefined ? [options.timeoutMs] : [])
|
|
64
|
+
);
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
@action
|