@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,247 +0,0 @@
1
- /**
2
- * Gets the claim id from a subsegment.
3
- * @param subsegment A subsegment string of the form "<|r1|".
4
- * @returns The numeric claim id.
5
- */
6
- function getClaimIdFromSubsegment(subsegment) {
7
- const textClaimId = subsegment.split('|')[1];
8
- const integerClaimId = parseInt(textClaimId.split('r')[1]);
9
- return integerClaimId;
10
- }
11
-
12
- /**
13
- * Gets the citations from a subsegment.
14
- * @param subsegment A subsegment string of the form "|s1|,|s2|,|s3|,|s4|".
15
- * @returns The list of numeric citations.
16
- */
17
- function getClaimCitationsFromSubsegment(subsegment) {
18
- const citationSegments = subsegment.split(',');
19
- const segments = [];
20
- for (const citationSegment of citationSegments) {
21
- const segment = citationSegment.replaceAll('|', '').replaceAll('s', '');
22
- if (segment.includes('-')) {
23
- const segmentRange = segment.split('-');
24
- for (
25
- let i = parseInt(segmentRange[0].trim());
26
- i <= parseInt(segmentRange[1].trim());
27
- i++
28
- ) {
29
- segments.push(i);
30
- }
31
- } else if (segment.includes('to')) {
32
- const segmentRange = segment.split('to');
33
- for (
34
- let i = parseInt(segmentRange[0].trim());
35
- i <= parseInt(segmentRange[1].trim());
36
- i++
37
- ) {
38
- segments.push(i);
39
- }
40
- } else {
41
- const segmentInt = parseInt(segment);
42
- if (!isNaN(segmentInt)) {
43
- segments.push(parseInt(segment));
44
- }
45
- }
46
- }
47
- return segments;
48
- }
49
-
50
- /**
51
- * Gets the support status from a subsegment.
52
- * @param subsegment A subsegment string of the form "|supported|" or "|unsupported|".
53
- * @returns True if the claim is supported, false otherwise.
54
- */
55
- function getSupportStatusFromSubsegment(subsegment) {
56
- return subsegment.startsWith('|supported|');
57
- }
58
-
59
- /**
60
- * Gets a claim from a segment string.
61
- * @param segment A segment string containing all information for claim verification.
62
- * @returns The claim object with all relevant information.
63
- */
64
- function getClaimFromSegment(segment) {
65
- const claim_segments = segment.split('><');
66
- const claimId = getClaimIdFromSubsegment(claim_segments[0]);
67
- // Strip trailing '>' which can occur in malformed segments without subclaims
68
- const claimString = claim_segments[1]?.replace(/>$/, '') || '';
69
-
70
- const subclaims = [];
71
- let claimProgressIndex = 3; // Start at 3 to skip the claim id, claim string and the subclaims tag
72
- for (let i = claimProgressIndex; i < claim_segments.length; i++) {
73
- const subsegment = claim_segments[i];
74
- if (subsegment.startsWith('end||subclaims')) {
75
- claimProgressIndex = i + 1;
76
- break;
77
- } else {
78
- subclaims.push(subsegment);
79
- }
80
- }
81
-
82
- let cite_tag_index = -1;
83
- let explanation_index = -1;
84
- let label_index = -1;
85
- for (let i = claimProgressIndex; i < claim_segments.length; i++) {
86
- const subsegment = claim_segments[i];
87
- if (subsegment.startsWith('|cite|')) {
88
- cite_tag_index = i;
89
- } else if (subsegment.startsWith('|explain|')) {
90
- explanation_index = i + 1;
91
- } else if (
92
- subsegment.startsWith('|supported|') ||
93
- subsegment.startsWith('|unsupported|')
94
- ) {
95
- label_index = i;
96
- }
97
- }
98
-
99
- const segments =
100
- cite_tag_index >= 0
101
- ? getClaimCitationsFromSubsegment(claim_segments[cite_tag_index + 1])
102
- : [];
103
- const explanation =
104
- explanation_index >= 0 ? claim_segments[explanation_index] : '';
105
-
106
- // Default to true if label not found
107
- const supported =
108
- label_index >= 0
109
- ? getSupportStatusFromSubsegment(claim_segments[label_index])
110
- : true;
111
-
112
- const claim = {
113
- claimId,
114
- claimString,
115
- subclaims,
116
- segments,
117
- explanation,
118
- supported,
119
- probabilities: new Map(),
120
- };
121
-
122
- return claim;
123
- }
124
-
125
- /**
126
- * Gets all claims from a response.
127
- * @param response A string containing all claims and their information.
128
- * @returns A list of claim objects.
129
- */
130
- export function getClaimsFromResponse(response) {
131
- // Example response: <|r1|><There is no information about the average lifespan of a giant squid in the deep waters of the Pacific Ocean in the provided document.><|subclaims|><The document contains information about the average lifespan of a giant squid.><The information about giant squid lifespan is related to the Pacific Ocean.><end||subclaims><|cite|><|s1 to s49|><end||cite><|explain|><Upon reviewing the entire document, there is no mention of giant squid or any related topic, including their average lifespan or the Pacific Ocean. The document is focused on international relations, diplomacy, and conflict resolution.><end||explain><|supported|><end||r><|r2|><The document is focused on international relations, diplomacy, and conflict resolution, and does not mention giant squid or any related topic.><|subclaims|><The document is focused on international relations, diplomacy, and conflict resolution.><The document does not mention giant squid or any related topic.><end||subclaims><|cite|><|s1|,|s2|,|s3|,|s4|><end||cite><|explain|><The first four sentences clearly establish the document's focus on international relations, diplomacy, and conflict resolution, and there is no mention of giant squid or any related topic throughout the document.><end||explain><|supported|><end||r><|r3|><The document mentions cats.><|subclaims|><The document makes some mention of cats.><end||subclaims><|cite|><None><end||cite><|explain|><There is no mention of cats anywhere in the document.><end||explain><|unsupported|><end||r>
132
-
133
- // eslint-disable-next-line no-console
134
- // console.log('getClaimsFromResponse', response);
135
- let segments = response.split('<end||r>');
136
- const claims = [];
137
-
138
- for (let i = 0; i < segments.length; i++) {
139
- const segment = segments[i];
140
-
141
- if (segment.length === 0) {
142
- continue;
143
- }
144
-
145
- // Skip segments that don't start with a valid claim marker <|r*|>
146
- if (!segment.match(/^<\|r\d+\|>/)) {
147
- continue;
148
- }
149
-
150
- // Check if this segment is well-formed (has subclaims within it)
151
- // Well-formed: <|r1|><claim><|subclaims|>...<|supported|>...<end||r>
152
- // Malformed: <|r1|><claim><end||r><|subclaims|>...<|supported|>...<end||r>
153
- // The malformed case puts verification data AFTER <end||r>, creating orphan segments
154
- if (segment.includes('<|subclaims|>')) {
155
- // Well-formed segment - process normally
156
- const claim = getClaimFromSegment(segment);
157
- claims.push(claim);
158
- } else {
159
- // Malformed segment - look for orphan data in the next segment
160
- let fullSegment = segment;
161
-
162
- // Look ahead for orphan segment (starts with <|subclaims|>)
163
- if (i + 1 < segments.length) {
164
- const nextSegment = segments[i + 1];
165
- if (nextSegment.startsWith('<|subclaims|>')) {
166
- // Merge: segment ends with '>', orphan starts with '<'
167
- fullSegment = segment + nextSegment;
168
- i++; // Skip the orphan in next iteration
169
- }
170
- }
171
-
172
- const claim = getClaimFromSegment(fullSegment);
173
- claims.push(claim);
174
- }
175
- }
176
-
177
- return claims;
178
- }
179
-
180
- function exp(x) {
181
- return Math.pow(Math.E, x);
182
- }
183
-
184
- function softmax(logits) {
185
- const softmaxes = [];
186
- const exp_values = [];
187
- let total = 0;
188
- for (let i = 0; i < logits.length; i++) {
189
- const exponential = exp(logits[i]);
190
- total += exponential;
191
- exp_values.push(exponential);
192
- }
193
-
194
- for (let i = 0; i < exp_values.length; i++) {
195
- softmaxes.push(exp_values[i] / total);
196
- }
197
- return softmaxes;
198
- }
199
-
200
- function getTokenProbabilitiesFromLogit(logit, tokenChoices) {
201
- const tokenLogits = new Map();
202
- let smallestLogit = 1000000;
203
- for (const token of tokenChoices) {
204
- const tokenLogit = logit.top_logprobs.find(
205
- (logit) => logit.token === token,
206
- );
207
- if (tokenLogit !== undefined) {
208
- smallestLogit = Math.min(smallestLogit, tokenLogit.logprob);
209
- tokenLogits.set(token, tokenLogit.logprob);
210
- }
211
- }
212
-
213
- // If any class tokens aren't present, set them below the smallest logit.
214
- // Their true value is definitely <= the smallest logit value.
215
- for (const token of tokenChoices) {
216
- if (!tokenLogits.has(token)) {
217
- tokenLogits.set(token, smallestLogit - 1e-6);
218
- }
219
- }
220
-
221
- const classTokens = Array.from(tokenLogits.keys());
222
- const logitValues = Array.from(tokenLogits.values());
223
- const softmaxValues = softmax(logitValues);
224
- const tokenProbabilities = new Map();
225
- for (let i = 0; i < classTokens.length; i++) {
226
- tokenProbabilities.set(classTokens[i], softmaxValues[i]);
227
- }
228
-
229
- return tokenProbabilities;
230
- }
231
-
232
- export function getTokenProbabilitiesFromLogits(logits, tokenChoices) {
233
- const tokenProbabilities = [];
234
- for (const logit of logits) {
235
- const tokenPresent = tokenChoices.has(logit.token);
236
- if (!tokenPresent) {
237
- continue;
238
- }
239
-
240
- const tokenProbability = getTokenProbabilitiesFromLogit(
241
- logit,
242
- tokenChoices,
243
- );
244
- tokenProbabilities.push(tokenProbability);
245
- }
246
- return tokenProbabilities;
247
- }
@@ -1,441 +0,0 @@
1
- import {
2
- getClaimsFromResponse,
3
- getTokenProbabilitiesFromLogits,
4
- } from './postprocessing';
5
-
6
- describe('getClaimsFromResponse', () => {
7
- describe('well-formed responses', () => {
8
- it('should parse a single well-formed supported claim', () => {
9
- const response =
10
- '<|r1|><The weather is sunny.><|subclaims|><The weather is being described.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><The document states it is sunny.><end||explain><end||r>';
11
-
12
- const claims = getClaimsFromResponse(response);
13
-
14
- expect(claims).toHaveLength(1);
15
- expect(claims[0].claimId).toBe(1);
16
- expect(claims[0].claimString).toBe('The weather is sunny.');
17
- expect(claims[0].subclaims).toEqual(['The weather is being described.']);
18
- expect(claims[0].segments).toEqual([1]);
19
- expect(claims[0].supported).toBe(true);
20
- expect(claims[0].explanation).toBe('The document states it is sunny.');
21
- });
22
-
23
- it('should parse a single well-formed unsupported claim', () => {
24
- const response =
25
- '<|r1|><The document mentions cats.><|subclaims|><The document makes some mention of cats.><end||subclaims><|cite|><None><end||cite><|unsupported|><|explain|><There is no mention of cats.><end||explain><end||r>';
26
-
27
- const claims = getClaimsFromResponse(response);
28
-
29
- expect(claims).toHaveLength(1);
30
- expect(claims[0].claimId).toBe(1);
31
- expect(claims[0].supported).toBe(false);
32
- expect(claims[0].segments).toEqual([]);
33
- });
34
-
35
- it('should parse multiple well-formed claims', () => {
36
- const response =
37
- '<|r1|><First claim.><|subclaims|><Sub1.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Explanation 1.><end||explain><end||r>' +
38
- '<|r2|><Second claim.><|subclaims|><Sub2.><end||subclaims><|cite|><|s2|,|s3|><end||cite><|unsupported|><|explain|><Explanation 2.><end||explain><end||r>' +
39
- '<|r3|><Third claim.><|subclaims|><Sub3.><end||subclaims><|cite|><|s4|><end||cite><|supported|><|explain|><Explanation 3.><end||explain><end||r>';
40
-
41
- const claims = getClaimsFromResponse(response);
42
-
43
- expect(claims).toHaveLength(3);
44
- expect(claims[0].claimId).toBe(1);
45
- expect(claims[0].supported).toBe(true);
46
- expect(claims[1].claimId).toBe(2);
47
- expect(claims[1].supported).toBe(false);
48
- expect(claims[1].segments).toEqual([2, 3]);
49
- expect(claims[2].claimId).toBe(3);
50
- expect(claims[2].supported).toBe(true);
51
- });
52
-
53
- it('should parse multiple subclaims correctly', () => {
54
- const response =
55
- '<|r1|><Main claim.><|subclaims|><Subclaim one.><Subclaim two.><Subclaim three.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Explanation.><end||explain><end||r>';
56
-
57
- const claims = getClaimsFromResponse(response);
58
-
59
- expect(claims).toHaveLength(1);
60
- expect(claims[0].subclaims).toEqual([
61
- 'Subclaim one.',
62
- 'Subclaim two.',
63
- 'Subclaim three.',
64
- ]);
65
- });
66
- });
67
-
68
- describe('citation parsing', () => {
69
- it('should parse comma-separated citations', () => {
70
- const response =
71
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s1|,|s2|,|s3|,|s4|><end||cite><|supported|><|explain|><Exp.><end||explain><end||r>';
72
-
73
- const claims = getClaimsFromResponse(response);
74
-
75
- expect(claims[0].segments).toEqual([1, 2, 3, 4]);
76
- });
77
-
78
- it('should parse citation ranges with dash', () => {
79
- const response =
80
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s1-s5|><end||cite><|supported|><|explain|><Exp.><end||explain><end||r>';
81
-
82
- const claims = getClaimsFromResponse(response);
83
-
84
- expect(claims[0].segments).toEqual([1, 2, 3, 4, 5]);
85
- });
86
-
87
- it('should parse citation ranges with "to"', () => {
88
- const response =
89
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s1 to s49|><end||cite><|supported|><|explain|><Exp.><end||explain><end||r>';
90
-
91
- const claims = getClaimsFromResponse(response);
92
-
93
- expect(claims[0].segments).toHaveLength(49);
94
- expect(claims[0].segments[0]).toBe(1);
95
- expect(claims[0].segments[48]).toBe(49);
96
- });
97
-
98
- it('should handle "None" citations', () => {
99
- const response =
100
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><None><end||cite><|unsupported|><|explain|><Exp.><end||explain><end||r>';
101
-
102
- const claims = getClaimsFromResponse(response);
103
-
104
- expect(claims[0].segments).toEqual([]);
105
- });
106
-
107
- it('should parse mixed citation formats', () => {
108
- const response =
109
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s1|,|s3-s5|,|s10|><end||cite><|supported|><|explain|><Exp.><end||explain><end||r>';
110
-
111
- const claims = getClaimsFromResponse(response);
112
-
113
- expect(claims[0].segments).toEqual([1, 3, 4, 5, 10]);
114
- });
115
- });
116
-
117
- describe('malformed responses with orphan segments', () => {
118
- it('should merge orphan segment with preceding claim', () => {
119
- // Malformed: subclaims appear after <end||r>
120
- const response =
121
- '<|r1|><First claim with proper format.><|subclaims|><Sub1.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Exp1.><end||explain><end||r>' +
122
- '<|r2|><Second claim without subclaims.><end||r>' +
123
- '<|subclaims|><Sub2.><end||subclaims><|cite|><|s2|><end||cite><|supported|><|explain|><Exp2.><end||explain><end||r>';
124
-
125
- const claims = getClaimsFromResponse(response);
126
-
127
- expect(claims).toHaveLength(2);
128
- expect(claims[0].claimId).toBe(1);
129
- expect(claims[0].claimString).toBe('First claim with proper format.');
130
- expect(claims[0].subclaims).toEqual(['Sub1.']);
131
-
132
- expect(claims[1].claimId).toBe(2);
133
- expect(claims[1].claimString).toBe('Second claim without subclaims.');
134
- expect(claims[1].subclaims).toEqual(['Sub2.']);
135
- expect(claims[1].segments).toEqual([2]);
136
- expect(claims[1].supported).toBe(true);
137
- });
138
-
139
- it('should handle multiple consecutive malformed claims', () => {
140
- const response =
141
- '<|r1|><Claim 1.><end||r>' +
142
- '<|subclaims|><Sub1.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Exp1.><end||explain><end||r>' +
143
- '<|r2|><Claim 2.><end||r>' +
144
- '<|subclaims|><Sub2.><end||subclaims><|cite|><|s2|><end||cite><|unsupported|><|explain|><Exp2.><end||explain><end||r>' +
145
- '<|r3|><Claim 3.><end||r>' +
146
- '<|subclaims|><Sub3.><end||subclaims><|cite|><|s3|><end||cite><|supported|><|explain|><Exp3.><end||explain><end||r>';
147
-
148
- const claims = getClaimsFromResponse(response);
149
-
150
- expect(claims).toHaveLength(3);
151
- expect(claims[0].claimId).toBe(1);
152
- expect(claims[0].subclaims).toEqual(['Sub1.']);
153
- expect(claims[0].supported).toBe(true);
154
-
155
- expect(claims[1].claimId).toBe(2);
156
- expect(claims[1].subclaims).toEqual(['Sub2.']);
157
- expect(claims[1].supported).toBe(false);
158
-
159
- expect(claims[2].claimId).toBe(3);
160
- expect(claims[2].subclaims).toEqual(['Sub3.']);
161
- expect(claims[2].supported).toBe(true);
162
- });
163
-
164
- it('should skip orphan segments that are not associated with claims', () => {
165
- const response =
166
- '<|subclaims|><Orphan subclaim.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Orphan exp.><end||explain><end||r>' +
167
- '<|r1|><Valid claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s2|><end||cite><|supported|><|explain|><Exp.><end||explain><end||r>';
168
-
169
- const claims = getClaimsFromResponse(response);
170
-
171
- expect(claims).toHaveLength(1);
172
- expect(claims[0].claimId).toBe(1);
173
- expect(claims[0].claimString).toBe('Valid claim.');
174
- });
175
- });
176
-
177
- describe('edge cases', () => {
178
- it('should return empty array for empty response', () => {
179
- const claims = getClaimsFromResponse('');
180
- expect(claims).toEqual([]);
181
- });
182
-
183
- it('should return empty array for response with only end tags', () => {
184
- const claims = getClaimsFromResponse('<end||r><end||r><end||r>');
185
- expect(claims).toEqual([]);
186
- });
187
-
188
- it('should handle response without cite tag', () => {
189
- const response =
190
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|supported|><|explain|><Exp.><end||explain><end||r>';
191
-
192
- const claims = getClaimsFromResponse(response);
193
-
194
- expect(claims).toHaveLength(1);
195
- expect(claims[0].segments).toEqual([]);
196
- });
197
-
198
- it('should handle response without explain tag', () => {
199
- const response =
200
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s1|><end||cite><|supported|><end||r>';
201
-
202
- const claims = getClaimsFromResponse(response);
203
-
204
- expect(claims).toHaveLength(1);
205
- expect(claims[0].explanation).toBe('');
206
- });
207
-
208
- it('should handle response without supported/unsupported tag (defaults to true)', () => {
209
- const response =
210
- '<|r1|><Claim.><|subclaims|><Sub.><end||subclaims><|cite|><|s1|><end||cite><|explain|><Exp.><end||explain><end||r>';
211
-
212
- const claims = getClaimsFromResponse(response);
213
-
214
- expect(claims).toHaveLength(1);
215
- expect(claims[0].supported).toBe(true);
216
- });
217
-
218
- it('should handle malformed claim without orphan (missing all verification data)', () => {
219
- const response = '<|r1|><Claim without any verification data.><end||r>';
220
-
221
- const claims = getClaimsFromResponse(response);
222
-
223
- expect(claims).toHaveLength(1);
224
- expect(claims[0].claimId).toBe(1);
225
- expect(claims[0].claimString).toBe(
226
- 'Claim without any verification data.',
227
- );
228
- expect(claims[0].subclaims).toEqual([]);
229
- expect(claims[0].segments).toEqual([]);
230
- expect(claims[0].explanation).toBe('');
231
- expect(claims[0].supported).toBe(true); // defaults to true
232
- });
233
-
234
- it('should handle claims with special characters in content', () => {
235
- const response =
236
- '<|r1|><CO₂ emissions ≈ 417 Mt (–30% vs. 1990).><|subclaims|><Emissions data with special chars: €, £, ¥.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Unicode: émissions, naïve.><end||explain><end||r>';
237
-
238
- const claims = getClaimsFromResponse(response);
239
-
240
- expect(claims).toHaveLength(1);
241
- expect(claims[0].claimString).toBe(
242
- 'CO₂ emissions ≈ 417 Mt (–30% vs. 1990).',
243
- );
244
- expect(claims[0].subclaims).toEqual([
245
- 'Emissions data with special chars: €, £, ¥.',
246
- ]);
247
- });
248
-
249
- it('should handle claims with markdown table content', () => {
250
- const response =
251
- '<|r1|><| Year | Emissions |><|subclaims|><Table data.><end||subclaims><|cite|><|s1|><end||cite><|supported|><|explain|><Contains table.><end||explain><end||r>';
252
-
253
- const claims = getClaimsFromResponse(response);
254
-
255
- expect(claims).toHaveLength(1);
256
- expect(claims[0].claimString).toBe('| Year | Emissions |');
257
- });
258
- });
259
-
260
- describe('real-world examples', () => {
261
- it('should parse the example from the code comments', () => {
262
- const response =
263
- '<|r1|><There is no information about the average lifespan of a giant squid in the deep waters of the Pacific Ocean in the provided document.><|subclaims|><The document contains information about the average lifespan of a giant squid.><The information about giant squid lifespan is related to the Pacific Ocean.><end||subclaims><|cite|><|s1 to s49|><end||cite><|explain|><Upon reviewing the entire document, there is no mention of giant squid or any related topic, including their average lifespan or the Pacific Ocean. The document is focused on international relations, diplomacy, and conflict resolution.><end||explain><|supported|><end||r>' +
264
- "<|r2|><The document is focused on international relations, diplomacy, and conflict resolution, and does not mention giant squid or any related topic.><|subclaims|><The document is focused on international relations, diplomacy, and conflict resolution.><The document does not mention giant squid or any related topic.><end||subclaims><|cite|><|s1|,|s2|,|s3|,|s4|><end||cite><|explain|><The first four sentences clearly establish the document's focus on international relations, diplomacy, and conflict resolution, and there is no mention of giant squid or any related topic throughout the document.><end||explain><|supported|><end||r>" +
265
- '<|r3|><The document mentions cats.><|subclaims|><The document makes some mention of cats.><end||subclaims><|cite|><None><end||cite><|explain|><There is no mention of cats anywhere in the document.><end||explain><|unsupported|><end||r>';
266
-
267
- const claims = getClaimsFromResponse(response);
268
-
269
- expect(claims).toHaveLength(3);
270
-
271
- // Claim 1
272
- expect(claims[0].claimId).toBe(1);
273
- expect(claims[0].subclaims).toHaveLength(2);
274
- expect(claims[0].segments).toHaveLength(49);
275
- expect(claims[0].supported).toBe(true);
276
-
277
- // Claim 2
278
- expect(claims[1].claimId).toBe(2);
279
- expect(claims[1].segments).toEqual([1, 2, 3, 4]);
280
- expect(claims[1].supported).toBe(true);
281
-
282
- // Claim 3
283
- expect(claims[2].claimId).toBe(3);
284
- expect(claims[2].segments).toEqual([]);
285
- expect(claims[2].supported).toBe(false);
286
- });
287
- });
288
- });
289
-
290
- describe('getTokenProbabilitiesFromLogits', () => {
291
- const tokenChoices = new Set(['supported', 'unsupported']);
292
-
293
- it('should extract probabilities for matching tokens', () => {
294
- const logits = [
295
- {
296
- token: 'supported',
297
- logprob: -0.1,
298
- top_logprobs: [
299
- { token: 'supported', logprob: -0.1 },
300
- { token: 'unsupported', logprob: -2.5 },
301
- ],
302
- },
303
- ];
304
-
305
- const probabilities = getTokenProbabilitiesFromLogits(logits, tokenChoices);
306
-
307
- expect(probabilities).toHaveLength(1);
308
- expect(probabilities[0].get('supported')).toBeGreaterThan(0.5);
309
- expect(probabilities[0].get('unsupported')).toBeLessThan(0.5);
310
- });
311
-
312
- it('should skip non-matching tokens', () => {
313
- const logits = [
314
- {
315
- token: '<',
316
- logprob: -0.001,
317
- top_logprobs: [{ token: '<', logprob: -0.001 }],
318
- },
319
- {
320
- token: '|',
321
- logprob: -0.001,
322
- top_logprobs: [{ token: '|', logprob: -0.001 }],
323
- },
324
- {
325
- token: 'supported',
326
- logprob: -0.1,
327
- top_logprobs: [
328
- { token: 'supported', logprob: -0.1 },
329
- { token: 'unsupported', logprob: -2.5 },
330
- ],
331
- },
332
- {
333
- token: '|',
334
- logprob: -0.001,
335
- top_logprobs: [{ token: '|', logprob: -0.001 }],
336
- },
337
- ];
338
-
339
- const probabilities = getTokenProbabilitiesFromLogits(logits, tokenChoices);
340
-
341
- expect(probabilities).toHaveLength(1);
342
- });
343
-
344
- it('should handle multiple supported/unsupported tokens', () => {
345
- const logits = [
346
- {
347
- token: 'supported',
348
- logprob: -0.1,
349
- top_logprobs: [
350
- { token: 'supported', logprob: -0.1 },
351
- { token: 'unsupported', logprob: -3.0 },
352
- ],
353
- },
354
- {
355
- token: 'unsupported',
356
- logprob: -0.2,
357
- top_logprobs: [
358
- { token: 'unsupported', logprob: -0.2 },
359
- { token: 'supported', logprob: -2.8 },
360
- ],
361
- },
362
- {
363
- token: 'supported',
364
- logprob: -0.5,
365
- top_logprobs: [
366
- { token: 'supported', logprob: -0.5 },
367
- { token: 'unsupported', logprob: -1.5 },
368
- ],
369
- },
370
- ];
371
-
372
- const probabilities = getTokenProbabilitiesFromLogits(logits, tokenChoices);
373
-
374
- expect(probabilities).toHaveLength(3);
375
- // First token: strongly supported
376
- expect(probabilities[0].get('supported')).toBeGreaterThan(0.9);
377
- // Second token: strongly unsupported
378
- expect(probabilities[1].get('unsupported')).toBeGreaterThan(0.9);
379
- // Third token: moderately supported
380
- expect(probabilities[2].get('supported')).toBeGreaterThan(0.5);
381
- });
382
-
383
- it('should return empty array for empty logits', () => {
384
- const probabilities = getTokenProbabilitiesFromLogits([], tokenChoices);
385
- expect(probabilities).toEqual([]);
386
- });
387
-
388
- it('should return empty array when no matching tokens', () => {
389
- const logits = [
390
- {
391
- token: 'other',
392
- logprob: -0.1,
393
- top_logprobs: [{ token: 'other', logprob: -0.1 }],
394
- },
395
- ];
396
-
397
- const probabilities = getTokenProbabilitiesFromLogits(logits, tokenChoices);
398
- expect(probabilities).toEqual([]);
399
- });
400
-
401
- it('should handle missing token in top_logprobs', () => {
402
- const logits = [
403
- {
404
- token: 'supported',
405
- logprob: -0.1,
406
- top_logprobs: [
407
- { token: 'supported', logprob: -0.1 },
408
- // 'unsupported' not in top_logprobs
409
- ],
410
- },
411
- ];
412
-
413
- const probabilities = getTokenProbabilitiesFromLogits(logits, tokenChoices);
414
-
415
- expect(probabilities).toHaveLength(1);
416
- // Should still have both keys
417
- expect(probabilities[0].has('supported')).toBe(true);
418
- expect(probabilities[0].has('unsupported')).toBe(true);
419
- // Supported should have high probability since unsupported is estimated lower
420
- expect(probabilities[0].get('supported')).toBeGreaterThan(0.5);
421
- });
422
-
423
- it('should apply softmax correctly', () => {
424
- const logits = [
425
- {
426
- token: 'supported',
427
- logprob: 0, // e^0 = 1
428
- top_logprobs: [
429
- { token: 'supported', logprob: 0 },
430
- { token: 'unsupported', logprob: 0 }, // e^0 = 1
431
- ],
432
- },
433
- ];
434
-
435
- const probabilities = getTokenProbabilitiesFromLogits(logits, tokenChoices);
436
-
437
- // With equal logprobs, softmax should give 0.5 each
438
- expect(probabilities[0].get('supported')).toBeCloseTo(0.5, 5);
439
- expect(probabilities[0].get('unsupported')).toBeCloseTo(0.5, 5);
440
- });
441
- });