@eeacms/volto-eea-chatbot 1.0.9 → 1.0.11

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -752
  2. package/package.json +1 -1
  3. package/razzle.extend.js +8 -4
  4. package/src/ChatBlock/ChatBlockView.jsx +26 -2
  5. package/src/ChatBlock/chat/AIMessage.tsx +5 -1
  6. package/src/ChatBlock/chat/ChatWindow.tsx +12 -3
  7. package/src/ChatBlock/components/AutoResizeTextarea.jsx +2 -1
  8. package/src/ChatBlock/components/QualityCheckToggle.jsx +1 -1
  9. package/src/ChatBlock/hooks/useChatController.ts +10 -2
  10. package/src/ChatBlock/index.js +1 -1
  11. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +8 -0
  12. package/src/ChatBlock/services/streamingService.ts +30 -26
  13. package/src/ChatBlock/style.less +3 -1
  14. package/src/ChatBlock/tests/ChatMessage.test.jsx +75 -0
  15. package/src/ChatBlock/tests/ClaimModal.test.jsx +136 -0
  16. package/src/ChatBlock/tests/ClaimSegments.test.jsx +206 -0
  17. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +241 -0
  18. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +161 -0
  19. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +178 -0
  20. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +227 -0
  21. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +134 -0
  22. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +163 -0
  23. package/src/ChatBlock/tests/RenderClaimView.test.jsx +191 -0
  24. package/src/ChatBlock/tests/RendererComponent.test.jsx +139 -0
  25. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +295 -0
  26. package/src/ChatBlock/tests/SourceChip.test.jsx +108 -0
  27. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +135 -0
  28. package/src/ChatBlock/tests/UserMessage.test.jsx +83 -0
  29. package/src/ChatBlock/tests/WebResultIcon.test.jsx +61 -0
  30. package/src/ChatBlock/tests/citations.test.js +114 -0
  31. package/src/ChatBlock/tests/messageProcessor.test.jsx +285 -1
  32. package/src/ChatBlock/tests/packetUtils.test.js +158 -0
  33. package/src/ChatBlock/tests/streamingService.test.js +467 -0
  34. package/src/ChatBlock/tests/useChatController.test.jsx +268 -0
  35. package/src/ChatBlock/tests/useChatStreaming.test.jsx +163 -0
  36. package/src/ChatBlock/tests/useMarked.test.jsx +107 -0
  37. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +150 -0
  38. package/src/ChatBlock/tests/useScrollonStream.test.jsx +121 -0
  39. package/src/ChatBlock/tests/utils.test.jsx +241 -0
  40. package/src/ChatBlock/tests/withOnyxData.test.jsx +81 -0
  41. package/src/ChatBlock/utils/citations.ts +1 -1
  42. package/src/halloumi/generative.js +1 -0
  43. package/src/halloumi/generative.test.js +278 -0
  44. package/src/halloumi/middleware.test.js +69 -0
  45. package/src/index.js +1 -0
  46. package/src/middleware.js +21 -13
  47. package/src/middleware.test.js +221 -0
@@ -5,7 +5,7 @@ describe('MessageProcessor', () => {
5
5
  let processor;
6
6
 
7
7
  beforeEach(() => {
8
- processor = new MessageProcessor();
8
+ processor = new MessageProcessor(1, null);
9
9
  });
10
10
 
11
11
  it('should initialize with empty state', () => {
@@ -151,4 +151,288 @@ describe('MessageProcessor', () => {
151
151
  expect(result.documents).toEqual(null);
152
152
  expect(result.citations || {}).toEqual({});
153
153
  });
154
+
155
+ it('should process MESSAGE_END_ID_INFO packets and expose messageIds', () => {
156
+ processor.addPackets([
157
+ {
158
+ ind: -1,
159
+ obj: {
160
+ type: PacketType.MESSAGE_END_ID_INFO,
161
+ user_message_id: 42,
162
+ reserved_assistant_message_id: 43,
163
+ },
164
+ },
165
+ ]);
166
+
167
+ expect(processor.messageIds).toEqual({
168
+ userMessageId: 42,
169
+ assistantMessageId: 43,
170
+ });
171
+ });
172
+
173
+ it('should set isFinalMessageComing on MESSAGE_START', () => {
174
+ expect(processor.isFinalMessageComing).toBe(false);
175
+
176
+ processor.addPackets([
177
+ {
178
+ ind: 0,
179
+ obj: {
180
+ type: PacketType.MESSAGE_START,
181
+ id: 'msg1',
182
+ content: 'Hi',
183
+ final_documents: null,
184
+ },
185
+ },
186
+ ]);
187
+
188
+ expect(processor.isFinalMessageComing).toBe(true);
189
+ });
190
+
191
+ it('should set isFinalMessageComing on IMAGE_GENERATION_TOOL_START', () => {
192
+ processor.addPackets([
193
+ {
194
+ ind: 0,
195
+ obj: { type: PacketType.IMAGE_GENERATION_TOOL_START },
196
+ },
197
+ ]);
198
+
199
+ expect(processor.isFinalMessageComing).toBe(true);
200
+ });
201
+
202
+ it('should handle SECTION_END with synthetic packet mapping', () => {
203
+ // First start an index, then send SECTION_END
204
+ processor.addPackets([
205
+ {
206
+ ind: 5,
207
+ obj: {
208
+ type: PacketType.SEARCH_TOOL_START,
209
+ },
210
+ },
211
+ ]);
212
+
213
+ // SECTION_END should get remapped to ind=5
214
+ processor.addPackets([
215
+ {
216
+ ind: -1,
217
+ obj: { type: PacketType.SECTION_END },
218
+ },
219
+ ]);
220
+
221
+ const result = processor.getMessage();
222
+ // The SECTION_END should be stored with ind=5
223
+ const sectionEndPackets = result.packets.filter(
224
+ (p) => p.obj.type === PacketType.SECTION_END,
225
+ );
226
+ expect(sectionEndPackets).toHaveLength(1);
227
+ expect(sectionEndPackets[0].ind).toBe(5);
228
+ });
229
+
230
+ it('should skip SECTION_END when no indices started', () => {
231
+ processor.addPackets([
232
+ {
233
+ ind: -1,
234
+ obj: { type: PacketType.SECTION_END },
235
+ },
236
+ ]);
237
+
238
+ const result = processor.getMessage();
239
+ expect(result.packets).toHaveLength(0);
240
+ });
241
+
242
+ it('should extract tool call from search tool packets on complete', () => {
243
+ processor.addPackets([
244
+ {
245
+ ind: 0,
246
+ obj: { type: PacketType.SEARCH_TOOL_START },
247
+ },
248
+ {
249
+ ind: 0,
250
+ obj: {
251
+ type: PacketType.SEARCH_TOOL_DELTA,
252
+ documents: [
253
+ {
254
+ document_id: 'doc1',
255
+ semantic_identifier: 'Doc 1',
256
+ link: 'http://example.com',
257
+ blurb: 'some content',
258
+ },
259
+ ],
260
+ },
261
+ },
262
+ {
263
+ ind: -1,
264
+ obj: { type: PacketType.SECTION_END },
265
+ },
266
+ {
267
+ ind: 1,
268
+ obj: {
269
+ type: PacketType.MESSAGE_START,
270
+ id: 'msg1',
271
+ content: 'Answer',
272
+ final_documents: null,
273
+ },
274
+ },
275
+ { ind: 2, obj: { type: PacketType.STOP } },
276
+ ]);
277
+
278
+ const result = processor.getMessage();
279
+ expect(result.isComplete).toBe(true);
280
+ expect(result.toolCall).not.toBeNull();
281
+ expect(result.toolCall.tool_name).toBe('run_search');
282
+ expect(result.toolCall.tool_result).toHaveLength(1);
283
+ expect(result.toolCall.tool_result[0].document_id).toBe('doc1');
284
+ });
285
+
286
+ it('should return null toolCall when no search tool packets', () => {
287
+ processor.addPackets([
288
+ {
289
+ ind: 0,
290
+ obj: { type: PacketType.MESSAGE_DELTA, content: 'Hello' },
291
+ },
292
+ { ind: 1, obj: { type: PacketType.STOP } },
293
+ ]);
294
+
295
+ const result = processor.getMessage();
296
+ expect(result.isComplete).toBe(true);
297
+ expect(result.toolCall).toBeNull();
298
+ });
299
+
300
+ it('should deduplicate documents by document_id', () => {
301
+ const doc = {
302
+ document_id: 'dup1',
303
+ semantic_identifier: 'Doc',
304
+ link: 'http://example.com',
305
+ };
306
+
307
+ processor.addPackets([
308
+ { ind: 0, obj: { type: PacketType.SEARCH_TOOL_DELTA, documents: [doc] } },
309
+ { ind: 0, obj: { type: PacketType.SEARCH_TOOL_DELTA, documents: [doc] } },
310
+ ]);
311
+
312
+ const result = processor.getMessage();
313
+ expect(result.documents).toHaveLength(1);
314
+ });
315
+
316
+ it('should deduplicate citations by citation_num', () => {
317
+ processor.addPackets([
318
+ {
319
+ ind: 0,
320
+ obj: {
321
+ type: PacketType.CITATION_DELTA,
322
+ citations: [{ citation_num: 1, document_id: 'doc1' }],
323
+ },
324
+ },
325
+ {
326
+ ind: 0,
327
+ obj: {
328
+ type: PacketType.CITATION_DELTA,
329
+ citations: [{ citation_num: 1, document_id: 'doc2' }],
330
+ },
331
+ },
332
+ ]);
333
+
334
+ const result = processor.getMessage();
335
+ // First citation wins
336
+ expect(result.citations).toEqual({ 1: 'doc1' });
337
+ });
338
+
339
+ it('should accumulate text from MESSAGE_START and MESSAGE_DELTA', () => {
340
+ processor.addPackets([
341
+ {
342
+ ind: 0,
343
+ obj: {
344
+ type: PacketType.MESSAGE_START,
345
+ id: 'msg1',
346
+ content: 'Hello ',
347
+ final_documents: null,
348
+ },
349
+ },
350
+ {
351
+ ind: 0,
352
+ obj: { type: PacketType.MESSAGE_DELTA, content: 'world' },
353
+ },
354
+ ]);
355
+
356
+ const result = processor.getMessage();
357
+ expect(result.message).toBe('Hello world');
358
+ });
359
+
360
+ it('should process documents from FETCH_TOOL_START', () => {
361
+ processor.addPackets([
362
+ {
363
+ ind: 0,
364
+ obj: {
365
+ type: PacketType.FETCH_TOOL_START,
366
+ documents: [
367
+ {
368
+ document_id: 'fetch1',
369
+ semantic_identifier: 'Fetched',
370
+ link: 'http://fetch.com',
371
+ },
372
+ ],
373
+ queries: null,
374
+ },
375
+ },
376
+ ]);
377
+
378
+ const result = processor.getMessage();
379
+ expect(result.documents).toHaveLength(1);
380
+ expect(result.documents[0].document_id).toBe('fetch1');
381
+ });
382
+
383
+ it('should process documents from MESSAGE_START final_documents', () => {
384
+ processor.addPackets([
385
+ {
386
+ ind: 0,
387
+ obj: {
388
+ type: PacketType.MESSAGE_START,
389
+ id: 'msg1',
390
+ content: 'Hello',
391
+ final_documents: [
392
+ {
393
+ document_id: 'fd1',
394
+ semantic_identifier: 'Final Doc',
395
+ link: 'http://final.com',
396
+ },
397
+ ],
398
+ },
399
+ },
400
+ ]);
401
+
402
+ const result = processor.getMessage();
403
+ expect(result.documents).toHaveLength(1);
404
+ expect(result.documents[0].document_id).toBe('fd1');
405
+ });
406
+
407
+ it('should mark complete on ERROR packet', () => {
408
+ processor.addPackets([
409
+ {
410
+ ind: 0,
411
+ obj: { type: PacketType.ERROR, error: 'fail' },
412
+ },
413
+ ]);
414
+
415
+ expect(processor.isComplete).toBe(true);
416
+ });
417
+
418
+ it('should return null toolCall when search has no documents', () => {
419
+ processor.addPackets([
420
+ { ind: 0, obj: { type: PacketType.SEARCH_TOOL_START } },
421
+ {
422
+ ind: 0,
423
+ obj: { type: PacketType.SEARCH_TOOL_DELTA, documents: [] },
424
+ },
425
+ { ind: 1, obj: { type: PacketType.STOP } },
426
+ ]);
427
+
428
+ const result = processor.getMessage();
429
+ expect(result.toolCall).toBeNull();
430
+ });
431
+
432
+ it('should set nodeId and parentNodeId from constructor', () => {
433
+ const p = new MessageProcessor(10, 5);
434
+ const msg = p.getMessage();
435
+ expect(msg.nodeId).toBe(10);
436
+ expect(msg.parentNodeId).toBe(5);
437
+ });
154
438
  });
@@ -0,0 +1,158 @@
1
+ import {
2
+ getSynteticPacket,
3
+ isToolPacket,
4
+ isDisplayPacket,
5
+ isFinalAnswerComplete,
6
+ } from '../services/packetUtils';
7
+ import { PacketType } from '../types/streamingModels';
8
+
9
+ describe('packetUtils', () => {
10
+ describe('getSynteticPacket', () => {
11
+ it('creates a synthetic packet with given index and type', () => {
12
+ const packet = getSynteticPacket(1, PacketType.MESSAGE_START);
13
+ expect(packet).toEqual({
14
+ ind: 1,
15
+ obj: { type: PacketType.MESSAGE_START },
16
+ });
17
+ });
18
+
19
+ it('creates packets with different types', () => {
20
+ const searchPacket = getSynteticPacket(2, PacketType.SEARCH_TOOL_START);
21
+ expect(searchPacket.ind).toBe(2);
22
+ expect(searchPacket.obj.type).toBe(PacketType.SEARCH_TOOL_START);
23
+ });
24
+ });
25
+
26
+ describe('isToolPacket', () => {
27
+ it('returns true for SEARCH_TOOL_START packets', () => {
28
+ const packet = { ind: 1, obj: { type: PacketType.SEARCH_TOOL_START } };
29
+ expect(isToolPacket(packet)).toBe(true);
30
+ });
31
+
32
+ it('returns true for SEARCH_TOOL_DELTA packets', () => {
33
+ const packet = { ind: 1, obj: { type: PacketType.SEARCH_TOOL_DELTA } };
34
+ expect(isToolPacket(packet)).toBe(true);
35
+ });
36
+
37
+ it('returns true for CUSTOM_TOOL_START packets', () => {
38
+ const packet = { ind: 1, obj: { type: PacketType.CUSTOM_TOOL_START } };
39
+ expect(isToolPacket(packet)).toBe(true);
40
+ });
41
+
42
+ it('returns true for CUSTOM_TOOL_DELTA packets', () => {
43
+ const packet = { ind: 1, obj: { type: PacketType.CUSTOM_TOOL_DELTA } };
44
+ expect(isToolPacket(packet)).toBe(true);
45
+ });
46
+
47
+ it('returns true for REASONING_START packets', () => {
48
+ const packet = { ind: 1, obj: { type: PacketType.REASONING_START } };
49
+ expect(isToolPacket(packet)).toBe(true);
50
+ });
51
+
52
+ it('returns true for REASONING_DELTA packets', () => {
53
+ const packet = { ind: 1, obj: { type: PacketType.REASONING_DELTA } };
54
+ expect(isToolPacket(packet)).toBe(true);
55
+ });
56
+
57
+ it('returns true for FETCH_TOOL_START packets', () => {
58
+ const packet = { ind: 1, obj: { type: PacketType.FETCH_TOOL_START } };
59
+ expect(isToolPacket(packet)).toBe(true);
60
+ });
61
+
62
+ it('returns false for MESSAGE_START packets', () => {
63
+ const packet = { ind: 1, obj: { type: PacketType.MESSAGE_START } };
64
+ expect(isToolPacket(packet)).toBe(false);
65
+ });
66
+
67
+ it('returns false for MESSAGE_DELTA packets', () => {
68
+ const packet = { ind: 1, obj: { type: PacketType.MESSAGE_DELTA } };
69
+ expect(isToolPacket(packet)).toBe(false);
70
+ });
71
+
72
+ it('returns false for STOP packets', () => {
73
+ const packet = { ind: 1, obj: { type: PacketType.STOP } };
74
+ expect(isToolPacket(packet)).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe('isDisplayPacket', () => {
79
+ it('returns true for MESSAGE_START packets', () => {
80
+ const packet = { ind: 1, obj: { type: PacketType.MESSAGE_START } };
81
+ expect(isDisplayPacket(packet)).toBe(true);
82
+ });
83
+
84
+ it('returns true for IMAGE_GENERATION_TOOL_START packets', () => {
85
+ const packet = {
86
+ ind: 1,
87
+ obj: { type: PacketType.IMAGE_GENERATION_TOOL_START },
88
+ };
89
+ expect(isDisplayPacket(packet)).toBe(true);
90
+ });
91
+
92
+ it('returns false for SEARCH_TOOL_START packets', () => {
93
+ const packet = { ind: 1, obj: { type: PacketType.SEARCH_TOOL_START } };
94
+ expect(isDisplayPacket(packet)).toBe(false);
95
+ });
96
+
97
+ it('returns false for MESSAGE_DELTA packets', () => {
98
+ const packet = { ind: 1, obj: { type: PacketType.MESSAGE_DELTA } };
99
+ expect(isDisplayPacket(packet)).toBe(false);
100
+ });
101
+ });
102
+
103
+ describe('isFinalAnswerComplete', () => {
104
+ it('returns false when no packets', () => {
105
+ expect(isFinalAnswerComplete([])).toBe(false);
106
+ });
107
+
108
+ it('returns false when no MESSAGE_START packet', () => {
109
+ const packets = [
110
+ { ind: 1, obj: { type: PacketType.MESSAGE_DELTA, content: 'Hello' } },
111
+ { ind: 1, obj: { type: PacketType.SECTION_END } },
112
+ ];
113
+ expect(isFinalAnswerComplete(packets)).toBe(false);
114
+ });
115
+
116
+ it('returns false when MESSAGE_START exists but no matching SECTION_END', () => {
117
+ const packets = [
118
+ {
119
+ ind: 1,
120
+ obj: { type: PacketType.MESSAGE_START, content: '', id: '1' },
121
+ },
122
+ { ind: 1, obj: { type: PacketType.MESSAGE_DELTA, content: 'Hello' } },
123
+ ];
124
+ expect(isFinalAnswerComplete(packets)).toBe(false);
125
+ });
126
+
127
+ it('returns true when MESSAGE_START and matching SECTION_END exist', () => {
128
+ const packets = [
129
+ {
130
+ ind: 1,
131
+ obj: { type: PacketType.MESSAGE_START, content: '', id: '1' },
132
+ },
133
+ { ind: 1, obj: { type: PacketType.MESSAGE_DELTA, content: 'Hello' } },
134
+ { ind: 1, obj: { type: PacketType.SECTION_END } },
135
+ ];
136
+ expect(isFinalAnswerComplete(packets)).toBe(true);
137
+ });
138
+
139
+ it('returns true when IMAGE_GENERATION_TOOL_START and matching SECTION_END exist', () => {
140
+ const packets = [
141
+ { ind: 2, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
142
+ { ind: 2, obj: { type: PacketType.SECTION_END } },
143
+ ];
144
+ expect(isFinalAnswerComplete(packets)).toBe(true);
145
+ });
146
+
147
+ it('returns false when SECTION_END has different index', () => {
148
+ const packets = [
149
+ {
150
+ ind: 1,
151
+ obj: { type: PacketType.MESSAGE_START, content: '', id: '1' },
152
+ },
153
+ { ind: 2, obj: { type: PacketType.SECTION_END } },
154
+ ];
155
+ expect(isFinalAnswerComplete(packets)).toBe(false);
156
+ });
157
+ });
158
+ });