@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.
- package/.github/workflows/betterleaks.yml +133 -0
- package/.gitleaks.toml +89 -0
- package/CHANGELOG.md +32 -1
- package/README.md +102 -12
- package/RELEASE.md +1 -1
- package/package.json +1 -1
- package/src/ChatBlock/chat/AIMessage.tsx +60 -41
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
- package/src/ChatBlock/components/markdown/index.jsx +1 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +11 -2
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +20 -11
- package/src/ChatBlock/services/messageProcessor.ts +36 -14
- package/src/ChatBlock/style.less +41 -51
- package/src/ChatBlock/utils/index.tsx +10 -0
- package/src/halloumi/README.md +27 -1
- package/src/halloumi/middleware.js +40 -50
- package/src/halloumi/middleware.test.js +102 -103
- package/src/middleware.js +2 -2
- package/src/middleware.test.js +11 -11
- package/src/halloumi/filtering.js +0 -149
- package/src/halloumi/filtering.test.js +0 -242
- package/src/halloumi/generative.js +0 -324
- package/src/halloumi/generative.test.js +0 -386
- package/src/halloumi/markdown-splitter.js +0 -172
- package/src/halloumi/markdown-splitter.test.js +0 -133
- package/src/halloumi/postprocessing.js +0 -247
- package/src/halloumi/postprocessing.test.js +0 -441
- package/src/halloumi/preprocessing.js +0 -117
- package/src/halloumi/preprocessing.test.js +0 -184
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
parseExcludeIndices,
|
|
5
|
-
callLLM,
|
|
6
|
-
excludeClaimSentences,
|
|
7
|
-
excludeContextSentences,
|
|
8
|
-
} from './filtering';
|
|
9
|
-
jest.mock('node-fetch');
|
|
10
|
-
|
|
11
|
-
describe('parseExcludeIndices', () => {
|
|
12
|
-
it('parses single indices', () => {
|
|
13
|
-
const result = parseExcludeIndices('1,3,5', 6);
|
|
14
|
-
expect(result).toEqual(new Set([1, 3, 5]));
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns empty set for NONE', () => {
|
|
18
|
-
const result = parseExcludeIndices('NONE', 10);
|
|
19
|
-
expect(result).toEqual(new Set());
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('returns empty set for none (lowercase)', () => {
|
|
23
|
-
const result = parseExcludeIndices(' none ', 10);
|
|
24
|
-
expect(result).toEqual(new Set());
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('ignores indices below 1', () => {
|
|
28
|
-
const result = parseExcludeIndices('0, 1, 3', 5);
|
|
29
|
-
expect(result).toEqual(new Set([1, 3]));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('ignores indices above maxIndex', () => {
|
|
33
|
-
const result = parseExcludeIndices('1, 3, 99', 5);
|
|
34
|
-
expect(result).toEqual(new Set([1, 3]));
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('handles whitespace variations', () => {
|
|
38
|
-
const result = parseExcludeIndices(' 1 , 5 , 7 ', 10);
|
|
39
|
-
expect(result).toEqual(new Set([1, 5, 7]));
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('extracts numbers even from unexpected formats', () => {
|
|
43
|
-
// Parser uses match(/\d+/g) so it extracts all numbers
|
|
44
|
-
const result = parseExcludeIndices('1-3, 5', 10);
|
|
45
|
-
expect(result).toEqual(new Set([1, 3, 5]));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('returns empty set for empty string with no numbers', () => {
|
|
49
|
-
const result = parseExcludeIndices('no numbers here', 10);
|
|
50
|
-
expect(result).toEqual(new Set());
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('callLLM', () => {
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
fetch.mockReset();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterEach(() => {
|
|
60
|
-
fetch.mockReset();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('makes POST request with correct headers', async () => {
|
|
64
|
-
const mockResponse = { choices: [{ message: { content: 'result' } }] };
|
|
65
|
-
fetch.mockResolvedValue({
|
|
66
|
-
json: () => Promise.resolve(mockResponse),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await callLLM('https://api.example.com', 'my-api-key', { model: 'gpt-4' });
|
|
70
|
-
|
|
71
|
-
expect(fetch).toHaveBeenCalledWith(
|
|
72
|
-
'https://api.example.com',
|
|
73
|
-
expect.objectContaining({
|
|
74
|
-
method: 'POST',
|
|
75
|
-
headers: expect.objectContaining({
|
|
76
|
-
'Content-Type': 'application/json',
|
|
77
|
-
Authorization: 'Bearer my-api-key',
|
|
78
|
-
}),
|
|
79
|
-
}),
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('omits Authorization header when no apiKey', async () => {
|
|
84
|
-
fetch.mockResolvedValue({
|
|
85
|
-
json: () => Promise.resolve({}),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
await callLLM('https://api.example.com', null, { model: 'gpt-4' });
|
|
89
|
-
|
|
90
|
-
const headers = fetch.mock.calls[0][1].headers;
|
|
91
|
-
expect(headers['Authorization']).toBeUndefined();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('adds X-Forwarded-For header when ip is provided', async () => {
|
|
95
|
-
fetch.mockResolvedValue({
|
|
96
|
-
json: () => Promise.resolve({}),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
await callLLM('https://api.example.com', null, {}, { ip: '1.2.3.4' });
|
|
100
|
-
|
|
101
|
-
const headers = fetch.mock.calls[0][1].headers;
|
|
102
|
-
expect(headers['X-Forwarded-For']).toBe('1.2.3.4');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('returns parsed JSON response', async () => {
|
|
106
|
-
const mockResponse = { choices: [{ message: { content: 'hello' } }] };
|
|
107
|
-
fetch.mockResolvedValue({
|
|
108
|
-
json: () => Promise.resolve(mockResponse),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const result = await callLLM('https://api.example.com', null, {});
|
|
112
|
-
expect(result).toEqual(mockResponse);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('excludeClaimSentences', () => {
|
|
117
|
-
beforeEach(() => {
|
|
118
|
-
fetch.mockReset();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
afterEach(() => {
|
|
122
|
-
fetch.mockReset();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('returns empty set for empty sentences array', async () => {
|
|
126
|
-
const result = await excludeClaimSentences([]);
|
|
127
|
-
expect(result).toEqual(new Set());
|
|
128
|
-
expect(fetch).not.toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('calls LLM and returns excluded sentence indices', async () => {
|
|
132
|
-
fetch.mockResolvedValue({
|
|
133
|
-
json: () =>
|
|
134
|
-
Promise.resolve({
|
|
135
|
-
choices: [{ message: { content: '1,3' } }],
|
|
136
|
-
}),
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const sentences = ['Sentence one.', 'Sentence two.', 'Sentence three.'];
|
|
140
|
-
const result = await excludeClaimSentences(sentences);
|
|
141
|
-
|
|
142
|
-
expect(result).toEqual(new Set([1, 3]));
|
|
143
|
-
expect(fetch).toHaveBeenCalledTimes(1);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('returns empty set when LLM returns NONE', async () => {
|
|
147
|
-
fetch.mockResolvedValue({
|
|
148
|
-
json: () =>
|
|
149
|
-
Promise.resolve({
|
|
150
|
-
choices: [{ message: { content: 'NONE' } }],
|
|
151
|
-
}),
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const sentences = ['Some factual sentence.', 'Another factual sentence.'];
|
|
155
|
-
const result = await excludeClaimSentences(sentences);
|
|
156
|
-
|
|
157
|
-
expect(result).toEqual(new Set());
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('returns empty set on LLM error', async () => {
|
|
161
|
-
fetch.mockRejectedValue(new Error('Network error'));
|
|
162
|
-
|
|
163
|
-
const sentences = ['Sentence one.'];
|
|
164
|
-
const result = await excludeClaimSentences(sentences);
|
|
165
|
-
|
|
166
|
-
expect(result).toEqual(new Set());
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('passes ip option to LLM call', async () => {
|
|
170
|
-
fetch.mockResolvedValue({
|
|
171
|
-
json: () =>
|
|
172
|
-
Promise.resolve({ choices: [{ message: { content: 'NONE' } }] }),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
await excludeClaimSentences(['test sentence'], { ip: '10.0.0.1' });
|
|
176
|
-
|
|
177
|
-
const headers = fetch.mock.calls[0][1].headers;
|
|
178
|
-
expect(headers['X-Forwarded-For']).toBe('10.0.0.1');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('excludeContextSentences', () => {
|
|
183
|
-
const MIN_SENTENCES = 75;
|
|
184
|
-
|
|
185
|
-
beforeEach(() => {
|
|
186
|
-
fetch.mockReset();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
afterEach(() => {
|
|
190
|
-
fetch.mockReset();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('returns empty set when context sentences <= minimum threshold', async () => {
|
|
194
|
-
const shortContext = Array(10).fill('Short sentence.');
|
|
195
|
-
const claims = ['A claim.'];
|
|
196
|
-
const result = await excludeContextSentences(shortContext, claims);
|
|
197
|
-
|
|
198
|
-
expect(result).toEqual(new Set());
|
|
199
|
-
expect(fetch).not.toHaveBeenCalled();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('calls LLM when context exceeds minimum threshold', async () => {
|
|
203
|
-
fetch.mockResolvedValue({
|
|
204
|
-
json: () =>
|
|
205
|
-
Promise.resolve({
|
|
206
|
-
choices: [{ message: { content: '1,2' } }],
|
|
207
|
-
}),
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
|
|
211
|
-
const claims = ['A claim.'];
|
|
212
|
-
const result = await excludeContextSentences(longContext, claims);
|
|
213
|
-
|
|
214
|
-
expect(result).toEqual(new Set([1, 2]));
|
|
215
|
-
expect(fetch).toHaveBeenCalledTimes(1);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('returns empty set on LLM error', async () => {
|
|
219
|
-
fetch.mockRejectedValue(new Error('timeout'));
|
|
220
|
-
|
|
221
|
-
const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
|
|
222
|
-
const claims = ['A claim.'];
|
|
223
|
-
const result = await excludeContextSentences(longContext, claims);
|
|
224
|
-
|
|
225
|
-
expect(result).toEqual(new Set());
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('returns empty set when LLM returns NONE for context', async () => {
|
|
229
|
-
fetch.mockResolvedValue({
|
|
230
|
-
json: () =>
|
|
231
|
-
Promise.resolve({
|
|
232
|
-
choices: [{ message: { content: 'NONE' } }],
|
|
233
|
-
}),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const longContext = Array(MIN_SENTENCES + 1).fill('Context sentence.');
|
|
237
|
-
const claims = ['A claim.'];
|
|
238
|
-
const result = await excludeContextSentences(longContext, claims);
|
|
239
|
-
|
|
240
|
-
expect(result).toEqual(new Set());
|
|
241
|
-
});
|
|
242
|
-
});
|
|
@@ -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
|
-
}
|