@eeacms/volto-eea-chatbot 3.0.1 → 4.1.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/README.md +115 -12
  3. package/package.json +1 -1
  4. package/src/ChatBlock/ChatBlockView.jsx +7 -1
  5. package/src/ChatBlock/chat/AIMessage.tsx +79 -45
  6. package/src/ChatBlock/chat/ChatMessage.tsx +17 -10
  7. package/src/ChatBlock/chat/ChatWindow.tsx +11 -0
  8. package/src/ChatBlock/chat/MessageContext.ts +12 -0
  9. package/src/ChatBlock/chat/index.ts +1 -0
  10. package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
  11. package/src/ChatBlock/components/markdown/index.jsx +1 -0
  12. package/src/ChatBlock/hooks/useQualityMarkers.js +21 -4
  13. package/src/ChatBlock/index.js +9 -1
  14. package/src/ChatBlock/packets/RendererComponent.tsx +9 -0
  15. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +15 -3
  16. package/src/ChatBlock/schema.jsx +13 -0
  17. package/src/ChatBlock/services/messageProcessor.ts +9 -12
  18. package/src/ChatBlock/types/interfaces.ts +25 -0
  19. package/src/ChatBlock/utils/index.tsx +10 -0
  20. package/src/ChatBlock/variations.ts +44 -0
  21. package/src/halloumi/README.md +27 -1
  22. package/src/halloumi/middleware.js +41 -50
  23. package/src/halloumi/middleware.test.js +102 -103
  24. package/src/halloumi/filtering.js +0 -149
  25. package/src/halloumi/filtering.test.js +0 -242
  26. package/src/halloumi/generative.js +0 -324
  27. package/src/halloumi/generative.test.js +0 -386
  28. package/src/halloumi/markdown-splitter.js +0 -172
  29. package/src/halloumi/markdown-splitter.test.js +0 -133
  30. package/src/halloumi/postprocessing.js +0 -247
  31. package/src/halloumi/postprocessing.test.js +0 -441
  32. package/src/halloumi/preprocessing.js +0 -117
  33. package/src/halloumi/preprocessing.test.js +0 -184
@@ -1,324 +0,0 @@
1
- import debug from 'debug';
2
- import fs from 'fs';
3
- import {
4
- getClaimsFromResponse,
5
- getTokenProbabilitiesFromLogits,
6
- } from './postprocessing';
7
- import { createChunkedHalloumiPrompts, getOffsets } from './preprocessing';
8
- import { splitMarkdown, splitProse } from './markdown-splitter';
9
- import { callLLM, excludeClaimSentences } from './filtering';
10
-
11
- const log = debug('halloumi');
12
-
13
- const tokenChoices = new Set(['supported', 'unsupported']);
14
-
15
- function sigmoid(x) {
16
- return 1 / (1 + Math.exp(-x));
17
- }
18
-
19
- export function applyPlattScaling(platt, probability) {
20
- probability = Math.min(Math.max(probability, 1e-6), 1 - 1e-6);
21
- const log_prob = Math.log(probability / (1 - probability));
22
- return sigmoid(-1 * (platt.a * log_prob + platt.b));
23
- }
24
-
25
- /**
26
- * Merges claims from multiple chunked HallOumi responses.
27
- * For each response sentence (claimId), combines segment citations
28
- * and takes the max supported score.
29
- */
30
- function mergeChunkClaims(chunkResults) {
31
- const claimMap = new Map();
32
-
33
- for (const claims of chunkResults) {
34
- for (const claim of claims) {
35
- if (!claimMap.has(claim.claimId)) {
36
- claimMap.set(claim.claimId, { ...claim });
37
- } else {
38
- const existing = claimMap.get(claim.claimId);
39
- existing.segments.push(...claim.segments);
40
- // Keep the result with the higher supported score
41
- const existingScore = existing.probabilities.get('supported') || 0;
42
- const newScore = claim.probabilities.get('supported') || 0;
43
- if (newScore > existingScore) {
44
- existing.probabilities = claim.probabilities;
45
- existing.explanation = claim.explanation;
46
- existing.supported = claim.supported;
47
- }
48
- }
49
- }
50
- }
51
-
52
- return Array.from(claimMap.values());
53
- }
54
-
55
- export async function getVerifyClaimResponse(
56
- model,
57
- sources,
58
- answer,
59
- { ip } = {},
60
- ) {
61
- const emptyResponse = {
62
- claims: [],
63
- segments: {},
64
- };
65
- if (!sources?.length || !answer) {
66
- return { ...emptyResponse, reason: 'Context is empty' };
67
- }
68
-
69
- // Split sentences
70
- const responseSentences = splitMarkdown(answer);
71
- const responseOffsets = getOffsets(answer, responseSentences);
72
-
73
- // Filter claims and context in parallel
74
- const [excludeResponseIndices] = await Promise.all([
75
- excludeClaimSentences(responseSentences, { ip }),
76
- ]);
77
-
78
- const contextSentences = [];
79
- const indexedContextSentences = sources.reduce((acc, text, sourceIdx) => {
80
- const sentences = splitProse(text, 50).map((sentence, sentenceIdx) => {
81
- const globalId = acc.length + sentenceIdx + 1;
82
- contextSentences.push(sentence);
83
- return {
84
- sentence,
85
- sourceId: sourceIdx + 1,
86
- globalId,
87
- };
88
- });
89
- acc.push(...sentences);
90
- return acc;
91
- }, []);
92
- const joinedContext = sources.join('');
93
- const contextOffsets = getOffsets(joinedContext, contextSentences);
94
-
95
- if (excludeResponseIndices.size === responseSentences.length) {
96
- log('All response sentences excluded');
97
- return {
98
- ...emptyResponse,
99
- empty: 'Claims in the document could not be verified',
100
- };
101
- }
102
- log('Excluded response indices', excludeResponseIndices);
103
- const { prompts } = createChunkedHalloumiPrompts({
104
- indexedContextSentences,
105
- responseSentences,
106
- responseOffsets,
107
- request: null,
108
- excludeResponseIndices,
109
- });
110
-
111
- log(`Split into ${prompts.length} chunk(s)`);
112
-
113
- // Run all chunks in parallel
114
- const chunkResults = await Promise.all(
115
- prompts.map((chunkPrompt, i) => {
116
- log(`Chunk ${i + 1} request`);
117
- return halloumiGenerativeAPI(model, chunkPrompt, { ip });
118
- }),
119
- );
120
-
121
- // Merge raw claims across chunks
122
- const rawClaims = mergeChunkClaims(chunkResults);
123
-
124
- const mergedPrompt = {
125
- contextOffsets,
126
- responseOffsets,
127
- joinedContext,
128
- };
129
- const converted = convertGenerativesClaimToVerifyClaimResponse(
130
- rawClaims,
131
- mergedPrompt,
132
- );
133
-
134
- if (excludeResponseIndices.size > 0) {
135
- for (const idx of excludeResponseIndices) {
136
- if (responseOffsets.has(idx)) {
137
- const offsets = responseOffsets.get(idx);
138
- converted.claims.push({
139
- claimId: idx,
140
- claimString: responseSentences[idx - 1],
141
- startOffset: offsets.startOffset,
142
- endOffset: offsets.endOffset,
143
- skipped: true,
144
- score: null,
145
- });
146
- }
147
- }
148
- converted.claims.sort((a, b) => a.startOffset - b.startOffset);
149
- }
150
-
151
- const result = {
152
- ...converted,
153
- rawClaims,
154
- ...(prompts.length === 1
155
- ? { halloumiPrompt: prompts[0] }
156
- : { halloumiPrompts: prompts }),
157
- };
158
-
159
- return result;
160
- }
161
-
162
- /**
163
- * Fetches a response from the LLM.
164
- *
165
- * @param {object} model The model configuration.
166
- * @param {object} prompt The prompt to send to the LLM.
167
- * @returns {Promise<object>} The JSON response from the LLM.
168
- *
169
- * Environment Variables:
170
- * - `MOCK_HALLOUMI_FILE_PATH`: If set, the function reads the LLM response from the specified file path instead of making an API call.
171
- * - `DUMP_HALLOUMI_REQ_FILE_PATH`: If set, the LLM request (URL and parameters) is dumped to the specified file path.
172
- * - `DUMP_HALLOUMI_FILE_PATH`: If set, the LLM response is dumped to the specified file path.
173
- */
174
- async function getLLMResponse(model, prompt, { ip } = {}) {
175
- let jsonData;
176
-
177
- if (process.env.MOCK_HALLOUMI_FILE_PATH) {
178
- const filePath = process.env.MOCK_HALLOUMI_FILE_PATH;
179
- const fileContent = fs.readFileSync(filePath, 'utf-8');
180
- jsonData = JSON.parse(fileContent);
181
- await new Promise((resolve) => setTimeout(resolve, 2000));
182
- return jsonData;
183
- }
184
-
185
- const data = {
186
- messages: [{ role: 'user', content: prompt.prompt }],
187
- temperature: 0.0,
188
- model: model.name,
189
- logprobs: true,
190
- top_logprobs: 3,
191
- };
192
-
193
- if (process.env.DUMP_HALLOUMI_REQ_FILE_PATH) {
194
- const filePath = process.env.DUMP_HALLOUMI_REQ_FILE_PATH;
195
- fs.writeFileSync(
196
- filePath,
197
- JSON.stringify({ url: model.apiUrl, body: data }, null, 2),
198
- );
199
- log(`Dumped halloumi request: ${filePath}`);
200
- }
201
-
202
- jsonData = await callLLM(model.apiUrl, model.apiKey, data, { ip });
203
-
204
- if (process.env.DUMP_HALLOUMI_FILE_PATH) {
205
- const filePath = process.env.DUMP_HALLOUMI_FILE_PATH;
206
- fs.writeFileSync(filePath, JSON.stringify(jsonData, null, 2));
207
- log(`Dumped halloumi response: ${filePath}`);
208
- }
209
-
210
- return jsonData;
211
- }
212
-
213
- /**
214
- * Gets all claims from a response.
215
- * @param response A string containing all claims and their information.
216
- * @returns A list of claim objects.
217
- */
218
- export async function halloumiGenerativeAPI(model, prompt, { ip } = {}) {
219
- const jsonData = await getLLMResponse(model, prompt, { ip });
220
-
221
- // Todo: restore log
222
- // log('Generative response', jsonData);
223
-
224
- const finishReason = jsonData.choices?.[0]?.finish_reason;
225
- if (finishReason === 'length') {
226
- throw new Error('HallOumi response truncated (finish_reason: length)');
227
- }
228
-
229
- // Todo: restore log
230
- // log('Logprobs', jsonData.choices[0].logprobs.content);
231
-
232
- const logits = jsonData.choices[0].logprobs.content;
233
- const tokenProbabilities = getTokenProbabilitiesFromLogits(
234
- logits,
235
- tokenChoices,
236
- );
237
- const parsedResponse = getClaimsFromResponse(
238
- jsonData.choices[0].message.content,
239
- );
240
-
241
- if (parsedResponse.length !== tokenProbabilities.length) {
242
- log(
243
- 'Warning: token probabilities (%d) and claims (%d) do not match — using available probabilities, defaulting remainder to 0.5',
244
- tokenProbabilities.length,
245
- parsedResponse.length,
246
- );
247
- }
248
-
249
- const defaultScoreMap = new Map([
250
- ['supported', 0.5],
251
- ['unsupported', 0.5],
252
- ]);
253
-
254
- for (let i = 0; i < parsedResponse.length; i++) {
255
- const scoreMap = tokenProbabilities[i] ?? new Map(defaultScoreMap);
256
- if (model.plattScaling) {
257
- const platt = model.plattScaling;
258
- const unsupportedScore = applyPlattScaling(
259
- platt,
260
- scoreMap.get('unsupported'),
261
- );
262
- const supportedScore = 1 - unsupportedScore;
263
- scoreMap.set('supported', supportedScore);
264
- scoreMap.set('unsupported', unsupportedScore);
265
- }
266
- parsedResponse[i].probabilities = scoreMap;
267
- }
268
-
269
- return parsedResponse;
270
- }
271
-
272
- export function convertGenerativesClaimToVerifyClaimResponse(
273
- generativeClaims,
274
- prompt,
275
- ) {
276
- const segments = {};
277
- const claims = [];
278
-
279
- for (const offset of prompt.contextOffsets) {
280
- const id = offset[0].toString();
281
- segments[id] = {
282
- id,
283
- startOffset: offset[1].startOffset,
284
- endOffset: offset[1].endOffset,
285
- text: prompt.joinedContext.slice(
286
- offset[1].startOffset,
287
- offset[1].endOffset,
288
- ),
289
- };
290
- }
291
-
292
- for (const generativeClaim of generativeClaims) {
293
- const segmentIds = [];
294
- for (const seg of generativeClaim.segments) {
295
- if (!seg) continue;
296
- segmentIds.push(seg.toString());
297
- }
298
-
299
- const claimId = generativeClaim.claimId;
300
- if (!prompt.responseOffsets.has(claimId)) {
301
- throw new Error(`Claim ${claimId} not found in response offsets.`);
302
- }
303
-
304
- const claimResponseWindow = prompt.responseOffsets.get(claimId);
305
- const score = generativeClaim.probabilities.get('supported');
306
- const claim = {
307
- claimId,
308
- claimString: generativeClaim.claimString,
309
- startOffset: claimResponseWindow.startOffset,
310
- endOffset: claimResponseWindow.endOffset,
311
- rationale: generativeClaim.explanation,
312
- segmentIds,
313
- score,
314
- };
315
- claims.push(claim);
316
- }
317
-
318
- const response = {
319
- claims,
320
- segments,
321
- };
322
-
323
- return response;
324
- }
@@ -1,386 +0,0 @@
1
- import {
2
- halloumiGenerativeAPI,
3
- convertGenerativesClaimToVerifyClaimResponse,
4
- applyPlattScaling,
5
- getVerifyClaimResponse,
6
- } from './generative';
7
- import path from 'path';
8
-
9
- jest.mock('node-fetch');
10
-
11
- describe('halloumiGenerativeAPI reads from mock file', () => {
12
- const originalEnv = process.env;
13
-
14
- beforeEach(() => {
15
- jest.resetModules(); // Most important - reset modules between test runs
16
- process.env = {
17
- ...originalEnv,
18
- MOCK_HALLOUMI_FILE_PATH: path.join(__dirname, '../dummy/qa-raw-3.json'),
19
- };
20
- });
21
-
22
- afterEach(() => {
23
- process.env = originalEnv; // Restore original env
24
- jest.restoreAllMocks(); // Restore all mocks
25
- });
26
-
27
- it('should read from the mock file when MOCK_LLM_CALL is true', async () => {
28
- const model = { name: 'test-model', apiUrl: 'http://test.com' };
29
- const prompt = {
30
- prompt: 'test-prompt',
31
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
32
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
33
- };
34
-
35
- // We are testing halloumiGenerativeAPI which internally calls getLLMResponse
36
- // and getLLMResponse uses the MOCK_LLM_CALL env variable.
37
- const response = await halloumiGenerativeAPI(model, prompt);
38
-
39
- expect(response[0].claimString).toEqual(
40
- '**France – total waste generation (latest available data)** \n',
41
- );
42
- expect(response[0].segments).toEqual([
43
- 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
44
- ]);
45
- });
46
- });
47
-
48
- describe('applyPlattScaling', () => {
49
- it('returns calibrated probability', () => {
50
- const platt = { a: -0.5764, b: 0.1665 };
51
- const result = applyPlattScaling(platt, 0.5);
52
- expect(result).toBeGreaterThan(0);
53
- expect(result).toBeLessThan(1);
54
- });
55
-
56
- it('clamps very low probability', () => {
57
- const platt = { a: -1, b: 0 };
58
- const result = applyPlattScaling(platt, 0);
59
- expect(result).toBeGreaterThan(0);
60
- expect(result).toBeLessThan(1);
61
- });
62
-
63
- it('clamps very high probability', () => {
64
- const platt = { a: -1, b: 0 };
65
- const result = applyPlattScaling(platt, 1);
66
- expect(result).toBeGreaterThan(0);
67
- expect(result).toBeLessThan(1);
68
- });
69
-
70
- it('returns 0.5 when platt params are zero', () => {
71
- const platt = { a: 0, b: 0 };
72
- const result = applyPlattScaling(platt, 0.5);
73
- expect(result).toBeCloseTo(0.5, 1);
74
- });
75
- });
76
-
77
- describe('getVerifyClaimResponse', () => {
78
- it('returns empty response when sources is empty', async () => {
79
- const result = await getVerifyClaimResponse({}, [], 'claims');
80
- expect(result).toEqual({
81
- claims: [],
82
- segments: {},
83
- reason: 'Context is empty',
84
- });
85
- });
86
-
87
- it('returns empty response when sources is null', async () => {
88
- const result = await getVerifyClaimResponse({}, null, 'claims');
89
- expect(result).toEqual({
90
- claims: [],
91
- segments: {},
92
- reason: 'Context is empty',
93
- });
94
- });
95
-
96
- it('returns empty response when claims is falsy', async () => {
97
- const result = await getVerifyClaimResponse({}, ['source'], null);
98
- expect(result).toEqual({
99
- claims: [],
100
- segments: {},
101
- reason: 'Context is empty',
102
- });
103
- });
104
- });
105
-
106
- describe('halloumiGenerativeAPI with plattScaling', () => {
107
- const originalEnv = process.env;
108
-
109
- beforeEach(() => {
110
- jest.resetModules();
111
- process.env = {
112
- ...originalEnv,
113
- MOCK_HALLOUMI_FILE_PATH: path.join(__dirname, '../dummy/qa-raw-3.json'),
114
- };
115
- });
116
-
117
- afterEach(() => {
118
- process.env = originalEnv;
119
- jest.restoreAllMocks();
120
- });
121
-
122
- it('applies platt scaling when model has plattScaling config', async () => {
123
- const model = {
124
- name: 'test-model',
125
- apiUrl: 'http://test.com',
126
- plattScaling: { a: -0.5764, b: 0.1665 },
127
- };
128
- const prompt = {
129
- prompt: 'test-prompt',
130
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
131
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
132
- };
133
-
134
- const response = await halloumiGenerativeAPI(model, prompt);
135
-
136
- // With platt scaling, probabilities should be calibrated
137
- expect(response[0].probabilities).toBeDefined();
138
- const supported = response[0].probabilities.get('supported');
139
- const unsupported = response[0].probabilities.get('unsupported');
140
- expect(supported + unsupported).toBeCloseTo(1, 5);
141
- });
142
- });
143
-
144
- describe('halloumiGenerativeAPI via real fetch', () => {
145
- const originalEnv = process.env;
146
-
147
- beforeEach(() => {
148
- jest.resetModules();
149
- // Remove mock file path to exercise the fetch path
150
- process.env = { ...originalEnv };
151
- delete process.env.MOCK_HALLOUMI_FILE_PATH;
152
- });
153
-
154
- afterEach(() => {
155
- process.env = originalEnv;
156
- jest.restoreAllMocks();
157
- });
158
-
159
- it('calls fetch with correct parameters and auth header', async () => {
160
- // Mock postprocessing to control output and avoid format issues
161
- jest.doMock('./postprocessing', () => ({
162
- getClaimsFromResponse: jest.fn(() => [
163
- {
164
- claimId: 1,
165
- claimString: 'Test claim',
166
- subclaims: [],
167
- segments: [1],
168
- explanation: 'ok',
169
- supported: true,
170
- },
171
- ]),
172
- getTokenProbabilitiesFromLogits: jest.fn(() => [
173
- new Map([
174
- ['supported', 0.9],
175
- ['unsupported', 0.1],
176
- ]),
177
- ]),
178
- }));
179
-
180
- const { halloumiGenerativeAPI } = require('./generative');
181
- const nodeFetch = require('node-fetch');
182
-
183
- const mockResponse = {
184
- choices: [
185
- {
186
- message: { content: 'mock content' },
187
- logprobs: { content: [] },
188
- },
189
- ],
190
- };
191
-
192
- nodeFetch.mockResolvedValueOnce({
193
- json: () => Promise.resolve(mockResponse),
194
- });
195
-
196
- const model = {
197
- name: 'test-model',
198
- apiUrl: 'http://test.com/api',
199
- apiKey: 'test-key', //betterleaks:allow
200
- };
201
- const prompt = {
202
- prompt: 'test prompt',
203
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
204
- responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
205
- };
206
-
207
- const response = await halloumiGenerativeAPI(model, prompt);
208
- expect(response).toBeDefined();
209
- expect(Array.isArray(response)).toBe(true);
210
- expect(response[0].probabilities.get('supported')).toBe(0.9);
211
-
212
- // Verify fetch was called with auth header
213
- expect(nodeFetch).toHaveBeenCalledWith(
214
- 'http://test.com/api',
215
- expect.objectContaining({
216
- method: 'POST',
217
- headers: expect.objectContaining({
218
- Authorization: 'Bearer test-key',
219
- }),
220
- }),
221
- );
222
- });
223
-
224
- it('calls fetch without auth header when no apiKey', async () => {
225
- jest.doMock('./postprocessing', () => ({
226
- getClaimsFromResponse: jest.fn(() => [
227
- {
228
- claimId: 1,
229
- claimString: 'Test',
230
- subclaims: [],
231
- segments: [],
232
- explanation: 'ok',
233
- supported: true,
234
- },
235
- ]),
236
- getTokenProbabilitiesFromLogits: jest.fn(() => [
237
- new Map([
238
- ['supported', 0.8],
239
- ['unsupported', 0.2],
240
- ]),
241
- ]),
242
- }));
243
-
244
- const { halloumiGenerativeAPI } = require('./generative');
245
- const nodeFetch = require('node-fetch');
246
-
247
- nodeFetch.mockResolvedValueOnce({
248
- json: () =>
249
- Promise.resolve({
250
- choices: [
251
- {
252
- message: { content: 'mock' },
253
- logprobs: { content: [] },
254
- },
255
- ],
256
- }),
257
- });
258
-
259
- const model = { name: 'test-model', apiUrl: 'http://test.com/api' };
260
- const prompt = {
261
- prompt: 'test',
262
- contextOffsets: new Map(),
263
- responseOffsets: new Map(),
264
- };
265
-
266
- await halloumiGenerativeAPI(model, prompt);
267
-
268
- const callHeaders = nodeFetch.mock.calls[0][1].headers;
269
- expect(callHeaders.Authorization).toBeUndefined();
270
- });
271
-
272
- it('defaults to 0.5 when token probabilities and claims do not match', async () => {
273
- jest.doMock('./postprocessing', () => ({
274
- getClaimsFromResponse: jest.fn(() => [
275
- { claimId: 1, claimString: 'Claim 1' },
276
- { claimId: 2, claimString: 'Claim 2' },
277
- ]),
278
- getTokenProbabilitiesFromLogits: jest.fn(() => [
279
- new Map([['supported', 0.9]]),
280
- ]),
281
- }));
282
-
283
- const { halloumiGenerativeAPI } = require('./generative');
284
- const nodeFetch = require('node-fetch');
285
-
286
- nodeFetch.mockResolvedValueOnce({
287
- json: () =>
288
- Promise.resolve({
289
- choices: [
290
- {
291
- message: { content: 'mock' },
292
- logprobs: { content: [] },
293
- },
294
- ],
295
- }),
296
- });
297
-
298
- const model = { name: 'test-model', apiUrl: 'http://test.com' };
299
- const prompt = {
300
- prompt: 'test',
301
- contextOffsets: new Map(),
302
- responseOffsets: new Map(),
303
- };
304
-
305
- const result = await halloumiGenerativeAPI(model, prompt);
306
- // First claim gets the available probability
307
- expect(result[0].probabilities.get('supported')).toBe(0.9);
308
- // Second claim defaults to 0.5
309
- expect(result[1].probabilities.get('supported')).toBe(0.5);
310
- });
311
- });
312
-
313
- describe('convertGenerativesClaimToVerifyClaimResponse', () => {
314
- it('should correctly convert generative claims to verify claim response', () => {
315
- const generativeClaims = [
316
- {
317
- claimId: 1,
318
- claimString: 'Test claim string',
319
- subclaims: ['subclaim1', 'subclaim2'],
320
- segments: [1, 2, 3],
321
- explanation: 'Test explanation',
322
- supported: true,
323
- probabilities: new Map([
324
- ['supported', 0.9],
325
- ['unsupported', 0.1],
326
- ]),
327
- },
328
- ];
329
-
330
- const prompt = {
331
- contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
332
- responseOffsets: new Map([[1, { startOffset: 100, endOffset: 120 }]]),
333
- joinedContext: 'Test conte',
334
- };
335
-
336
- const result = convertGenerativesClaimToVerifyClaimResponse(
337
- generativeClaims,
338
- prompt,
339
- );
340
-
341
- expect(result).toEqual({
342
- claims: [
343
- {
344
- claimId: 1,
345
- claimString: 'Test claim string',
346
- startOffset: 100,
347
- endOffset: 120,
348
- rationale: 'Test explanation',
349
- segmentIds: ['1', '2', '3'],
350
- score: 0.9,
351
- },
352
- ],
353
- segments: {
354
- 1: {
355
- id: '1',
356
- startOffset: 0,
357
- endOffset: 10,
358
- text: 'Test conte',
359
- },
360
- },
361
- });
362
- });
363
-
364
- it('throws when claim ID is not found in response offsets', () => {
365
- const generativeClaims = [
366
- {
367
- claimId: 999,
368
- claimString: 'Unknown claim',
369
- subclaims: [],
370
- segments: [],
371
- explanation: 'Missing',
372
- supported: false,
373
- probabilities: new Map([['supported', 0.1]]),
374
- },
375
- ];
376
-
377
- const prompt = {
378
- contextOffsets: new Map(),
379
- responseOffsets: new Map(),
380
- };
381
-
382
- expect(() =>
383
- convertGenerativesClaimToVerifyClaimResponse(generativeClaims, prompt),
384
- ).toThrow('Claim 999 not found in response offsets');
385
- });
386
- });