@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,441 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const DEFAULT_HALLOUMI_REQUEST =
|
|
2
|
+
'Make one or more claims about information in the documents.';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Splits a given text into sentences using sentence-splitter.
|
|
6
|
+
* @param text The input string to split.
|
|
7
|
+
* @returns An array of sentence strings.
|
|
8
|
+
*/
|
|
9
|
+
export function splitIntoSentences(text, maxSegments = 0) {
|
|
10
|
+
const segmenter = new Intl.Segmenter('en', { granularity: 'sentence' });
|
|
11
|
+
const segments = Array.from(segmenter.segment(text)).map((s) => s.segment);
|
|
12
|
+
|
|
13
|
+
const initialSentences = [];
|
|
14
|
+
let currentSentence = '';
|
|
15
|
+
|
|
16
|
+
for (const segment of segments) {
|
|
17
|
+
currentSentence += segment;
|
|
18
|
+
if (currentSentence.trim().length > 8) {
|
|
19
|
+
initialSentences.push(currentSentence);
|
|
20
|
+
currentSentence = '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Push any remaining part that didn't make it to 8 characters
|
|
24
|
+
if (currentSentence) {
|
|
25
|
+
initialSentences.push(currentSentence);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (maxSegments <= 0) {
|
|
29
|
+
return initialSentences;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (initialSentences.length > maxSegments) {
|
|
33
|
+
const groupSize = Math.ceil(initialSentences.length / maxSegments);
|
|
34
|
+
const mergedSentences = [];
|
|
35
|
+
for (let i = 0; i < initialSentences.length; i += groupSize) {
|
|
36
|
+
const group = initialSentences.slice(i, i + groupSize);
|
|
37
|
+
mergedSentences.push(group.join(''));
|
|
38
|
+
}
|
|
39
|
+
return mergedSentences;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return initialSentences;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Annotate a set of sentences with a given annotation character.
|
|
47
|
+
* @param sentences A list of sentences to annotate.
|
|
48
|
+
* @param annotationChar The character to use for annotation.
|
|
49
|
+
* @returns The annotated string with annotation characters + sentence number.
|
|
50
|
+
*/
|
|
51
|
+
export function annotate(sentences, annotationChar) {
|
|
52
|
+
return sentences
|
|
53
|
+
.map(
|
|
54
|
+
(sentence, i) =>
|
|
55
|
+
`<|${annotationChar}${i + 1}|><${sentence}><end||${annotationChar}>`,
|
|
56
|
+
)
|
|
57
|
+
.join('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getOffsets(originalString, sentences) {
|
|
61
|
+
const offsets = new Map();
|
|
62
|
+
let stringProgressPointer = 0;
|
|
63
|
+
let sentenceId = 1;
|
|
64
|
+
for (const sentence of sentences) {
|
|
65
|
+
const stringToSearch = originalString.slice(stringProgressPointer);
|
|
66
|
+
const startOffset =
|
|
67
|
+
stringToSearch.indexOf(sentence) + stringProgressPointer;
|
|
68
|
+
const endOffset = startOffset + sentence.length;
|
|
69
|
+
stringProgressPointer = endOffset;
|
|
70
|
+
offsets.set(sentenceId, { startOffset: startOffset, endOffset: endOffset });
|
|
71
|
+
sentenceId++;
|
|
72
|
+
}
|
|
73
|
+
return offsets;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates a Halloumi prompt from a given context, request and response.
|
|
78
|
+
* @param context The context or document to reference.
|
|
79
|
+
* @param response The response to the request.
|
|
80
|
+
* @param request The request or question that was used to produce the response.
|
|
81
|
+
* @returns The Halloumi prompt.
|
|
82
|
+
*/
|
|
83
|
+
export function createHalloumiPrompt({
|
|
84
|
+
sources,
|
|
85
|
+
response,
|
|
86
|
+
request,
|
|
87
|
+
maxContextSegments = 0,
|
|
88
|
+
}) {
|
|
89
|
+
const finalRequest = request || DEFAULT_HALLOUMI_REQUEST;
|
|
90
|
+
const contextSentences = sources.flatMap((text) =>
|
|
91
|
+
splitIntoSentences(text, maxContextSegments),
|
|
92
|
+
);
|
|
93
|
+
const joinedContext = sources.join('\n');
|
|
94
|
+
// const contextSentences = splitIntoSentences(sources, maxContextSegments);
|
|
95
|
+
const contextOffsets = getOffsets(joinedContext, contextSentences);
|
|
96
|
+
|
|
97
|
+
const annotatedContextSentences = annotate(contextSentences, 's');
|
|
98
|
+
|
|
99
|
+
const responseSentences = splitIntoSentences(response, maxContextSegments);
|
|
100
|
+
const responseOffsets = getOffsets(response, responseSentences);
|
|
101
|
+
const annotatedResponseSentences = annotate(responseSentences, 'r');
|
|
102
|
+
|
|
103
|
+
const annotatedContext = `<|context|>${annotatedContextSentences}<end||context>`;
|
|
104
|
+
const annotatedRequest = `<|request|><${finalRequest.trim()}><end||request>`;
|
|
105
|
+
const annotatedResponse = `<|response|>${annotatedResponseSentences}<end||response>`;
|
|
106
|
+
|
|
107
|
+
const prompt = `${annotatedContext}${annotatedRequest}${annotatedResponse}`;
|
|
108
|
+
const halloumiPrompt = {
|
|
109
|
+
prompt,
|
|
110
|
+
contextOffsets, // used by convertGenerativesClaimToVerifyClaimResponse
|
|
111
|
+
responseOffsets,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return halloumiPrompt;
|
|
115
|
+
}
|