@servicetitan/titan-chatbot-api 3.0.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 +9 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.d.ts +11 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.d.ts.map +1 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.js +47 -0
- package/dist/api-client/__mocks__/chatbot-api-client.mock.js.map +1 -0
- package/dist/api-client/base/chatbot-api-client.d.ts +27 -0
- package/dist/api-client/base/chatbot-api-client.d.ts.map +1 -0
- package/dist/api-client/base/chatbot-api-client.js +10 -0
- package/dist/api-client/base/chatbot-api-client.js.map +1 -0
- package/dist/api-client/help-center/__tests__/converter-from-models.test.d.ts +2 -0
- package/dist/api-client/help-center/__tests__/converter-from-models.test.d.ts.map +1 -0
- package/dist/api-client/help-center/__tests__/converter-from-models.test.js +34 -0
- package/dist/api-client/help-center/__tests__/converter-from-models.test.js.map +1 -0
- package/dist/api-client/help-center/__tests__/converter-to-models.test.d.ts +2 -0
- package/dist/api-client/help-center/__tests__/converter-to-models.test.d.ts.map +1 -0
- package/dist/api-client/help-center/__tests__/converter-to-models.test.js +82 -0
- package/dist/api-client/help-center/__tests__/converter-to-models.test.js.map +1 -0
- package/dist/api-client/help-center/chatbot-api-client.d.ts +31 -0
- package/dist/api-client/help-center/chatbot-api-client.d.ts.map +1 -0
- package/dist/api-client/help-center/chatbot-api-client.js +90 -0
- package/dist/api-client/help-center/chatbot-api-client.js.map +1 -0
- package/dist/api-client/help-center/converter-from-models.d.ts +13 -0
- package/dist/api-client/help-center/converter-from-models.d.ts.map +1 -0
- package/dist/api-client/help-center/converter-from-models.js +113 -0
- package/dist/api-client/help-center/converter-from-models.js.map +1 -0
- package/dist/api-client/help-center/converter-to-models.d.ts +13 -0
- package/dist/api-client/help-center/converter-to-models.d.ts.map +1 -0
- package/dist/api-client/help-center/converter-to-models.js +95 -0
- package/dist/api-client/help-center/converter-to-models.js.map +1 -0
- package/dist/api-client/help-center/index.d.ts +2 -0
- package/dist/api-client/help-center/index.d.ts.map +1 -0
- package/dist/api-client/help-center/index.js +2 -0
- package/dist/api-client/help-center/index.js.map +1 -0
- package/dist/api-client/help-center/native-client.d.ts +1260 -0
- package/dist/api-client/help-center/native-client.d.ts.map +1 -0
- package/dist/api-client/help-center/native-client.js +6169 -0
- package/dist/api-client/help-center/native-client.js.map +1 -0
- package/dist/api-client/index.d.ts +8 -0
- package/dist/api-client/index.d.ts.map +1 -0
- package/dist/api-client/index.js +8 -0
- package/dist/api-client/index.js.map +1 -0
- package/dist/api-client/models/__mocks__/models.mock.d.ts +13 -0
- package/dist/api-client/models/__mocks__/models.mock.d.ts.map +1 -0
- package/dist/api-client/models/__mocks__/models.mock.js +114 -0
- package/dist/api-client/models/__mocks__/models.mock.js.map +1 -0
- package/dist/api-client/models/index.d.ts +22 -0
- package/dist/api-client/models/index.d.ts.map +1 -0
- package/dist/api-client/models/index.js +15 -0
- package/dist/api-client/models/index.js.map +1 -0
- package/dist/api-client/titan-chat/chatbot-api-client.d.ts +34 -0
- package/dist/api-client/titan-chat/chatbot-api-client.d.ts.map +1 -0
- package/dist/api-client/titan-chat/chatbot-api-client.js +72 -0
- package/dist/api-client/titan-chat/chatbot-api-client.js.map +1 -0
- package/dist/api-client/titan-chat/index.d.ts +2 -0
- package/dist/api-client/titan-chat/index.d.ts.map +1 -0
- package/dist/api-client/titan-chat/index.js +2 -0
- package/dist/api-client/titan-chat/index.js.map +1 -0
- package/dist/api-client/titan-chat/native-client.d.ts +225 -0
- package/dist/api-client/titan-chat/native-client.d.ts.map +1 -0
- package/dist/api-client/titan-chat/native-client.js +931 -0
- package/dist/api-client/titan-chat/native-client.js.map +1 -0
- package/dist/api-client/utils/model-utils.d.ts +4 -0
- package/dist/api-client/utils/model-utils.d.ts.map +1 -0
- package/dist/api-client/utils/model-utils.js +58 -0
- package/dist/api-client/utils/model-utils.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/models/chatbot-customizations.d.ts +15 -0
- package/dist/models/chatbot-customizations.d.ts.map +1 -0
- package/dist/models/chatbot-customizations.js +2 -0
- package/dist/models/chatbot-customizations.js.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +2 -0
- package/dist/models/index.js.map +1 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.test.d.ts +2 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.test.d.ts.map +1 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.test.js +341 -0
- package/dist/stores/__tests__/chatbot-ui-backend.store.test.js.map +1 -0
- package/dist/stores/__tests__/chatbot-ui.store.test.d.ts +2 -0
- package/dist/stores/__tests__/chatbot-ui.store.test.d.ts.map +1 -0
- package/dist/stores/__tests__/chatbot-ui.store.test.js +166 -0
- package/dist/stores/__tests__/chatbot-ui.store.test.js.map +1 -0
- package/dist/stores/__tests__/filter.store.test.d.ts +2 -0
- package/dist/stores/__tests__/filter.store.test.d.ts.map +1 -0
- package/dist/stores/__tests__/filter.store.test.js +316 -0
- package/dist/stores/__tests__/filter.store.test.js.map +1 -0
- package/dist/stores/__tests__/initialize.store.test.d.ts +2 -0
- package/dist/stores/__tests__/initialize.store.test.d.ts.map +1 -0
- package/dist/stores/__tests__/initialize.store.test.js +54 -0
- package/dist/stores/__tests__/initialize.store.test.js.map +1 -0
- package/dist/stores/chatbot-ui-backend.store.d.ts +61 -0
- package/dist/stores/chatbot-ui-backend.store.d.ts.map +1 -0
- package/dist/stores/chatbot-ui-backend.store.js +396 -0
- package/dist/stores/chatbot-ui-backend.store.js.map +1 -0
- package/dist/stores/chatbot-ui.store.d.ts +25 -0
- package/dist/stores/chatbot-ui.store.d.ts.map +1 -0
- package/dist/stores/chatbot-ui.store.js +87 -0
- package/dist/stores/chatbot-ui.store.js.map +1 -0
- package/dist/stores/filter.store.d.ts +30 -0
- package/dist/stores/filter.store.d.ts.map +1 -0
- package/dist/stores/filter.store.js +334 -0
- package/dist/stores/filter.store.js.map +1 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/stores/initialize.store.d.ts +17 -0
- package/dist/stores/initialize.store.d.ts.map +1 -0
- package/dist/stores/initialize.store.js +98 -0
- package/dist/stores/initialize.store.js.map +1 -0
- package/dist/utils/__tests__/axios-utils.test.d.ts +2 -0
- package/dist/utils/__tests__/axios-utils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/axios-utils.test.js +33 -0
- package/dist/utils/__tests__/axios-utils.test.js.map +1 -0
- package/dist/utils/axios-utils.d.ts +5 -0
- package/dist/utils/axios-utils.d.ts.map +1 -0
- package/dist/utils/axios-utils.js +23 -0
- package/dist/utils/axios-utils.js.map +1 -0
- package/dist/utils/test-utils.d.ts +5 -0
- package/dist/utils/test-utils.d.ts.map +1 -0
- package/dist/utils/test-utils.js +17 -0
- package/dist/utils/test-utils.js.map +1 -0
- package/package.json +45 -0
- package/src/api-client/__mocks__/chatbot-api-client.mock.ts +11 -0
- package/src/api-client/base/chatbot-api-client.ts +33 -0
- package/src/api-client/help-center/__tests__/converter-from-models.test.ts +41 -0
- package/src/api-client/help-center/__tests__/converter-to-models.test.ts +89 -0
- package/src/api-client/help-center/chatbot-api-client.ts +107 -0
- package/src/api-client/help-center/converter-from-models.ts +132 -0
- package/src/api-client/help-center/converter-to-models.ts +124 -0
- package/src/api-client/help-center/index.ts +1 -0
- package/src/api-client/help-center/native-client.ts +5662 -0
- package/src/api-client/index.ts +12 -0
- package/src/api-client/models/__mocks__/models.mock.ts +141 -0
- package/src/api-client/models/index.ts +48 -0
- package/src/api-client/titan-chat/chatbot-api-client.ts +77 -0
- package/src/api-client/titan-chat/index.ts +1 -0
- package/src/api-client/titan-chat/native-client.ts +826 -0
- package/src/api-client/utils/model-utils.ts +68 -0
- package/src/cypress.d.ts +10 -0
- package/src/index.ts +6 -0
- package/src/models/chatbot-customizations.ts +16 -0
- package/src/models/index.ts +1 -0
- package/src/stores/__tests__/chatbot-ui-backend.store.test.ts +426 -0
- package/src/stores/__tests__/chatbot-ui.store.test.ts +196 -0
- package/src/stores/__tests__/filter.store.test.ts +363 -0
- package/src/stores/__tests__/initialize.store.test.ts +73 -0
- package/src/stores/chatbot-ui-backend.store.ts +401 -0
- package/src/stores/chatbot-ui.store.ts +82 -0
- package/src/stores/filter.store.ts +250 -0
- package/src/stores/index.ts +12 -0
- package/src/stores/initialize.store.ts +62 -0
- package/src/utils/__tests__/axios-utils.test.ts +40 -0
- package/src/utils/axios-utils.ts +25 -0
- package/src/utils/test-utils.ts +22 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Models } from '..';
|
|
2
|
+
|
|
3
|
+
export function createNewSessionModel(): Models.Session {
|
|
4
|
+
return new Models.Session();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createSelectionsModel(
|
|
8
|
+
filters: Models.IOption[],
|
|
9
|
+
selected: Record<string, string[]>
|
|
10
|
+
): Models.Selections | undefined {
|
|
11
|
+
const process = (filters: Models.IOption[]): Models.Selections | undefined => {
|
|
12
|
+
let result: Models.Selections | undefined;
|
|
13
|
+
const ensureResult = () => {
|
|
14
|
+
if (!result) {
|
|
15
|
+
result = new Models.Selections({
|
|
16
|
+
subOptions: {},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const filterList = filters.filter(
|
|
22
|
+
x => x.type === Models.OptionType.Group && Boolean(x.subOptions?.length)
|
|
23
|
+
);
|
|
24
|
+
for (const filter of filterList) {
|
|
25
|
+
const hasSubFilters = Boolean(
|
|
26
|
+
filter.subOptions?.some(x => Boolean(x.subOptions?.length))
|
|
27
|
+
);
|
|
28
|
+
if (!hasSubFilters) {
|
|
29
|
+
// Leaf filter: just collect selected values
|
|
30
|
+
const values = filter.subOptions
|
|
31
|
+
?.map(o => o.key)
|
|
32
|
+
.filter(o => selected[filter.key]?.includes(o));
|
|
33
|
+
if (values?.length) {
|
|
34
|
+
ensureResult();
|
|
35
|
+
result!.subOptions![filter.key] = new Models.Selections({ values });
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// Non-leaf filter: add selected options as selectables and process sub-filters
|
|
39
|
+
const filterSelectables = filter.subOptions!.filter(
|
|
40
|
+
x =>
|
|
41
|
+
x.type === Models.OptionType.Selectable &&
|
|
42
|
+
Boolean(x.subOptions?.length) &&
|
|
43
|
+
selected[filter.key]?.includes(x.key!)
|
|
44
|
+
);
|
|
45
|
+
if (!filterSelectables.length) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
ensureResult();
|
|
49
|
+
const filterResult = new Models.Selections({
|
|
50
|
+
subOptions: {},
|
|
51
|
+
});
|
|
52
|
+
result!.subOptions![filter.key] = filterResult;
|
|
53
|
+
for (const filterSelectable of filterSelectables) {
|
|
54
|
+
// Process sub-filters: if any sub-filters selected, add them to the result
|
|
55
|
+
const subFilters = filterSelectable.subOptions!;
|
|
56
|
+
const resultSubFilters = process(subFilters);
|
|
57
|
+
filterResult.subOptions![filterSelectable.key] =
|
|
58
|
+
resultSubFilters ??
|
|
59
|
+
new Models.Selections({
|
|
60
|
+
subOptions: {},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
};
|
|
67
|
+
return process(filters);
|
|
68
|
+
}
|
package/src/cypress.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// eslint-disable-next-line spaced-comment
|
|
2
|
+
/// <reference types="cypress" />
|
|
3
|
+
declare namespace Cypress {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5
|
+
interface Chainable<Subject = any> {
|
|
6
|
+
getCy<E extends Node = HTMLElement>(value: string): Chainable<JQuery<E>>;
|
|
7
|
+
getCy2<E extends Node = HTMLElement>(value: string): Chainable<JQuery<E>>;
|
|
8
|
+
getAnvil<E extends Node = HTMLElement>(value: string, extra?: string): Chainable<JQuery<E>>;
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-imports */
|
|
2
|
+
export * from './api-client';
|
|
3
|
+
export * from './stores';
|
|
4
|
+
export * from './models';
|
|
5
|
+
export * as NativeClientHC from './api-client/help-center/native-client';
|
|
6
|
+
export * as NativeClientTC from './api-client/titan-chat/native-client';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ChatCustomizations, ChatError } from '@servicetitan/titan-chat-ui-common';
|
|
2
|
+
|
|
3
|
+
export type ChatbotCustomizations = ChatCustomizations &
|
|
4
|
+
Partial<{
|
|
5
|
+
error?: {
|
|
6
|
+
recover: (error?: ChatError) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
feedback?: {
|
|
9
|
+
title?: string;
|
|
10
|
+
showGuardrailFeedback?: boolean;
|
|
11
|
+
isCommentAlwaysRequired?: boolean;
|
|
12
|
+
};
|
|
13
|
+
filters?: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './chatbot-customizations';
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { expect } from '@jest/globals';
|
|
2
|
+
import { ILog, Log, LogError, LogInfo, LogWarning } from '@servicetitan/log-service';
|
|
3
|
+
import { Container } from '@servicetitan/react-ioc';
|
|
4
|
+
import {
|
|
5
|
+
ChatError,
|
|
6
|
+
ChatMessageModelText,
|
|
7
|
+
ChatMessageModelWelcome,
|
|
8
|
+
ChatRunState,
|
|
9
|
+
ChatUiEventListener,
|
|
10
|
+
} from '@servicetitan/titan-chat-ui-common';
|
|
11
|
+
import { CHATBOT_API_CLIENT, IChatbotApiClient, Models, ModelsMocks } from '../../api-client';
|
|
12
|
+
import { ChatbotApiClientMock } from '../../api-client/__mocks__/chatbot-api-client.mock';
|
|
13
|
+
import { initTestContainer } from '../../utils/test-utils';
|
|
14
|
+
import { ChatbotUiBackendStore } from '../chatbot-ui-backend.store';
|
|
15
|
+
import { CHATBOT_UI_STORE_TOKEN, ChatbotUiStore } from '../chatbot-ui.store';
|
|
16
|
+
|
|
17
|
+
const WELCOME_MESSAGE =
|
|
18
|
+
'Hi there! I’m Titan, an AI chatbot powered by Titan Intelligence. I’m here to answer your questions about using ServiceTitan. You can ask me things like "How to merge duplicate customers?". Do you have a question for me?';
|
|
19
|
+
|
|
20
|
+
const initContainer = initTestContainer(ChatbotUiBackendStore, container => {
|
|
21
|
+
container
|
|
22
|
+
.bind<ILog>(Log)
|
|
23
|
+
.to(
|
|
24
|
+
class implements ILog {
|
|
25
|
+
error: (entry: LogError) => void = jest.fn();
|
|
26
|
+
info: (entry: LogInfo) => void = jest.fn();
|
|
27
|
+
warning: (entry: LogWarning) => void = jest.fn();
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
.inSingletonScope();
|
|
31
|
+
container
|
|
32
|
+
.bind<IChatbotApiClient>(CHATBOT_API_CLIENT)
|
|
33
|
+
.toConstantValue(new ChatbotApiClientMock());
|
|
34
|
+
container.bind(CHATBOT_UI_STORE_TOKEN).to(ChatbotUiStore).inSingletonScope();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('[ChatbotUiBackendStore]', () => {
|
|
38
|
+
let container: Container;
|
|
39
|
+
let store: ChatbotUiBackendStore;
|
|
40
|
+
let chatbotApi: ChatbotApiClientMock;
|
|
41
|
+
let chatUiStore: ChatbotUiStore;
|
|
42
|
+
let log: ILog;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
container = initContainer();
|
|
46
|
+
store = container.get(ChatbotUiBackendStore);
|
|
47
|
+
chatbotApi = container.get<IChatbotApiClient>(CHATBOT_API_CLIENT) as ChatbotApiClientMock;
|
|
48
|
+
chatUiStore = container.get<ChatbotUiStore>(CHATBOT_UI_STORE_TOKEN);
|
|
49
|
+
log = container.get<ILog>(Log);
|
|
50
|
+
jest.useFakeTimers();
|
|
51
|
+
jest.setSystemTime(new Date('2000-01-01T00:00:00.000Z'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.useRealTimers();
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('with subscription', () => {
|
|
60
|
+
let spyOn: jest.SpyInstance;
|
|
61
|
+
let spyOff: jest.SpyInstance;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
spyOn = jest.spyOn(chatUiStore, 'on');
|
|
65
|
+
spyOff = jest.spyOn(chatUiStore, 'off');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should subscribe on external events', () => {
|
|
69
|
+
store.subscribe();
|
|
70
|
+
expect(spyOn).toHaveBeenCalledTimes(7);
|
|
71
|
+
expect(spyOff).toHaveBeenCalledTimes(7);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should unsubscribe from external events', () => {
|
|
75
|
+
store.unsubscribe();
|
|
76
|
+
expect(chatUiStore.on).toHaveBeenCalledTimes(0);
|
|
77
|
+
expect(chatUiStore.off).toHaveBeenCalledTimes(7);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('with run', () => {
|
|
82
|
+
const runChatUiEventListener = async <T = void>(
|
|
83
|
+
listener: ChatUiEventListener<T>,
|
|
84
|
+
args: unknown[]
|
|
85
|
+
) => {
|
|
86
|
+
return new Promise<T>((resolve, reject) => {
|
|
87
|
+
listener.apply(store, [resolve, reject, ...args]);
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const runStore = async () => {
|
|
92
|
+
await runChatUiEventListener(store.handleRun, []);
|
|
93
|
+
await jest.advanceTimersToNextTimerAsync(); // Wait for the timer to be set
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const mockChatbotSession = () => {
|
|
97
|
+
chatbotApi.getOptions.mockResolvedValue(ModelsMocks.mockFrontendModel());
|
|
98
|
+
chatbotApi.postSession.mockResolvedValue(ModelsMocks.mockSession());
|
|
99
|
+
chatbotApi.deleteSession.mockResolvedValue(ModelsMocks.mockSession());
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const mockChatbotAnswer = (answer: string) => {
|
|
103
|
+
chatbotApi.postMessage.mockResolvedValue(
|
|
104
|
+
ModelsMocks.mockBotMessage({
|
|
105
|
+
answer,
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const ensureChatStarted = () => {
|
|
111
|
+
expect(chatUiStore.isAgentTyping).toBe(false);
|
|
112
|
+
expect(chatUiStore.isFilePickerEnabled).toBe(false);
|
|
113
|
+
expect(chatUiStore.file).toBe(undefined);
|
|
114
|
+
expect(chatUiStore.currentFileMessage).toBe(undefined);
|
|
115
|
+
expect(chatUiStore.error).toBe(undefined);
|
|
116
|
+
expect(chatUiStore.endReason).toBe(undefined);
|
|
117
|
+
expect(chatUiStore.timer).toBe(undefined);
|
|
118
|
+
expect(chatUiStore.scrollCounter > 1).toBe(true);
|
|
119
|
+
expect(chatUiStore.status).toBe(ChatRunState.Started);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
test('should run chatbot without stored session', async () => {
|
|
123
|
+
store.subscribe();
|
|
124
|
+
mockChatbotSession();
|
|
125
|
+
|
|
126
|
+
await runStore();
|
|
127
|
+
|
|
128
|
+
ensureChatStarted();
|
|
129
|
+
expect(chatUiStore.messages.length).toBe(1);
|
|
130
|
+
expect(chatUiStore.messages[0].type).toBe('welcome');
|
|
131
|
+
expect((chatUiStore.messages[0] as ChatMessageModelText).message).toBe(WELCOME_MESSAGE);
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* We don't have session yet because we've just initialized the chat with welcome message
|
|
135
|
+
* and we don't do any chatbotApi.postSession call
|
|
136
|
+
*/
|
|
137
|
+
expect(store.session).toBeUndefined();
|
|
138
|
+
expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
|
|
139
|
+
|
|
140
|
+
mockChatbotAnswer('bot answer');
|
|
141
|
+
await chatUiStore.sendMessageText('user question');
|
|
142
|
+
expect(chatUiStore.messages.length).toBe(3);
|
|
143
|
+
expect(chatUiStore.messages[0].type).toBe('welcome');
|
|
144
|
+
expect(chatUiStore.messages[1].type).toBe('message');
|
|
145
|
+
expect(chatUiStore.messages[2].type).toBe('message');
|
|
146
|
+
expect(chatUiStore.messages[1].participant.isAgent).toBe(false);
|
|
147
|
+
expect(chatUiStore.messages[2].participant.isAgent).toBe(true);
|
|
148
|
+
expect((chatUiStore.messages[1] as ChatMessageModelText).message).toBe('user question');
|
|
149
|
+
expect((chatUiStore.messages[2] as ChatMessageModelText).message).toBe('bot answer');
|
|
150
|
+
expect(log.error).toHaveBeenCalledTimes(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('should run chatbot with stored session with expiration', async () => {
|
|
154
|
+
mockChatbotSession();
|
|
155
|
+
|
|
156
|
+
await jest.advanceTimersByTimeAsync(1000 * 60 * 60 * 16);
|
|
157
|
+
await runStore();
|
|
158
|
+
ensureChatStarted();
|
|
159
|
+
expect(store.session).not.toBeDefined();
|
|
160
|
+
expect(chatUiStore.messages.length).toBe(1);
|
|
161
|
+
expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
|
|
162
|
+
expect(log.error).toHaveBeenCalledTimes(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('should destroy', async () => {
|
|
166
|
+
mockChatbotSession();
|
|
167
|
+
|
|
168
|
+
await runStore();
|
|
169
|
+
store.session = ModelsMocks.mockSession();
|
|
170
|
+
await runChatUiEventListener(store.handleDestroy, []);
|
|
171
|
+
|
|
172
|
+
expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
|
|
173
|
+
expect(chatbotApi.deleteSession).toHaveBeenCalledTimes(1);
|
|
174
|
+
expect(chatUiStore.status).toBe(ChatRunState.Offline);
|
|
175
|
+
expect(store.session).toBeUndefined();
|
|
176
|
+
expect(log.error).toHaveBeenCalledTimes(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should restart', async () => {
|
|
180
|
+
mockChatbotSession();
|
|
181
|
+
|
|
182
|
+
await runStore();
|
|
183
|
+
store.session = ModelsMocks.mockSession();
|
|
184
|
+
await runChatUiEventListener(store.handleRestart, []);
|
|
185
|
+
|
|
186
|
+
expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
|
|
187
|
+
expect(chatbotApi.deleteSession).toHaveBeenCalledTimes(1);
|
|
188
|
+
expect(chatUiStore.status).toBe(ChatRunState.Started);
|
|
189
|
+
expect(log.error).toHaveBeenCalledTimes(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('with messages', () => {
|
|
193
|
+
test('should send message', async () => {
|
|
194
|
+
mockChatbotSession();
|
|
195
|
+
mockChatbotAnswer('bot answer');
|
|
196
|
+
|
|
197
|
+
await runStore();
|
|
198
|
+
|
|
199
|
+
await chatUiStore.sendMessageText('user question');
|
|
200
|
+
const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
201
|
+
await runChatUiEventListener(store.handleMessageSend, [message]);
|
|
202
|
+
|
|
203
|
+
expect(chatUiStore.messages.length).toBe(3);
|
|
204
|
+
expect((chatUiStore.messages[0] as ChatMessageModelText).message).toBe(
|
|
205
|
+
WELCOME_MESSAGE
|
|
206
|
+
);
|
|
207
|
+
expect((chatUiStore.messages[1] as ChatMessageModelText).message).toBe(
|
|
208
|
+
'user question'
|
|
209
|
+
);
|
|
210
|
+
expect((chatUiStore.messages[2] as ChatMessageModelText).message).toBe(
|
|
211
|
+
'bot answer'
|
|
212
|
+
);
|
|
213
|
+
expect(log.error).toHaveBeenCalledTimes(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('should send message with error', async () => {
|
|
217
|
+
mockChatbotSession();
|
|
218
|
+
chatbotApi.postMessage.mockRejectedValue(new Error('error'));
|
|
219
|
+
|
|
220
|
+
await chatUiStore.sendMessageText('user question');
|
|
221
|
+
const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
|
|
222
|
+
await runChatUiEventListener(store.handleMessageSend, [message]);
|
|
223
|
+
|
|
224
|
+
expect(chatbotApi.postSession).toHaveBeenCalled();
|
|
225
|
+
expect(chatbotApi.postMessage).toHaveBeenCalled();
|
|
226
|
+
expect(chatUiStore.isError).toBe(true);
|
|
227
|
+
expect(log.error).toHaveBeenCalledWith({
|
|
228
|
+
category: 'TitanChatbot',
|
|
229
|
+
code: 'TitanChatbot_FailedToSendMessage',
|
|
230
|
+
error: new Error('error'),
|
|
231
|
+
message: 'Failed to send message',
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('should send message retry', async () => {
|
|
236
|
+
mockChatbotSession();
|
|
237
|
+
const spyMessage = jest.spyOn(chatbotApi, 'postMessage');
|
|
238
|
+
|
|
239
|
+
// Retry with non-text message should not call "message"
|
|
240
|
+
const messageNonText = ModelsMocks.mockChatMessageModel<ChatMessageModelWelcome>({
|
|
241
|
+
type: 'welcome',
|
|
242
|
+
});
|
|
243
|
+
await runChatUiEventListener(store.handleMessageSendRetry, [messageNonText]);
|
|
244
|
+
expect(spyMessage).not.toHaveBeenCalled();
|
|
245
|
+
|
|
246
|
+
// Retry with text message should call "message"
|
|
247
|
+
const message = ModelsMocks.mockChatMessageModel<ChatMessageModelText>({
|
|
248
|
+
message: 'message',
|
|
249
|
+
});
|
|
250
|
+
await runChatUiEventListener(store.handleMessageSendRetry, [message]);
|
|
251
|
+
expect(spyMessage).toHaveBeenCalledWith(
|
|
252
|
+
ModelsMocks.mockUserMessage({
|
|
253
|
+
context: undefined,
|
|
254
|
+
selections: undefined,
|
|
255
|
+
question: 'message',
|
|
256
|
+
}),
|
|
257
|
+
expect.any(AbortSignal)
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('with session feedback', () => {
|
|
263
|
+
const createSessionFeedback = () =>
|
|
264
|
+
ModelsMocks.mockFeedback({
|
|
265
|
+
sessionId: 1,
|
|
266
|
+
messageId: undefined,
|
|
267
|
+
rating: Models.FeedbackRatings.ThumbsUp,
|
|
268
|
+
description: 'description',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('should send feedback', async () => {
|
|
272
|
+
mockChatbotSession();
|
|
273
|
+
|
|
274
|
+
chatbotApi.postFeedback.mockResolvedValueOnce(createSessionFeedback());
|
|
275
|
+
|
|
276
|
+
const { feedback, state } = await runChatUiEventListener(
|
|
277
|
+
store.handleSessionFeedback,
|
|
278
|
+
[createSessionFeedback()]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
expect(state).toBe(Models.ChatbotFeedbackState.Success);
|
|
282
|
+
expect(feedback).toEqual(createSessionFeedback());
|
|
283
|
+
expect(chatbotApi.postSession).toHaveBeenCalled();
|
|
284
|
+
expect(chatbotApi.postFeedback).toHaveBeenCalledWith(
|
|
285
|
+
createSessionFeedback(),
|
|
286
|
+
expect.any(AbortSignal)
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('should send feedback with session error', async () => {
|
|
291
|
+
chatbotApi.postSession.mockRejectedValueOnce('session error');
|
|
292
|
+
chatbotApi.postFeedback.mockResolvedValueOnce(createSessionFeedback());
|
|
293
|
+
|
|
294
|
+
const { feedback, state } = await runChatUiEventListener(
|
|
295
|
+
store.handleSessionFeedback,
|
|
296
|
+
[createSessionFeedback()]
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
expect(state).toBe(Models.ChatbotFeedbackState.Failure);
|
|
300
|
+
expect(feedback).toEqual(createSessionFeedback());
|
|
301
|
+
expect(chatbotApi.postSession).toHaveBeenCalled();
|
|
302
|
+
expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
|
|
303
|
+
expect(chatUiStore.isError).toBe(true);
|
|
304
|
+
expect(log.error).toHaveBeenCalledWith({
|
|
305
|
+
category: 'TitanChatbot',
|
|
306
|
+
code: 'TitanChatbot_FailedToSendFeedback',
|
|
307
|
+
error: 'session error',
|
|
308
|
+
message: 'Failed to send feedback',
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('should send feedback with error', async () => {
|
|
313
|
+
mockChatbotSession();
|
|
314
|
+
chatbotApi.postSession.mockReset().mockRejectedValue(new Error('session error'));
|
|
315
|
+
chatbotApi.postFeedback.mockResolvedValueOnce(createSessionFeedback());
|
|
316
|
+
|
|
317
|
+
const { feedback, state } = await runChatUiEventListener(
|
|
318
|
+
store.handleSessionFeedback,
|
|
319
|
+
[createSessionFeedback()]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(state).toBe(Models.ChatbotFeedbackState.Failure);
|
|
323
|
+
expect(feedback).toEqual(createSessionFeedback());
|
|
324
|
+
expect(chatbotApi.postSession).toHaveBeenCalled();
|
|
325
|
+
expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
|
|
326
|
+
expect(chatUiStore.isError).toBe(true);
|
|
327
|
+
expect(log.error).toHaveBeenCalledWith({
|
|
328
|
+
category: 'TitanChatbot',
|
|
329
|
+
code: 'TitanChatbot_FailedToSendFeedback',
|
|
330
|
+
error: new Error('session error'),
|
|
331
|
+
message: 'Failed to send feedback',
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('with message feedback', () => {
|
|
337
|
+
const createMessageFeedback = () =>
|
|
338
|
+
ModelsMocks.mockFeedback({
|
|
339
|
+
sessionId: 1,
|
|
340
|
+
messageId: 1,
|
|
341
|
+
description: 'description',
|
|
342
|
+
linkUrl: 'linkUrl',
|
|
343
|
+
options: [Models.FeedbackOptions.Unclear, Models.FeedbackOptions.Incorrect],
|
|
344
|
+
rating: Models.FeedbackRatings.ThumbsUp,
|
|
345
|
+
});
|
|
346
|
+
const createLogError = (errorMessage: string) => ({
|
|
347
|
+
category: 'TitanChatbot',
|
|
348
|
+
code: 'TitanChatbot_FailedToSendMessageFeedback',
|
|
349
|
+
error: errorMessage,
|
|
350
|
+
message: 'Failed to send message feedback',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('should assert message feedback 1', async () => {
|
|
354
|
+
const { feedback, state } = await runChatUiEventListener(
|
|
355
|
+
store.handleMessageFeedback,
|
|
356
|
+
[]
|
|
357
|
+
);
|
|
358
|
+
expect(feedback).toBeUndefined();
|
|
359
|
+
expect(state).toBe(Models.ChatbotFeedbackState.Failure);
|
|
360
|
+
expect(chatUiStore.isError).toBe(true);
|
|
361
|
+
expect(chatUiStore.error?.message).toBe('Failed to send message feedback');
|
|
362
|
+
expect(log.error).toHaveBeenCalledWith({
|
|
363
|
+
category: 'TitanChatbot',
|
|
364
|
+
code: 'TitanChatbot_FailedToSendMessageFeedback',
|
|
365
|
+
error: new ChatError('Message feedback is missing.'),
|
|
366
|
+
message: 'Failed to send message feedback',
|
|
367
|
+
});
|
|
368
|
+
expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('should assert message feedback 3', async () => {
|
|
372
|
+
chatbotApi.postSession.mockRejectedValueOnce('session error');
|
|
373
|
+
const { feedback, state } = await runChatUiEventListener(
|
|
374
|
+
store.handleMessageFeedback,
|
|
375
|
+
[createMessageFeedback()]
|
|
376
|
+
);
|
|
377
|
+
expect(state).toBe(Models.ChatbotFeedbackState.Failure);
|
|
378
|
+
expect(feedback).toEqual(createMessageFeedback());
|
|
379
|
+
expect(chatUiStore.isError).toBe(true);
|
|
380
|
+
expect(chatUiStore.error?.message).toBe('Failed to send message feedback');
|
|
381
|
+
expect(log.error).toHaveBeenCalledWith({
|
|
382
|
+
category: 'TitanChatbot',
|
|
383
|
+
code: 'TitanChatbot_FailedToSendMessageFeedback',
|
|
384
|
+
error: 'session error',
|
|
385
|
+
message: 'Failed to send message feedback',
|
|
386
|
+
});
|
|
387
|
+
expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('should send message feedback', async () => {
|
|
391
|
+
mockChatbotSession();
|
|
392
|
+
chatbotApi.postFeedback.mockResolvedValueOnce(createMessageFeedback());
|
|
393
|
+
|
|
394
|
+
const feedback = createMessageFeedback();
|
|
395
|
+
await runChatUiEventListener(store.handleMessageFeedback, [feedback]);
|
|
396
|
+
|
|
397
|
+
expect(chatbotApi.postFeedback).toHaveBeenCalledWith(
|
|
398
|
+
{
|
|
399
|
+
...feedback,
|
|
400
|
+
},
|
|
401
|
+
expect.any(AbortSignal)
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('should send message feedback with session error', async () => {
|
|
406
|
+
chatbotApi.postSession.mockRejectedValueOnce('session error');
|
|
407
|
+
chatbotApi.postFeedback.mockResolvedValueOnce(createMessageFeedback());
|
|
408
|
+
const feedback = createMessageFeedback();
|
|
409
|
+
await runChatUiEventListener(store.handleMessageFeedback, [feedback]);
|
|
410
|
+
|
|
411
|
+
expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
|
|
412
|
+
expect(log.error).toHaveBeenCalledWith(createLogError('session error'));
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('should send message feedback with feedback error', async () => {
|
|
416
|
+
mockChatbotSession();
|
|
417
|
+
chatbotApi.postFeedback.mockRejectedValueOnce('feedback error');
|
|
418
|
+
const feedback = createMessageFeedback();
|
|
419
|
+
await runChatUiEventListener(store.handleMessageFeedback, [feedback]);
|
|
420
|
+
|
|
421
|
+
expect(chatbotApi.postFeedback).toHaveBeenCalledTimes(1);
|
|
422
|
+
expect(log.error).toHaveBeenCalledWith(createLogError('feedback error'));
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|