@eeacms/volto-eea-chatbot 1.0.9
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/.coverage.babel.config.js +9 -0
- package/.eslintrc.js +68 -0
- package/.husky/pre-commit +2 -0
- package/.release-it.json +17 -0
- package/AGENTS.md +89 -0
- package/CHANGELOG.md +770 -0
- package/DEVELOP.md +124 -0
- package/LICENSE.md +9 -0
- package/README.md +170 -0
- package/RELEASE.md +74 -0
- package/TESTING.md +5 -0
- package/babel.config.js +17 -0
- package/bootstrap +41 -0
- package/cypress.config.js +27 -0
- package/docker-compose.yml +32 -0
- package/jest-addon.config.js +465 -0
- package/jest.setup.js +65 -0
- package/locales/de/LC_MESSAGES/volto.po +14 -0
- package/locales/en/LC_MESSAGES/volto.po +14 -0
- package/locales/it/LC_MESSAGES/volto.po +14 -0
- package/locales/ro/LC_MESSAGES/volto.po +14 -0
- package/locales/volto.pot +16 -0
- package/package.json +98 -0
- package/razzle.extend.js +40 -0
- package/src/ChatBlock/ChatBlockEdit.jsx +46 -0
- package/src/ChatBlock/ChatBlockView.jsx +21 -0
- package/src/ChatBlock/chat/AIMessage.tsx +566 -0
- package/src/ChatBlock/chat/ChatMessage.tsx +35 -0
- package/src/ChatBlock/chat/ChatWindow.tsx +288 -0
- package/src/ChatBlock/chat/UserMessage.tsx +27 -0
- package/src/ChatBlock/chat/index.ts +4 -0
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +67 -0
- package/src/ChatBlock/components/BlinkingDot.tsx +3 -0
- package/src/ChatBlock/components/ChatMessageFeedback.jsx +77 -0
- package/src/ChatBlock/components/EmptyState.jsx +70 -0
- package/src/ChatBlock/components/FeedbackModal.jsx +125 -0
- package/src/ChatBlock/components/HalloumiFeedback.jsx +126 -0
- package/src/ChatBlock/components/Icon.tsx +35 -0
- package/src/ChatBlock/components/QualityCheckToggle.jsx +26 -0
- package/src/ChatBlock/components/RelatedQuestions.jsx +59 -0
- package/src/ChatBlock/components/Source.jsx +93 -0
- package/src/ChatBlock/components/SourceChip.tsx +55 -0
- package/src/ChatBlock/components/Spinner.jsx +3 -0
- package/src/ChatBlock/components/UserActionsToolbar.jsx +44 -0
- package/src/ChatBlock/components/WebResultIcon.tsx +42 -0
- package/src/ChatBlock/components/markdown/Citation.jsx +70 -0
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +98 -0
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +172 -0
- package/src/ChatBlock/components/markdown/RenderClaimView.jsx +96 -0
- package/src/ChatBlock/components/markdown/colors.js +29 -0
- package/src/ChatBlock/components/markdown/colors.less +52 -0
- package/src/ChatBlock/components/markdown/colors.test.js +69 -0
- package/src/ChatBlock/components/markdown/index.js +115 -0
- package/src/ChatBlock/fonts/DejaVuSans.ttf +0 -0
- package/src/ChatBlock/hocs/withOnyxData.jsx +46 -0
- package/src/ChatBlock/hooks/index.ts +7 -0
- package/src/ChatBlock/hooks/useChatController.ts +333 -0
- package/src/ChatBlock/hooks/useChatStreaming.ts +82 -0
- package/src/ChatBlock/hooks/useDeepCompareMemoize.js +17 -0
- package/src/ChatBlock/hooks/useMarked.js +44 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +119 -0
- package/src/ChatBlock/hooks/useScrollonStream.ts +131 -0
- package/src/ChatBlock/hooks/useToolDisplayTiming.ts +80 -0
- package/src/ChatBlock/index.js +32 -0
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +235 -0
- package/src/ChatBlock/packets/RendererComponent.tsx +115 -0
- package/src/ChatBlock/packets/index.ts +4 -0
- package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +63 -0
- package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +59 -0
- package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +62 -0
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +172 -0
- package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +122 -0
- package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +323 -0
- package/src/ChatBlock/packets/renderers/index.ts +6 -0
- package/src/ChatBlock/schema.js +403 -0
- package/src/ChatBlock/services/index.ts +3 -0
- package/src/ChatBlock/services/messageProcessor.ts +348 -0
- package/src/ChatBlock/services/packetUtils.ts +48 -0
- package/src/ChatBlock/services/streamingService.ts +342 -0
- package/src/ChatBlock/style.less +1881 -0
- package/src/ChatBlock/tests/AIMessage.test.jsx +95 -0
- package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +49 -0
- package/src/ChatBlock/tests/BlinkingDot.test.jsx +71 -0
- package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +73 -0
- package/src/ChatBlock/tests/Citation.test.jsx +107 -0
- package/src/ChatBlock/tests/EmptyState.test.jsx +137 -0
- package/src/ChatBlock/tests/FeedbackModal.test.jsx +138 -0
- package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +94 -0
- package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +105 -0
- package/src/ChatBlock/tests/RelatedQuestions.test.jsx +215 -0
- package/src/ChatBlock/tests/Source.test.jsx +79 -0
- package/src/ChatBlock/tests/Spinner.test.jsx +18 -0
- package/src/ChatBlock/tests/index.test.js +51 -0
- package/src/ChatBlock/tests/messageProcessor.test.jsx +154 -0
- package/src/ChatBlock/tests/schema.test.js +166 -0
- package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +107 -0
- package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +151 -0
- package/src/ChatBlock/types/cssmodules.d.ts +7 -0
- package/src/ChatBlock/types/interfaces.ts +154 -0
- package/src/ChatBlock/types/slate.d.ts +3 -0
- package/src/ChatBlock/types/streamingModels.ts +267 -0
- package/src/ChatBlock/types/volto.d.ts +3 -0
- package/src/ChatBlock/utils/citations.ts +25 -0
- package/src/ChatBlock/utils/index.tsx +114 -0
- package/src/halloumi/README.md +1 -0
- package/src/halloumi/generative.js +219 -0
- package/src/halloumi/generative.test.js +88 -0
- package/src/halloumi/middleware.js +70 -0
- package/src/halloumi/postprocessing.js +273 -0
- package/src/halloumi/postprocessing.test.js +441 -0
- package/src/halloumi/preprocessing.js +115 -0
- package/src/halloumi/preprocessing.test.js +245 -0
- package/src/icons/bot.svg +1 -0
- package/src/icons/check.svg +1 -0
- package/src/icons/chevron.svg +3 -0
- package/src/icons/clear.svg +1 -0
- package/src/icons/copy.svg +1 -0
- package/src/icons/done.svg +5 -0
- package/src/icons/external-link.svg +1 -0
- package/src/icons/file.svg +1 -0
- package/src/icons/glasses.svg +1 -0
- package/src/icons/globe.svg +1 -0
- package/src/icons/rotate.svg +1 -0
- package/src/icons/search.svg +5 -0
- package/src/icons/send.svg +1 -0
- package/src/icons/square-pen.svg +1 -0
- package/src/icons/stop.svg +9 -0
- package/src/icons/thumbs-down.svg +1 -0
- package/src/icons/thumbs-up.svg +1 -0
- package/src/icons/user.svg +1 -0
- package/src/index.js +58 -0
- package/src/middleware.js +250 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import {
|
|
5
|
+
getClaimsFromResponse,
|
|
6
|
+
getTokenProbabilitiesFromLogits,
|
|
7
|
+
} from './postprocessing';
|
|
8
|
+
import { createHalloumiPrompt } from './preprocessing';
|
|
9
|
+
|
|
10
|
+
// const CONTEXT_SEPARTOR = '\n---\n';
|
|
11
|
+
|
|
12
|
+
const log = debug('halloumi');
|
|
13
|
+
|
|
14
|
+
function sigmoid(x) {
|
|
15
|
+
return 1 / (1 + Math.exp(-x));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function applyPlattScaling(platt, probability) {
|
|
19
|
+
probability = Math.min(Math.max(probability, 1e-6), 1 - 1e-6);
|
|
20
|
+
const log_prob = Math.log(probability / (1 - probability));
|
|
21
|
+
return sigmoid(-1 * (platt.a * log_prob + platt.b));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getVerifyClaimResponse(
|
|
25
|
+
model,
|
|
26
|
+
sources,
|
|
27
|
+
claims,
|
|
28
|
+
maxContextSegments = 0,
|
|
29
|
+
) {
|
|
30
|
+
// const contextSeparator = CONTEXT_SEPARTOR;
|
|
31
|
+
// const joinedContext = sources.join(contextSeparator);
|
|
32
|
+
|
|
33
|
+
if (!sources?.length || !claims) {
|
|
34
|
+
const response = {
|
|
35
|
+
claims: [],
|
|
36
|
+
segments: {},
|
|
37
|
+
};
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const prompt = createHalloumiPrompt({
|
|
42
|
+
sources,
|
|
43
|
+
response: claims,
|
|
44
|
+
maxContextSegments,
|
|
45
|
+
request: undefined,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
log('Halloumi prompt', JSON.stringify(prompt, null, 2));
|
|
49
|
+
|
|
50
|
+
const rawClaims = await halloumiGenerativeAPI(model, prompt);
|
|
51
|
+
log('Raw claims', rawClaims);
|
|
52
|
+
const result = {
|
|
53
|
+
...convertGenerativesClaimToVerifyClaimResponse(rawClaims, prompt),
|
|
54
|
+
rawClaims,
|
|
55
|
+
halloumiPrompt: prompt,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tokenChoices = new Set(['supported', 'unsupported']);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetches a response from the LLM.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} model The model configuration.
|
|
67
|
+
* @param {object} prompt The prompt to send to the LLM.
|
|
68
|
+
* @returns {Promise<object>} The JSON response from the LLM.
|
|
69
|
+
*
|
|
70
|
+
* Environment Variables:
|
|
71
|
+
* - `MOCK_HALLOUMI_FILE_PATH`: If set, the function reads the LLM response from the specified file path instead of making an API call.
|
|
72
|
+
* - `DUMP_HALLOUMI_REQ_FILE_PATH`: If set, the LLM request (URL and parameters) is dumped to the specified file path.
|
|
73
|
+
* - `DUMP_HALLOUMI_FILE_PATH`: If set, the LLM response is dumped to the specified file path.
|
|
74
|
+
*/
|
|
75
|
+
async function getLLMResponse(model, prompt) {
|
|
76
|
+
let jsonData;
|
|
77
|
+
|
|
78
|
+
if (process.env.MOCK_HALLOUMI_FILE_PATH) {
|
|
79
|
+
const filePath = process.env.MOCK_HALLOUMI_FILE_PATH;
|
|
80
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
81
|
+
jsonData = JSON.parse(fileContent);
|
|
82
|
+
return jsonData;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const data = {
|
|
86
|
+
messages: [{ role: 'user', content: prompt.prompt }],
|
|
87
|
+
temperature: 0.0,
|
|
88
|
+
model: model.name,
|
|
89
|
+
logprobs: true,
|
|
90
|
+
top_logprobs: 3,
|
|
91
|
+
};
|
|
92
|
+
const headers = {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
};
|
|
96
|
+
if (model.apiKey) {
|
|
97
|
+
headers['Authorization'] = `Bearer ${model.apiKey}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const params = {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: headers,
|
|
103
|
+
body: JSON.stringify(data),
|
|
104
|
+
};
|
|
105
|
+
if (process.env.DUMP_HALLOUMI_REQ_FILE_PATH) {
|
|
106
|
+
const filePath = process.env.DUMP_HALLOUMI_REQ_FILE_PATH;
|
|
107
|
+
fs.writeFileSync(
|
|
108
|
+
filePath,
|
|
109
|
+
JSON.stringify(
|
|
110
|
+
{ url: model.apiUrl, params: { ...params, body: data } },
|
|
111
|
+
null,
|
|
112
|
+
2,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
log(`Dumped halloumi response: ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const response = await fetch(model.apiUrl, params);
|
|
119
|
+
jsonData = await response.json();
|
|
120
|
+
|
|
121
|
+
if (process.env.DUMP_HALLOUMI_FILE_PATH) {
|
|
122
|
+
const filePath = process.env.DUMP_HALLOUMI_FILE_PATH;
|
|
123
|
+
fs.writeFileSync(filePath, JSON.stringify(jsonData, null, 2));
|
|
124
|
+
log(`Dumped halloumi response: ${filePath}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return jsonData;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Gets all claims from a response.
|
|
132
|
+
* @param response A string containing all claims and their information.
|
|
133
|
+
* @returns A list of claim objects.
|
|
134
|
+
*/
|
|
135
|
+
export async function halloumiGenerativeAPI(model, prompt) {
|
|
136
|
+
const jsonData = await getLLMResponse(model, prompt);
|
|
137
|
+
|
|
138
|
+
log('Generative response', jsonData);
|
|
139
|
+
log('Logprobs', jsonData.choices[0].logprobs.content);
|
|
140
|
+
|
|
141
|
+
const logits = jsonData.choices[0].logprobs.content;
|
|
142
|
+
const tokenProbabilities = getTokenProbabilitiesFromLogits(
|
|
143
|
+
logits,
|
|
144
|
+
tokenChoices,
|
|
145
|
+
);
|
|
146
|
+
const parsedResponse = getClaimsFromResponse(
|
|
147
|
+
jsonData.choices[0].message.content,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (parsedResponse.length !== tokenProbabilities.length) {
|
|
151
|
+
throw new Error('Token probabilities and claims do not match.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < parsedResponse.length; i++) {
|
|
155
|
+
const scoreMap = tokenProbabilities[i];
|
|
156
|
+
if (model.plattScaling) {
|
|
157
|
+
const platt = model.plattScaling;
|
|
158
|
+
const unsupportedScore = applyPlattScaling(
|
|
159
|
+
platt,
|
|
160
|
+
scoreMap.get('unsupported'),
|
|
161
|
+
);
|
|
162
|
+
const supportedScore = 1 - unsupportedScore;
|
|
163
|
+
scoreMap.set('supported', supportedScore);
|
|
164
|
+
scoreMap.set('unsupported', unsupportedScore);
|
|
165
|
+
}
|
|
166
|
+
parsedResponse[i].probabilities = scoreMap;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return parsedResponse;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function convertGenerativesClaimToVerifyClaimResponse(
|
|
173
|
+
generativeClaims,
|
|
174
|
+
prompt,
|
|
175
|
+
) {
|
|
176
|
+
const segments = {};
|
|
177
|
+
const claims = [];
|
|
178
|
+
|
|
179
|
+
for (const offset of prompt.contextOffsets) {
|
|
180
|
+
const id = offset[0].toString();
|
|
181
|
+
segments[id] = {
|
|
182
|
+
id,
|
|
183
|
+
startOffset: offset[1].startOffset,
|
|
184
|
+
endOffset: offset[1].endOffset,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const generativeClaim of generativeClaims) {
|
|
189
|
+
const segmentIds = [];
|
|
190
|
+
for (const seg of generativeClaim.segments) {
|
|
191
|
+
segmentIds.push(seg.toString());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const claimId = generativeClaim.claimId;
|
|
195
|
+
if (!prompt.responseOffsets.has(claimId)) {
|
|
196
|
+
throw new Error(`Claim ${claimId} not found in response offsets.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const claimResponseWindow = prompt.responseOffsets.get(claimId);
|
|
200
|
+
const score = generativeClaim.probabilities.get('supported');
|
|
201
|
+
const claim = {
|
|
202
|
+
claimId,
|
|
203
|
+
claimString: generativeClaim.claimString,
|
|
204
|
+
startOffset: claimResponseWindow.startOffset,
|
|
205
|
+
endOffset: claimResponseWindow.endOffset,
|
|
206
|
+
rationale: generativeClaim.explanation,
|
|
207
|
+
segmentIds,
|
|
208
|
+
score,
|
|
209
|
+
};
|
|
210
|
+
claims.push(claim);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const response = {
|
|
214
|
+
claims,
|
|
215
|
+
segments,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return response;
|
|
219
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
halloumiGenerativeAPI,
|
|
3
|
+
convertGenerativesClaimToVerifyClaimResponse,
|
|
4
|
+
} from './generative';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
describe('halloumiGenerativeAPI reads from mock file', () => {
|
|
8
|
+
const originalEnv = process.env;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.resetModules(); // Most important - reset modules between test runs
|
|
12
|
+
process.env = {
|
|
13
|
+
...originalEnv,
|
|
14
|
+
MOCK_HALLOUMI_FILE_PATH: path.join(__dirname, '../dummy/qa-raw-3.json'),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
process.env = originalEnv; // Restore original env
|
|
20
|
+
jest.restoreAllMocks(); // Restore all mocks
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should read from the mock file when MOCK_LLM_CALL is true', async () => {
|
|
24
|
+
const model = { name: 'test-model', apiUrl: 'http://test.com' };
|
|
25
|
+
const prompt = {
|
|
26
|
+
prompt: 'test-prompt',
|
|
27
|
+
contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
|
|
28
|
+
responseOffsets: new Map([[1, { startOffset: 0, endOffset: 20 }]]),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// We are testing halloumiGenerativeAPI which internally calls getLLMResponse
|
|
32
|
+
// and getLLMResponse uses the MOCK_LLM_CALL env variable.
|
|
33
|
+
const response = await halloumiGenerativeAPI(model, prompt);
|
|
34
|
+
|
|
35
|
+
expect(response[0].claimString).toEqual(
|
|
36
|
+
'**France – total waste generation (latest available data)** \n',
|
|
37
|
+
);
|
|
38
|
+
expect(response[0].segments).toEqual([
|
|
39
|
+
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('convertGenerativesClaimToVerifyClaimResponse', () => {
|
|
45
|
+
it('should correctly convert generative claims to verify claim response', () => {
|
|
46
|
+
const generativeClaims = [
|
|
47
|
+
{
|
|
48
|
+
claimId: 1,
|
|
49
|
+
claimString: 'Test claim string',
|
|
50
|
+
subclaims: ['subclaim1', 'subclaim2'],
|
|
51
|
+
segments: [1, 2, 3],
|
|
52
|
+
explanation: 'Test explanation',
|
|
53
|
+
supported: true,
|
|
54
|
+
probabilities: new Map([
|
|
55
|
+
['supported', 0.9],
|
|
56
|
+
['unsupported', 0.1],
|
|
57
|
+
]),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const prompt = {
|
|
62
|
+
contextOffsets: new Map([[1, { startOffset: 0, endOffset: 10 }]]),
|
|
63
|
+
responseOffsets: new Map([[1, { startOffset: 100, endOffset: 120 }]]),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = convertGenerativesClaimToVerifyClaimResponse(
|
|
67
|
+
generativeClaims,
|
|
68
|
+
prompt,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
claims: [
|
|
73
|
+
{
|
|
74
|
+
claimId: 1,
|
|
75
|
+
claimString: 'Test claim string',
|
|
76
|
+
startOffset: 100,
|
|
77
|
+
endOffset: 120,
|
|
78
|
+
rationale: 'Test explanation',
|
|
79
|
+
segmentIds: ['1', '2', '3'],
|
|
80
|
+
score: 0.9,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
segments: {
|
|
84
|
+
1: { id: '1', startOffset: 0, endOffset: 10 },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { getVerifyClaimResponse } from './generative';
|
|
3
|
+
|
|
4
|
+
const log = debug('halloumi');
|
|
5
|
+
|
|
6
|
+
const MSG_INVALID_CONFIGURATION =
|
|
7
|
+
'Invalid configuration: missing LLMGW_TOKEN or LLMGW_URL';
|
|
8
|
+
|
|
9
|
+
const LLMGW_URL = process.env.LLMGW_URL;
|
|
10
|
+
const LLMGW_TOKEN = process.env.LLMGW_TOKEN;
|
|
11
|
+
|
|
12
|
+
const generativeModel = {
|
|
13
|
+
name: 'Inhouse-LLM/HallOumi-8B',
|
|
14
|
+
apiUrl: `${LLMGW_URL}/chat/completions`,
|
|
15
|
+
plattScaling: {
|
|
16
|
+
a: -0.5764390035379638,
|
|
17
|
+
b: 0.16648741572432335,
|
|
18
|
+
},
|
|
19
|
+
isEmbeddingModel: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const classifyModel = {
|
|
23
|
+
name: 'Inhouse-LLM/HallOumi-8B-classifier',
|
|
24
|
+
apiUrl: `${LLMGW_URL}/v1/embeddings`,
|
|
25
|
+
isEmbeddingModel: true,
|
|
26
|
+
plattScaling: {
|
|
27
|
+
a: -0.9468640744087437,
|
|
28
|
+
b: -0.07379217647931409,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default async function middleware(req, res, next) {
|
|
33
|
+
const path = req.url.replace('/_ha/', '/');
|
|
34
|
+
|
|
35
|
+
if (!(LLMGW_TOKEN && LLMGW_URL)) {
|
|
36
|
+
res.send({
|
|
37
|
+
error: MSG_INVALID_CONFIGURATION,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const model = {
|
|
43
|
+
...(path === '/classify' ? classifyModel : generativeModel),
|
|
44
|
+
apiKey: LLMGW_TOKEN,
|
|
45
|
+
};
|
|
46
|
+
const body = req.body;
|
|
47
|
+
|
|
48
|
+
log('Halloumi body', body);
|
|
49
|
+
const { sources, answer, maxContextSegments = 0 } = body;
|
|
50
|
+
|
|
51
|
+
res.set('Content-Type', 'application/json');
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const resp = await getVerifyClaimResponse(
|
|
55
|
+
model,
|
|
56
|
+
// TODO: map with citation id
|
|
57
|
+
sources,
|
|
58
|
+
answer,
|
|
59
|
+
maxContextSegments,
|
|
60
|
+
);
|
|
61
|
+
log('Halloumi response', resp);
|
|
62
|
+
res.send(resp);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
res.status(500).send({
|
|
65
|
+
error: `Halloumi error: ${error}`,
|
|
66
|
+
traceback: error.stack || null,
|
|
67
|
+
});
|
|
68
|
+
// throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// /**
|
|
2
|
+
// * Represents a claim object with all relevant information.
|
|
3
|
+
// */
|
|
4
|
+
// export interface GenerativeClaim {
|
|
5
|
+
// claimId: number;
|
|
6
|
+
// claimString: string;
|
|
7
|
+
// subclaims: string[];
|
|
8
|
+
// segments: number[];
|
|
9
|
+
// explanation: string;
|
|
10
|
+
// supported: boolean;
|
|
11
|
+
// probabilities: Map<string, number>;
|
|
12
|
+
// }
|
|
13
|
+
//
|
|
14
|
+
// export interface OpenAITokenLogProb {
|
|
15
|
+
// token: string;
|
|
16
|
+
// bytes: number[];
|
|
17
|
+
// logprob: number;
|
|
18
|
+
// }
|
|
19
|
+
//
|
|
20
|
+
// export interface OpenAILogProb {
|
|
21
|
+
// token: string;
|
|
22
|
+
// bytes: number[];
|
|
23
|
+
// logprob: number;
|
|
24
|
+
// top_logprobs: OpenAITokenLogProb[];
|
|
25
|
+
// }
|
|
26
|
+
//
|
|
27
|
+
/**
|
|
28
|
+
* Gets the claim id from a subsegment.
|
|
29
|
+
* @param subsegment A subsegment string of the form "<|r1|".
|
|
30
|
+
* @returns The numeric claim id.
|
|
31
|
+
*/
|
|
32
|
+
function getClaimIdFromSubsegment(subsegment) {
|
|
33
|
+
const textClaimId = subsegment.split('|')[1];
|
|
34
|
+
const integerClaimId = parseInt(textClaimId.split('r')[1]);
|
|
35
|
+
return integerClaimId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the citations from a subsegment.
|
|
40
|
+
* @param subsegment A subsegment string of the form "|s1|,|s2|,|s3|,|s4|".
|
|
41
|
+
* @returns The list of numeric citations.
|
|
42
|
+
*/
|
|
43
|
+
function getClaimCitationsFromSubsegment(subsegment) {
|
|
44
|
+
const citationSegments = subsegment.split(',');
|
|
45
|
+
const segments = [];
|
|
46
|
+
for (const citationSegment of citationSegments) {
|
|
47
|
+
const segment = citationSegment.replaceAll('|', '').replaceAll('s', '');
|
|
48
|
+
if (segment.includes('-')) {
|
|
49
|
+
const segmentRange = segment.split('-');
|
|
50
|
+
for (
|
|
51
|
+
let i = parseInt(segmentRange[0].trim());
|
|
52
|
+
i <= parseInt(segmentRange[1].trim());
|
|
53
|
+
i++
|
|
54
|
+
) {
|
|
55
|
+
segments.push(i);
|
|
56
|
+
}
|
|
57
|
+
} else if (segment.includes('to')) {
|
|
58
|
+
const segmentRange = segment.split('to');
|
|
59
|
+
for (
|
|
60
|
+
let i = parseInt(segmentRange[0].trim());
|
|
61
|
+
i <= parseInt(segmentRange[1].trim());
|
|
62
|
+
i++
|
|
63
|
+
) {
|
|
64
|
+
segments.push(i);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const segmentInt = parseInt(segment);
|
|
68
|
+
if (!isNaN(segmentInt)) {
|
|
69
|
+
segments.push(parseInt(segment));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return segments;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets the support status from a subsegment.
|
|
78
|
+
* @param subsegment A subsegment string of the form "|supported|" or "|unsupported|".
|
|
79
|
+
* @returns True if the claim is supported, false otherwise.
|
|
80
|
+
*/
|
|
81
|
+
function getSupportStatusFromSubsegment(subsegment) {
|
|
82
|
+
return subsegment.startsWith('|supported|');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Gets a claim from a segment string.
|
|
87
|
+
* @param segment A segment string containing all information for claim verification.
|
|
88
|
+
* @returns The claim object with all relevant information.
|
|
89
|
+
*/
|
|
90
|
+
function getClaimFromSegment(segment) {
|
|
91
|
+
const claim_segments = segment.split('><');
|
|
92
|
+
const claimId = getClaimIdFromSubsegment(claim_segments[0]);
|
|
93
|
+
// Strip trailing '>' which can occur in malformed segments without subclaims
|
|
94
|
+
const claimString = claim_segments[1]?.replace(/>$/, '') || '';
|
|
95
|
+
|
|
96
|
+
const subclaims = [];
|
|
97
|
+
let claimProgressIndex = 3; // Start at 3 to skip the claim id, claim string and the subclaims tag
|
|
98
|
+
for (let i = claimProgressIndex; i < claim_segments.length; i++) {
|
|
99
|
+
const subsegment = claim_segments[i];
|
|
100
|
+
if (subsegment.startsWith('end||subclaims')) {
|
|
101
|
+
claimProgressIndex = i + 1;
|
|
102
|
+
break;
|
|
103
|
+
} else {
|
|
104
|
+
subclaims.push(subsegment);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let cite_tag_index = -1;
|
|
109
|
+
let explanation_index = -1;
|
|
110
|
+
let label_index = -1;
|
|
111
|
+
for (let i = claimProgressIndex; i < claim_segments.length; i++) {
|
|
112
|
+
const subsegment = claim_segments[i];
|
|
113
|
+
if (subsegment.startsWith('|cite|')) {
|
|
114
|
+
cite_tag_index = i;
|
|
115
|
+
} else if (subsegment.startsWith('|explain|')) {
|
|
116
|
+
explanation_index = i + 1;
|
|
117
|
+
} else if (
|
|
118
|
+
subsegment.startsWith('|supported|') ||
|
|
119
|
+
subsegment.startsWith('|unsupported|')
|
|
120
|
+
) {
|
|
121
|
+
label_index = i;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const segments =
|
|
126
|
+
cite_tag_index >= 0
|
|
127
|
+
? getClaimCitationsFromSubsegment(claim_segments[cite_tag_index + 1])
|
|
128
|
+
: [];
|
|
129
|
+
const explanation =
|
|
130
|
+
explanation_index >= 0 ? claim_segments[explanation_index] : '';
|
|
131
|
+
|
|
132
|
+
// Default to true if label not found
|
|
133
|
+
const supported =
|
|
134
|
+
label_index >= 0
|
|
135
|
+
? getSupportStatusFromSubsegment(claim_segments[label_index])
|
|
136
|
+
: true;
|
|
137
|
+
|
|
138
|
+
const claim = {
|
|
139
|
+
claimId,
|
|
140
|
+
claimString,
|
|
141
|
+
subclaims,
|
|
142
|
+
segments,
|
|
143
|
+
explanation,
|
|
144
|
+
supported,
|
|
145
|
+
probabilities: new Map(),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return claim;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Gets all claims from a response.
|
|
153
|
+
* @param response A string containing all claims and their information.
|
|
154
|
+
* @returns A list of claim objects.
|
|
155
|
+
*/
|
|
156
|
+
export function getClaimsFromResponse(response) {
|
|
157
|
+
// 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>
|
|
158
|
+
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
// console.log('getClaimsFromResponse', response);
|
|
161
|
+
let segments = response.split('<end||r>');
|
|
162
|
+
const claims = [];
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < segments.length; i++) {
|
|
165
|
+
const segment = segments[i];
|
|
166
|
+
|
|
167
|
+
if (segment.length === 0) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Skip segments that don't start with a valid claim marker <|r*|>
|
|
172
|
+
if (!segment.match(/^<\|r\d+\|>/)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if this segment is well-formed (has subclaims within it)
|
|
177
|
+
// Well-formed: <|r1|><claim><|subclaims|>...<|supported|>...<end||r>
|
|
178
|
+
// Malformed: <|r1|><claim><end||r><|subclaims|>...<|supported|>...<end||r>
|
|
179
|
+
// The malformed case puts verification data AFTER <end||r>, creating orphan segments
|
|
180
|
+
if (segment.includes('<|subclaims|>')) {
|
|
181
|
+
// Well-formed segment - process normally
|
|
182
|
+
const claim = getClaimFromSegment(segment);
|
|
183
|
+
claims.push(claim);
|
|
184
|
+
} else {
|
|
185
|
+
// Malformed segment - look for orphan data in the next segment
|
|
186
|
+
let fullSegment = segment;
|
|
187
|
+
|
|
188
|
+
// Look ahead for orphan segment (starts with <|subclaims|>)
|
|
189
|
+
if (i + 1 < segments.length) {
|
|
190
|
+
const nextSegment = segments[i + 1];
|
|
191
|
+
if (nextSegment.startsWith('<|subclaims|>')) {
|
|
192
|
+
// Merge: segment ends with '>', orphan starts with '<'
|
|
193
|
+
fullSegment = segment + nextSegment;
|
|
194
|
+
i++; // Skip the orphan in next iteration
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const claim = getClaimFromSegment(fullSegment);
|
|
199
|
+
claims.push(claim);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return claims;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function exp(x) {
|
|
207
|
+
return Math.pow(Math.E, x);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function softmax(logits) {
|
|
211
|
+
const softmaxes = [];
|
|
212
|
+
const exp_values = [];
|
|
213
|
+
let total = 0;
|
|
214
|
+
for (let i = 0; i < logits.length; i++) {
|
|
215
|
+
const exponential = exp(logits[i]);
|
|
216
|
+
total += exponential;
|
|
217
|
+
exp_values.push(exponential);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < exp_values.length; i++) {
|
|
221
|
+
softmaxes.push(exp_values[i] / total);
|
|
222
|
+
}
|
|
223
|
+
return softmaxes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getTokenProbabilitiesFromLogit(logit, tokenChoices) {
|
|
227
|
+
const tokenLogits = new Map();
|
|
228
|
+
let smallestLogit = 1000000;
|
|
229
|
+
for (const token of tokenChoices) {
|
|
230
|
+
const tokenLogit = logit.top_logprobs.find(
|
|
231
|
+
(logit) => logit.token === token,
|
|
232
|
+
);
|
|
233
|
+
if (tokenLogit !== undefined) {
|
|
234
|
+
smallestLogit = Math.min(smallestLogit, tokenLogit.logprob);
|
|
235
|
+
tokenLogits.set(token, tokenLogit.logprob);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// If any class tokens aren't present, set them below the smallest logit.
|
|
240
|
+
// Their true value is definitely <= the smallest logit value.
|
|
241
|
+
for (const token of tokenChoices) {
|
|
242
|
+
if (!tokenLogits.has(token)) {
|
|
243
|
+
tokenLogits.set(token, smallestLogit - 1e-6);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const classTokens = Array.from(tokenLogits.keys());
|
|
248
|
+
const logitValues = Array.from(tokenLogits.values());
|
|
249
|
+
const softmaxValues = softmax(logitValues);
|
|
250
|
+
const tokenProbabilities = new Map();
|
|
251
|
+
for (let i = 0; i < classTokens.length; i++) {
|
|
252
|
+
tokenProbabilities.set(classTokens[i], softmaxValues[i]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return tokenProbabilities;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function getTokenProbabilitiesFromLogits(logits, tokenChoices) {
|
|
259
|
+
const tokenProbabilities = [];
|
|
260
|
+
for (const logit of logits) {
|
|
261
|
+
const tokenPresent = tokenChoices.has(logit.token);
|
|
262
|
+
if (!tokenPresent) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const tokenProbability = getTokenProbabilitiesFromLogit(
|
|
267
|
+
logit,
|
|
268
|
+
tokenChoices,
|
|
269
|
+
);
|
|
270
|
+
tokenProbabilities.push(tokenProbability);
|
|
271
|
+
}
|
|
272
|
+
return tokenProbabilities;
|
|
273
|
+
}
|