@eeacms/volto-eea-chatbot 2.0.0 → 2.0.2
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/.eslintrc.js +6 -6
- package/CHANGELOG.md +12 -1
- package/jest-addon.config.js +1 -0
- package/package.json +1 -1
- package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
- package/src/ChatBlock/chat/AIMessage.tsx +20 -16
- package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
- package/src/ChatBlock/chat/ChatWindow.tsx +10 -11
- package/src/ChatBlock/chat/UserMessage.tsx +4 -4
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
- package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
- package/src/ChatBlock/components/EmptyState.jsx +1 -1
- package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
- package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
- package/src/ChatBlock/components/Source.jsx +2 -2
- package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
- package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
- package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
- package/src/ChatBlock/hooks/useChatController.ts +7 -4
- package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
- package/src/ChatBlock/hooks/useToolDisplayTiming.ts +1 -1
- package/src/ChatBlock/index.js +8 -0
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +11 -12
- package/src/ChatBlock/packets/RendererComponent.tsx +6 -3
- package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
- package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
- package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -8
- package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +5 -5
- package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +10 -10
- package/src/ChatBlock/services/messageProcessor.ts +6 -3
- package/src/ChatBlock/services/packetUtils.ts +2 -2
- package/src/ChatBlock/services/streamingService.ts +8 -2
- package/src/ChatBlock/utils/citations.ts +1 -1
- package/src/halloumi/filtering.test.js +199 -1
- package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
- package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
- package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
- package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
- package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
- package/src/ChatBlock/tests/Citation.test.jsx +0 -107
- package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
- package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
- package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
- package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
- package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
- package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
- package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
- package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
- package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
- package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
- package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
- package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
- package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
- package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
- package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
- package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
- package/src/ChatBlock/tests/Source.test.jsx +0 -79
- package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
- package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
- package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
- package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
- package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
- package/src/ChatBlock/tests/citations.test.js +0 -114
- package/src/ChatBlock/tests/index.test.js +0 -51
- package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
- package/src/ChatBlock/tests/packetUtils.test.js +0 -158
- package/src/ChatBlock/tests/schema.test.js +0 -166
- package/src/ChatBlock/tests/streamingService.test.js +0 -467
- package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
- package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
- package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
- package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
- package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
- package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
- package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
- package/src/ChatBlock/tests/utils.test.jsx +0 -241
- package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
- /package/src/ChatBlock/{schema.js → schema.jsx} +0 -0
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import '@testing-library/jest-dom';
|
|
2
|
-
import { ChatBlockSchema } from '../schema';
|
|
3
|
-
|
|
4
|
-
describe('ChatBlockSchema', () => {
|
|
5
|
-
const mockAssistants = [
|
|
6
|
-
{ id: 1, name: 'Assistant 1' },
|
|
7
|
-
{ id: 2, name: 'Assistant 2' },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
it('returns schema object with title', () => {
|
|
11
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
12
|
-
expect(schema.title).toBe('Chatbot');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('returns schema with fieldsets', () => {
|
|
16
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
17
|
-
expect(schema.fieldsets).toBeDefined();
|
|
18
|
-
expect(Array.isArray(schema.fieldsets)).toBe(true);
|
|
19
|
-
expect(schema.fieldsets.length).toBeGreaterThan(0);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('returns schema with properties', () => {
|
|
23
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
24
|
-
expect(schema.properties).toBeDefined();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('includes assistant choices from assistants array', () => {
|
|
28
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
29
|
-
expect(schema.properties.assistant.choices).toEqual([
|
|
30
|
-
['1', 'Assistant 1'],
|
|
31
|
-
['2', 'Assistant 2'],
|
|
32
|
-
]);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('handles empty assistants array', () => {
|
|
36
|
-
const schema = ChatBlockSchema({ assistants: [], data: {} });
|
|
37
|
-
expect(schema.properties.assistant.choices).toEqual([]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('handles undefined assistants', () => {
|
|
41
|
-
const schema = ChatBlockSchema({ assistants: undefined, data: {} });
|
|
42
|
-
expect(schema.properties.assistant.choices).toEqual([]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('includes starterPrompts field when enableStarterPrompts is true', () => {
|
|
46
|
-
const schema = ChatBlockSchema({
|
|
47
|
-
assistants: mockAssistants,
|
|
48
|
-
data: { enableStarterPrompts: true },
|
|
49
|
-
});
|
|
50
|
-
expect(schema.fieldsets[0].fields).toContain('starterPrompts');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('excludes starterPrompts field when enableStarterPrompts is false', () => {
|
|
54
|
-
const schema = ChatBlockSchema({
|
|
55
|
-
assistants: mockAssistants,
|
|
56
|
-
data: { enableStarterPrompts: false },
|
|
57
|
-
});
|
|
58
|
-
expect(schema.fieldsets[0].fields).not.toContain('starterPrompts');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('includes feedbackReasons field when enableFeedback is true', () => {
|
|
62
|
-
const schema = ChatBlockSchema({
|
|
63
|
-
assistants: mockAssistants,
|
|
64
|
-
data: { enableFeedback: true },
|
|
65
|
-
});
|
|
66
|
-
expect(schema.fieldsets[0].fields).toContain('feedbackReasons');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('excludes feedbackReasons field when enableFeedback is false', () => {
|
|
70
|
-
const schema = ChatBlockSchema({
|
|
71
|
-
assistants: mockAssistants,
|
|
72
|
-
data: { enableFeedback: false },
|
|
73
|
-
});
|
|
74
|
-
expect(schema.fieldsets[0].fields).not.toContain('feedbackReasons');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('includes quality check fields when qualityCheck is enabled', () => {
|
|
78
|
-
const schema = ChatBlockSchema({
|
|
79
|
-
assistants: mockAssistants,
|
|
80
|
-
data: { qualityCheck: 'enabled' },
|
|
81
|
-
});
|
|
82
|
-
expect(schema.fieldsets[0].fields).toContain('maxContextSegments');
|
|
83
|
-
expect(schema.fieldsets[0].fields).toContain('noSupportDocumentsMessage');
|
|
84
|
-
expect(schema.fieldsets[0].fields).toContain('qualityCheckContext');
|
|
85
|
-
expect(schema.fieldsets[0].fields).toContain('qualityCheckStages');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('excludes quality check fields when qualityCheck is disabled', () => {
|
|
89
|
-
const schema = ChatBlockSchema({
|
|
90
|
-
assistants: mockAssistants,
|
|
91
|
-
data: { qualityCheck: 'disabled' },
|
|
92
|
-
});
|
|
93
|
-
expect(schema.fieldsets[0].fields).not.toContain('maxContextSegments');
|
|
94
|
-
expect(schema.fieldsets[0].fields).not.toContain('qualityCheckContext');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('includes onDemandInputToggle when qualityCheck is ondemand_toggle', () => {
|
|
98
|
-
const schema = ChatBlockSchema({
|
|
99
|
-
assistants: mockAssistants,
|
|
100
|
-
data: { qualityCheck: 'ondemand_toggle' },
|
|
101
|
-
});
|
|
102
|
-
expect(schema.fieldsets[0].fields).toContain('onDemandInputToggle');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('excludes onDemandInputToggle when qualityCheck is not ondemand_toggle', () => {
|
|
106
|
-
const schema = ChatBlockSchema({
|
|
107
|
-
assistants: mockAssistants,
|
|
108
|
-
data: { qualityCheck: 'enabled' },
|
|
109
|
-
});
|
|
110
|
-
expect(schema.fieldsets[0].fields).not.toContain('onDemandInputToggle');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('includes totalFailMessage when enableShowTotalFailMessage is true', () => {
|
|
114
|
-
const schema = ChatBlockSchema({
|
|
115
|
-
assistants: mockAssistants,
|
|
116
|
-
data: { enableShowTotalFailMessage: true },
|
|
117
|
-
});
|
|
118
|
-
expect(schema.fieldsets[0].fields).toContain('totalFailMessage');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('excludes totalFailMessage when enableShowTotalFailMessage is false', () => {
|
|
122
|
-
const schema = ChatBlockSchema({
|
|
123
|
-
assistants: mockAssistants,
|
|
124
|
-
data: { enableShowTotalFailMessage: false },
|
|
125
|
-
});
|
|
126
|
-
expect(schema.fieldsets[0].fields).not.toContain('totalFailMessage');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('has required array', () => {
|
|
130
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
131
|
-
expect(schema.required).toBeDefined();
|
|
132
|
-
expect(Array.isArray(schema.required)).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('has default values for key properties', () => {
|
|
136
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
137
|
-
expect(schema.properties.placeholderPrompt.default).toBe('Ask a question');
|
|
138
|
-
expect(schema.properties.chatTitle.default).toBe('Online public chat');
|
|
139
|
-
expect(schema.properties.enableFeedback.default).toBe(true);
|
|
140
|
-
expect(schema.properties.qualityCheck.default).toBe('disabled');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('has qualityCheckStages with default score ranges', () => {
|
|
144
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
145
|
-
expect(schema.properties.qualityCheckStages.default).toBeDefined();
|
|
146
|
-
expect(Array.isArray(schema.properties.qualityCheckStages.default)).toBe(
|
|
147
|
-
true,
|
|
148
|
-
);
|
|
149
|
-
expect(schema.properties.qualityCheckStages.default.length).toBe(5);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('has feedbackReasons with default choices', () => {
|
|
153
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
154
|
-
expect(schema.properties.feedbackReasons.default).toBeDefined();
|
|
155
|
-
expect(Array.isArray(schema.properties.feedbackReasons.default)).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('has deepResearch property with choices', () => {
|
|
159
|
-
const schema = ChatBlockSchema({ assistants: mockAssistants, data: {} });
|
|
160
|
-
expect(schema.properties.deepResearch.choices).toBeDefined();
|
|
161
|
-
expect(schema.properties.deepResearch.choices).toContainEqual([
|
|
162
|
-
'always_on',
|
|
163
|
-
'Always on',
|
|
164
|
-
]);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
import { TextEncoder, TextDecoder } from 'util';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
processRawChunkString,
|
|
5
|
-
handleStream,
|
|
6
|
-
createChatSession,
|
|
7
|
-
submitFeedback,
|
|
8
|
-
regenerateMessage,
|
|
9
|
-
} from '../services/streamingService';
|
|
10
|
-
import { PacketType } from '../types/streamingModels';
|
|
11
|
-
|
|
12
|
-
global.TextEncoder = TextEncoder;
|
|
13
|
-
global.TextDecoder = TextDecoder;
|
|
14
|
-
|
|
15
|
-
// ── processRawChunkString ──────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
describe('processRawChunkString', () => {
|
|
18
|
-
it('returns empty array for empty string', () => {
|
|
19
|
-
const [chunks, partial] = processRawChunkString('', null);
|
|
20
|
-
expect(chunks).toEqual([]);
|
|
21
|
-
expect(partial).toBeNull();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('parses a single complete JSON chunk', () => {
|
|
25
|
-
const raw = JSON.stringify({ ind: 1, obj: { type: 'message_delta' } });
|
|
26
|
-
const [chunks, partial] = processRawChunkString(raw, null);
|
|
27
|
-
expect(chunks).toHaveLength(1);
|
|
28
|
-
expect(chunks[0]).toEqual({ ind: 1, obj: { type: 'message_delta' } });
|
|
29
|
-
expect(partial).toBeNull();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('parses multiple newline-separated JSON chunks', () => {
|
|
33
|
-
const c1 = JSON.stringify({ ind: 1, obj: { type: 'message_start' } });
|
|
34
|
-
const c2 = JSON.stringify({ ind: 2, obj: { type: 'message_delta' } });
|
|
35
|
-
const raw = `${c1}\n${c2}`;
|
|
36
|
-
const [chunks, partial] = processRawChunkString(raw, null);
|
|
37
|
-
expect(chunks).toHaveLength(2);
|
|
38
|
-
expect(chunks[0].ind).toBe(1);
|
|
39
|
-
expect(chunks[1].ind).toBe(2);
|
|
40
|
-
expect(partial).toBeNull();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('handles incomplete chunk and returns it as partial', () => {
|
|
44
|
-
const raw = '{"ind": 1, "obj": {"type"';
|
|
45
|
-
const [chunks, partial] = processRawChunkString(raw, null);
|
|
46
|
-
expect(chunks).toHaveLength(0);
|
|
47
|
-
expect(partial).toBe(raw);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('combines previous partial chunk with new data', () => {
|
|
51
|
-
const prev = '{"ind": 1, "obj": {"type"';
|
|
52
|
-
const rest = ': "message_delta"}}';
|
|
53
|
-
const [chunks, partial] = processRawChunkString(rest, prev);
|
|
54
|
-
expect(chunks).toHaveLength(1);
|
|
55
|
-
expect(chunks[0]).toEqual({ ind: 1, obj: { type: 'message_delta' } });
|
|
56
|
-
expect(partial).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('filters out empty lines', () => {
|
|
60
|
-
const c1 = JSON.stringify({ ind: 1, obj: { type: 'stop' } });
|
|
61
|
-
const raw = `\n${c1}\n\n`;
|
|
62
|
-
const [chunks, partial] = processRawChunkString(raw, null);
|
|
63
|
-
expect(chunks).toHaveLength(1);
|
|
64
|
-
expect(partial).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns null for falsy input', () => {
|
|
68
|
-
const [chunks, partial] = processRawChunkString(null, null);
|
|
69
|
-
expect(chunks).toEqual([]);
|
|
70
|
-
expect(partial).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ── handleStream ───────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
describe('handleStream', () => {
|
|
77
|
-
function makeReadableStream(chunks) {
|
|
78
|
-
let index = 0;
|
|
79
|
-
return {
|
|
80
|
-
body: {
|
|
81
|
-
getReader() {
|
|
82
|
-
return {
|
|
83
|
-
async read() {
|
|
84
|
-
if (index >= chunks.length) {
|
|
85
|
-
return { done: true, value: undefined };
|
|
86
|
-
}
|
|
87
|
-
const value = new TextEncoder().encode(chunks[index++]);
|
|
88
|
-
return { done: false, value };
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
it('yields packets from a stream with ind/obj format', async () => {
|
|
97
|
-
const packet = JSON.stringify({
|
|
98
|
-
ind: 1,
|
|
99
|
-
obj: { type: PacketType.MESSAGE_DELTA, content: 'hello' },
|
|
100
|
-
});
|
|
101
|
-
const response = makeReadableStream([packet]);
|
|
102
|
-
const allPackets = [];
|
|
103
|
-
for await (const packets of handleStream(response)) {
|
|
104
|
-
allPackets.push(...packets);
|
|
105
|
-
}
|
|
106
|
-
expect(allPackets).toHaveLength(1);
|
|
107
|
-
expect(allPackets[0].obj.type).toBe(PacketType.MESSAGE_DELTA);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('handles MessageResponseIDInfo format', async () => {
|
|
111
|
-
const chunk = JSON.stringify({
|
|
112
|
-
user_message_id: 42,
|
|
113
|
-
reserved_assistant_message_id: 43,
|
|
114
|
-
});
|
|
115
|
-
const response = makeReadableStream([chunk]);
|
|
116
|
-
const allPackets = [];
|
|
117
|
-
for await (const packets of handleStream(response)) {
|
|
118
|
-
allPackets.push(...packets);
|
|
119
|
-
}
|
|
120
|
-
expect(allPackets).toHaveLength(1);
|
|
121
|
-
expect(allPackets[0].obj.type).toBe(PacketType.MESSAGE_END_ID_INFO);
|
|
122
|
-
expect(allPackets[0].obj.user_message_id).toBe(42);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('handles error format', async () => {
|
|
126
|
-
const chunk = JSON.stringify({ error: 'something went wrong' });
|
|
127
|
-
const response = makeReadableStream([chunk]);
|
|
128
|
-
const allPackets = [];
|
|
129
|
-
for await (const packets of handleStream(response)) {
|
|
130
|
-
allPackets.push(...packets);
|
|
131
|
-
}
|
|
132
|
-
expect(allPackets).toHaveLength(1);
|
|
133
|
-
expect(allPackets[0].obj.type).toBe(PacketType.ERROR);
|
|
134
|
-
expect(allPackets[0].obj.error).toBe('something went wrong');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('throws when no reader is available', async () => {
|
|
138
|
-
const response = { body: null };
|
|
139
|
-
const gen = handleStream(response);
|
|
140
|
-
await expect(gen.next()).rejects.toThrow('No reader available');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('handles multiple chunks across reads', async () => {
|
|
144
|
-
const c1 = JSON.stringify({
|
|
145
|
-
ind: 1,
|
|
146
|
-
obj: { type: PacketType.MESSAGE_START, content: 'hi' },
|
|
147
|
-
});
|
|
148
|
-
const c2 = JSON.stringify({
|
|
149
|
-
ind: 2,
|
|
150
|
-
obj: { type: PacketType.MESSAGE_DELTA, content: ' world' },
|
|
151
|
-
});
|
|
152
|
-
const response = makeReadableStream([c1, c2]);
|
|
153
|
-
const allPackets = [];
|
|
154
|
-
for await (const packets of handleStream(response)) {
|
|
155
|
-
allPackets.push(...packets);
|
|
156
|
-
}
|
|
157
|
-
expect(allPackets).toHaveLength(2);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('skips non-object chunks', async () => {
|
|
161
|
-
// A string that parses to a non-object (number)
|
|
162
|
-
const response = makeReadableStream(['42']);
|
|
163
|
-
const allPackets = [];
|
|
164
|
-
for await (const packets of handleStream(response)) {
|
|
165
|
-
allPackets.push(...packets);
|
|
166
|
-
}
|
|
167
|
-
expect(allPackets).toHaveLength(0);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('handles split chunk across two reads', async () => {
|
|
171
|
-
const full = JSON.stringify({
|
|
172
|
-
ind: 1,
|
|
173
|
-
obj: { type: PacketType.STOP },
|
|
174
|
-
});
|
|
175
|
-
const half1 = full.slice(0, 10);
|
|
176
|
-
const half2 = full.slice(10);
|
|
177
|
-
const response = makeReadableStream([half1, half2]);
|
|
178
|
-
const allPackets = [];
|
|
179
|
-
for await (const packets of handleStream(response)) {
|
|
180
|
-
allPackets.push(...packets);
|
|
181
|
-
}
|
|
182
|
-
expect(allPackets).toHaveLength(1);
|
|
183
|
-
expect(allPackets[0].obj.type).toBe(PacketType.STOP);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ── createChatSession ──────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
describe('createChatSession', () => {
|
|
190
|
-
beforeEach(() => {
|
|
191
|
-
global.fetch = jest.fn();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
afterEach(() => {
|
|
195
|
-
jest.restoreAllMocks();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('returns chat session id on success', async () => {
|
|
199
|
-
global.fetch.mockResolvedValue({
|
|
200
|
-
ok: true,
|
|
201
|
-
json: () => Promise.resolve({ chat_session_id: 'abc-123' }),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const result = await createChatSession(1, 'Test session');
|
|
205
|
-
expect(result).toBe('abc-123');
|
|
206
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
207
|
-
'/_da/chat/create-chat-session',
|
|
208
|
-
expect.objectContaining({
|
|
209
|
-
method: 'POST',
|
|
210
|
-
body: JSON.stringify({ persona_id: 1, description: 'Test session' }),
|
|
211
|
-
}),
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('throws on failed response', async () => {
|
|
216
|
-
global.fetch.mockResolvedValue({ ok: false });
|
|
217
|
-
await expect(createChatSession(1)).rejects.toThrow(
|
|
218
|
-
'Failed to create chat session',
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// ── submitFeedback ─────────────────────────────────────────────────────
|
|
224
|
-
|
|
225
|
-
describe('submitFeedback', () => {
|
|
226
|
-
beforeEach(() => {
|
|
227
|
-
global.fetch = jest.fn();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
afterEach(() => {
|
|
231
|
-
jest.restoreAllMocks();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('sends positive feedback', async () => {
|
|
235
|
-
global.fetch.mockResolvedValue({
|
|
236
|
-
ok: true,
|
|
237
|
-
json: () => Promise.resolve({}),
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
await submitFeedback({
|
|
241
|
-
chatMessageId: 1,
|
|
242
|
-
isPositive: true,
|
|
243
|
-
feedbackText: 'Great!',
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
247
|
-
expect(body.is_positive).toBe(true);
|
|
248
|
-
expect(body.chat_message_id).toBe(1);
|
|
249
|
-
expect(body.predefined_feedback).toBeUndefined();
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
it('sends negative feedback with predefined reason', async () => {
|
|
253
|
-
global.fetch.mockResolvedValue({
|
|
254
|
-
ok: true,
|
|
255
|
-
json: () => Promise.resolve({}),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
await submitFeedback({
|
|
259
|
-
chatMessageId: 2,
|
|
260
|
-
isPositive: false,
|
|
261
|
-
predefinedFeedback: 'Inaccurate',
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
265
|
-
expect(body.is_positive).toBe(false);
|
|
266
|
-
expect(body.predefined_feedback).toBe('Inaccurate');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('throws on failed response', async () => {
|
|
270
|
-
global.fetch.mockResolvedValue({ ok: false });
|
|
271
|
-
await expect(
|
|
272
|
-
submitFeedback({ chatMessageId: 1, isPositive: true }),
|
|
273
|
-
).rejects.toThrow('Failed to submit feedback');
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// ── sendMessage ──────────────────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
describe('sendMessage', () => {
|
|
280
|
-
const { sendMessage } = require('../services/streamingService');
|
|
281
|
-
|
|
282
|
-
beforeEach(() => {
|
|
283
|
-
global.fetch = jest.fn();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
afterEach(() => {
|
|
287
|
-
jest.restoreAllMocks();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
function makeStreamResponse(chunks) {
|
|
291
|
-
let index = 0;
|
|
292
|
-
return {
|
|
293
|
-
ok: true,
|
|
294
|
-
body: {
|
|
295
|
-
getReader() {
|
|
296
|
-
return {
|
|
297
|
-
async read() {
|
|
298
|
-
if (index >= chunks.length) {
|
|
299
|
-
return { done: true, value: undefined };
|
|
300
|
-
}
|
|
301
|
-
const value = new TextEncoder().encode(chunks[index++]);
|
|
302
|
-
return { done: false, value };
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
it('sends message and yields packets', async () => {
|
|
311
|
-
const chunk = JSON.stringify({
|
|
312
|
-
ind: 1,
|
|
313
|
-
obj: { type: 'message_delta', content: 'hello' },
|
|
314
|
-
});
|
|
315
|
-
global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
|
|
316
|
-
|
|
317
|
-
const allPackets = [];
|
|
318
|
-
for await (const packets of sendMessage({
|
|
319
|
-
message: 'test',
|
|
320
|
-
chatSessionId: 'session-1',
|
|
321
|
-
parentMessageId: null,
|
|
322
|
-
regenerate: false,
|
|
323
|
-
filters: null,
|
|
324
|
-
selectedDocumentIds: [],
|
|
325
|
-
})) {
|
|
326
|
-
allPackets.push(...packets);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
expect(allPackets).toHaveLength(1);
|
|
330
|
-
expect(allPackets[0].obj.content).toBe('hello');
|
|
331
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
332
|
-
'/_da/chat/send-message',
|
|
333
|
-
expect.objectContaining({ method: 'POST' }),
|
|
334
|
-
);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('uses _rq middleware for related questions', async () => {
|
|
338
|
-
const chunk = JSON.stringify({
|
|
339
|
-
ind: 1,
|
|
340
|
-
obj: { type: 'stop' },
|
|
341
|
-
});
|
|
342
|
-
global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
|
|
343
|
-
|
|
344
|
-
const allPackets = [];
|
|
345
|
-
for await (const packets of sendMessage(
|
|
346
|
-
{
|
|
347
|
-
message: 'test',
|
|
348
|
-
chatSessionId: 'session-1',
|
|
349
|
-
parentMessageId: null,
|
|
350
|
-
regenerate: false,
|
|
351
|
-
filters: null,
|
|
352
|
-
selectedDocumentIds: [],
|
|
353
|
-
},
|
|
354
|
-
true,
|
|
355
|
-
)) {
|
|
356
|
-
allPackets.push(...packets);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
360
|
-
'/_rq/chat/send-message',
|
|
361
|
-
expect.objectContaining({ method: 'POST' }),
|
|
362
|
-
);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('throws on failed response', async () => {
|
|
366
|
-
global.fetch.mockResolvedValue({
|
|
367
|
-
ok: false,
|
|
368
|
-
json: () => Promise.resolve({ message: 'Bad request' }),
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
const gen = sendMessage({
|
|
372
|
-
message: 'test',
|
|
373
|
-
chatSessionId: 'session-1',
|
|
374
|
-
parentMessageId: null,
|
|
375
|
-
regenerate: false,
|
|
376
|
-
filters: null,
|
|
377
|
-
selectedDocumentIds: [],
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
await expect(gen.next()).rejects.toThrow('Failed to send message');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('includes selectedDocumentIds in payload when provided', async () => {
|
|
384
|
-
const chunk = JSON.stringify({
|
|
385
|
-
ind: 1,
|
|
386
|
-
obj: { type: 'stop' },
|
|
387
|
-
});
|
|
388
|
-
global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
|
|
389
|
-
|
|
390
|
-
// eslint-disable-next-line no-unused-vars
|
|
391
|
-
for await (const _ of sendMessage({
|
|
392
|
-
message: 'test',
|
|
393
|
-
chatSessionId: 'session-1',
|
|
394
|
-
parentMessageId: null,
|
|
395
|
-
regenerate: false,
|
|
396
|
-
filters: null,
|
|
397
|
-
selectedDocumentIds: ['doc1', 'doc2'],
|
|
398
|
-
})) {
|
|
399
|
-
// consume
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
403
|
-
expect(body.search_doc_ids).toEqual(['doc1', 'doc2']);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('includes LLM override when temperature is set', async () => {
|
|
407
|
-
const chunk = JSON.stringify({
|
|
408
|
-
ind: 1,
|
|
409
|
-
obj: { type: 'stop' },
|
|
410
|
-
});
|
|
411
|
-
global.fetch.mockResolvedValue(makeStreamResponse([chunk]));
|
|
412
|
-
|
|
413
|
-
// eslint-disable-next-line no-unused-vars
|
|
414
|
-
for await (const _ of sendMessage({
|
|
415
|
-
message: 'test',
|
|
416
|
-
chatSessionId: 'session-1',
|
|
417
|
-
parentMessageId: null,
|
|
418
|
-
regenerate: false,
|
|
419
|
-
filters: null,
|
|
420
|
-
selectedDocumentIds: [],
|
|
421
|
-
temperature: 0.7,
|
|
422
|
-
modelVersion: 'gpt-4',
|
|
423
|
-
})) {
|
|
424
|
-
// consume
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const body = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
428
|
-
expect(body.llm_override).toEqual(
|
|
429
|
-
expect.objectContaining({
|
|
430
|
-
temperature: 0.7,
|
|
431
|
-
model_version: 'gpt-4',
|
|
432
|
-
}),
|
|
433
|
-
);
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// ── regenerateMessage ──────────────────────────────────────────────────
|
|
438
|
-
|
|
439
|
-
describe('regenerateMessage', () => {
|
|
440
|
-
beforeEach(() => {
|
|
441
|
-
global.fetch = jest.fn();
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
afterEach(() => {
|
|
445
|
-
jest.restoreAllMocks();
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('sends regenerate request', async () => {
|
|
449
|
-
global.fetch.mockResolvedValue({ ok: true });
|
|
450
|
-
await regenerateMessage(1, 2);
|
|
451
|
-
|
|
452
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
453
|
-
'/_da/chat/regenerate-message',
|
|
454
|
-
expect.objectContaining({
|
|
455
|
-
method: 'POST',
|
|
456
|
-
body: JSON.stringify({ message_id: 1, chat_session_id: 2 }),
|
|
457
|
-
}),
|
|
458
|
-
);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it('throws on failed response', async () => {
|
|
462
|
-
global.fetch.mockResolvedValue({ ok: false });
|
|
463
|
-
await expect(regenerateMessage(1, 2)).rejects.toThrow(
|
|
464
|
-
'Failed to regenerate message',
|
|
465
|
-
);
|
|
466
|
-
});
|
|
467
|
-
});
|