@eeacms/volto-eea-chatbot 3.0.0 → 4.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.
@@ -1,117 +0,0 @@
1
- const MAX_CONTEXT_SEGMENTS_PER_CHUNK = 100;
2
-
3
- /**
4
- * Annotate a set of sentences with a given annotation character.
5
- * @param sentences A list of sentences to annotate.
6
- * @param annotationChar The character to use for annotation.
7
- * @returns The annotated string with annotation characters + sentence number.
8
- */
9
- export function annotate(sentences, annotationChar, excludeIndices) {
10
- return sentences
11
- .map((sentence, i) => {
12
- const id = i + 1;
13
- if (excludeIndices && excludeIndices.has(id)) {
14
- return '';
15
- }
16
- return `<|${annotationChar}${id}|><${sentence}><end||${annotationChar}>`;
17
- })
18
- .join('');
19
- }
20
-
21
- /**
22
- * Annotates a chunk of indexed sentences with their global IDs.
23
- */
24
- function annotateChunk(chunk, annotationChar) {
25
- return chunk
26
- .map(
27
- ({ sentence, globalId }) =>
28
- `<|${annotationChar}${globalId}|><${sentence}><end||${annotationChar}>`,
29
- )
30
- .join('');
31
- }
32
-
33
- export function getOffsets(originalString, sentences) {
34
- const offsets = new Map();
35
- let stringProgressPointer = 0;
36
- let sentenceId = 1;
37
- for (const sentence of sentences) {
38
- const stringToSearch = originalString.slice(stringProgressPointer);
39
- const startOffset =
40
- stringToSearch.indexOf(sentence) + stringProgressPointer;
41
- const endOffset = startOffset + sentence.length;
42
- stringProgressPointer = endOffset;
43
- offsets.set(sentenceId, { startOffset: startOffset, endOffset: endOffset });
44
- sentenceId++;
45
- }
46
- return offsets;
47
- }
48
-
49
- /**
50
- * Creates multiple HallOumi prompts by chunking context segments.
51
- * Each chunk uses global segment IDs (s5, s42, ...) so no local-to-global
52
- * mapping is needed when merging results.
53
- *
54
- * @returns {{ prompts: Array<{prompt, responseOffsets}> }}
55
- */
56
- export function createChunkedHalloumiPrompts({
57
- indexedContextSentences,
58
- responseSentences,
59
- responseOffsets,
60
- request = 'Make one or more claims about information in the documents.',
61
- excludeResponseIndices,
62
- }) {
63
- // Build response annotation (same for all chunks)
64
- const annotatedResponseSentences = annotate(
65
- responseSentences || [],
66
- 'r',
67
- excludeResponseIndices,
68
- );
69
- const annotatedRequest = `<|request|><${request}><end||request>`;
70
- const annotatedResponse = `<|response|>${annotatedResponseSentences}<end||response>`;
71
-
72
- // Group sentences by source
73
- const sourceGroups = [];
74
- let currentSourceId = null;
75
- for (const s of indexedContextSentences) {
76
- if (s.sourceId !== currentSourceId) {
77
- sourceGroups.push([]);
78
- currentSourceId = s.sourceId;
79
- }
80
- sourceGroups[sourceGroups.length - 1].push(s);
81
- }
82
-
83
- // Pack whole sources into chunks (first-fit decreasing bin packing)
84
- // Sort by size descending so large sources get placed first
85
- const sorted = [...sourceGroups].sort((a, b) => b.length - a.length);
86
- const chunks = [];
87
- const chunkSizes = [];
88
- for (const group of sorted) {
89
- let placed = false;
90
- for (let c = 0; c < chunks.length; c++) {
91
- if (chunkSizes[c] + group.length <= MAX_CONTEXT_SEGMENTS_PER_CHUNK) {
92
- chunks[c].push(...group);
93
- chunkSizes[c] += group.length;
94
- placed = true;
95
- break;
96
- }
97
- }
98
- if (!placed) {
99
- chunks.push([...group]);
100
- chunkSizes.push(group.length);
101
- }
102
- }
103
- if (chunks.length === 0) chunks.push([]);
104
-
105
- // Build one prompt per chunk with global segment IDs
106
- const prompts = chunks.map((chunk) => {
107
- const annotatedContext = `<|context|>${annotateChunk(
108
- chunk,
109
- 's',
110
- )}<end||context>`;
111
- const prompt = `${annotatedContext}${annotatedRequest}${annotatedResponse}`;
112
-
113
- return { prompt, responseOffsets };
114
- });
115
-
116
- return { prompts };
117
- }
@@ -1,184 +0,0 @@
1
- import {
2
- createChunkedHalloumiPrompts,
3
- annotate,
4
- getOffsets,
5
- } from './preprocessing';
6
-
7
- describe('annotate', () => {
8
- it('should annotate multiple sentences correctly', () => {
9
- const sentences = ['Sentence one.', 'Sentence two.'];
10
- const annotationChar = 's';
11
- const expected =
12
- '<|s1|><Sentence one.><end||s><|s2|><Sentence two.><end||s>';
13
- expect(annotate(sentences, annotationChar)).toEqual(expected);
14
- });
15
-
16
- it('should handle an empty array of sentences', () => {
17
- const sentences = [];
18
- const annotationChar = 's';
19
- expect(annotate(sentences, annotationChar)).toEqual('');
20
- });
21
-
22
- it('should annotate a single sentence', () => {
23
- const sentences = ['Single sentence.'];
24
- const annotationChar = 'r';
25
- const expected = '<|r1|><Single sentence.><end||r>';
26
- expect(annotate(sentences, annotationChar)).toEqual(expected);
27
- });
28
-
29
- it('should use different annotation characters', () => {
30
- const sentences = ['Hello.'];
31
- const annotationChar = 'x';
32
- const expected = '<|x1|><Hello.><end||x>';
33
- expect(annotate(sentences, annotationChar)).toEqual(expected);
34
- });
35
- });
36
-
37
- describe('getOffsets', () => {
38
- it('should calculate correct offsets for multiple sentences', () => {
39
- const originalString = 'First sentence. Second sentence. Third sentence.';
40
- const sentences = [
41
- 'First sentence. ',
42
- 'Second sentence. ',
43
- 'Third sentence.',
44
- ];
45
- const expected = new Map([
46
- [1, { startOffset: 0, endOffset: 16 }],
47
- [2, { startOffset: 16, endOffset: 33 }],
48
- [3, { startOffset: 33, endOffset: 48 }],
49
- ]);
50
- expect(getOffsets(originalString, sentences)).toEqual(expected);
51
- });
52
-
53
- it('should handle empty original string and sentences', () => {
54
- const originalString = '';
55
- const sentences = [];
56
- expect(getOffsets(originalString, sentences)).toEqual(new Map());
57
- });
58
-
59
- it('should handle original string matching sentences exactly', () => {
60
- const originalString = 'Hello world.';
61
- const sentences = ['Hello world.'];
62
- const expected = new Map([[1, { startOffset: 0, endOffset: 12 }]]);
63
- expect(getOffsets(originalString, sentences)).toEqual(expected);
64
- });
65
-
66
- it('should handle original string with leading/trailing spaces', () => {
67
- const originalString = ' Sentence one. Sentence two. ';
68
- const sentences = ['Sentence one. ', 'Sentence two. '];
69
- const expected = new Map([
70
- [1, { startOffset: 2, endOffset: 17 }],
71
- [2, { startOffset: 17, endOffset: 32 }],
72
- ]);
73
- expect(getOffsets(originalString, sentences)).toEqual(expected);
74
- });
75
-
76
- it('should handle sentences with special characters', () => {
77
- const originalString = 'Sentence with !@#$%^&*() special characters.';
78
- const sentences = ['Sentence with !@#$%^&*() special characters.'];
79
- const expected = new Map([[1, { startOffset: 0, endOffset: 44 }]]);
80
- expect(getOffsets(originalString, sentences)).toEqual(expected);
81
- });
82
- });
83
-
84
- describe('createChunkedHalloumiPrompts', () => {
85
- it('should create a single chunk for small input', () => {
86
- const indexedContextSentences = [
87
- { sentence: 'Context one.', sourceId: 1, globalId: 1 },
88
- { sentence: 'Context two.', sourceId: 1, globalId: 2 },
89
- ];
90
- const responseSentences = ['Response one.', 'Response two.'];
91
- const responseOffsets = new Map([
92
- [1, { startOffset: 0, endOffset: 13 }],
93
- [2, { startOffset: 14, endOffset: 28 }],
94
- ]);
95
-
96
- const { prompts } = createChunkedHalloumiPrompts({
97
- indexedContextSentences,
98
- responseSentences,
99
- responseOffsets,
100
- });
101
-
102
- expect(prompts).toHaveLength(1);
103
- expect(prompts[0].prompt).toContain('<|s1|><Context one.><end||s>');
104
- expect(prompts[0].prompt).toContain('<|s2|><Context two.><end||s>');
105
- expect(prompts[0].prompt).toContain('<|r1|><Response one.><end||r>');
106
- expect(prompts[0].prompt).toContain('<|r2|><Response two.><end||r>');
107
- });
108
-
109
- it('should handle empty context', () => {
110
- const responseSentences = ['Response.'];
111
- const responseOffsets = new Map([[1, { startOffset: 0, endOffset: 9 }]]);
112
-
113
- const { prompts } = createChunkedHalloumiPrompts({
114
- indexedContextSentences: [],
115
- responseSentences,
116
- responseOffsets,
117
- });
118
-
119
- expect(prompts).toHaveLength(1);
120
- expect(prompts[0].prompt).toContain('<|context|><end||context>');
121
- });
122
-
123
- it('should exclude response sentences based on excludeResponseIndices', () => {
124
- const indexedContextSentences = [
125
- { sentence: 'Context.', sourceId: 1, globalId: 1 },
126
- ];
127
- const responseSentences = ['Keep this.', 'Skip this.', 'Keep too.'];
128
- const responseOffsets = new Map([
129
- [1, { startOffset: 0, endOffset: 10 }],
130
- [2, { startOffset: 11, endOffset: 21 }],
131
- [3, { startOffset: 22, endOffset: 31 }],
132
- ]);
133
- const excludeResponseIndices = new Set([2]);
134
-
135
- const { prompts } = createChunkedHalloumiPrompts({
136
- indexedContextSentences,
137
- responseSentences,
138
- responseOffsets,
139
- excludeResponseIndices,
140
- });
141
-
142
- expect(prompts[0].prompt).toContain('<|r1|><Keep this.><end||r>');
143
- expect(prompts[0].prompt).not.toContain('Skip this.');
144
- expect(prompts[0].prompt).toContain('<|r3|><Keep too.><end||r>');
145
- });
146
-
147
- it('should keep sources together with bin packing', () => {
148
- // 3 sources: 60 + 60 + 30 = 150 sentences
149
- // Should pack into 2 chunks: (60+30) and (60) or similar
150
- const indexed = [];
151
- let gid = 1;
152
- // Source 1: 60 sentences
153
- for (let i = 0; i < 60; i++) {
154
- indexed.push({ sentence: `S1-${i}`, sourceId: 1, globalId: gid++ });
155
- }
156
- // Source 2: 60 sentences
157
- for (let i = 0; i < 60; i++) {
158
- indexed.push({ sentence: `S2-${i}`, sourceId: 2, globalId: gid++ });
159
- }
160
- // Source 3: 30 sentences
161
- for (let i = 0; i < 30; i++) {
162
- indexed.push({ sentence: `S3-${i}`, sourceId: 3, globalId: gid++ });
163
- }
164
-
165
- const { prompts } = createChunkedHalloumiPrompts({
166
- indexedContextSentences: indexed,
167
- responseSentences: ['Claim.'],
168
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 6 }]]),
169
- });
170
-
171
- expect(prompts).toHaveLength(2);
172
-
173
- // Each source should be entirely within one chunk
174
- for (const prompt of prompts) {
175
- const s1Count = (prompt.prompt.match(/S1-/g) || []).length;
176
- const s2Count = (prompt.prompt.match(/S2-/g) || []).length;
177
- const s3Count = (prompt.prompt.match(/S3-/g) || []).length;
178
-
179
- if (s1Count > 0) expect(s1Count).toBe(60);
180
- if (s2Count > 0) expect(s2Count).toBe(60);
181
- if (s3Count > 0) expect(s3Count).toBe(30);
182
- }
183
- });
184
- });