@eeacms/volto-eea-chatbot 3.0.1 → 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/CHANGELOG.md +26 -4
- package/README.md +43 -12
- 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/services/messageProcessor.ts +7 -3
- 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/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,172 +0,0 @@
|
|
|
1
|
-
import nlp from 'compromise';
|
|
2
|
-
|
|
3
|
-
const MIN_SENTENCE_LENGTH = 15;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Splits markdown text into segments meaningful for fact-checking.
|
|
7
|
-
*
|
|
8
|
-
* Strategy:
|
|
9
|
-
* 1. Split into markdown blocks (paragraphs, list items, table rows, headers)
|
|
10
|
-
* 2. For prose blocks, further split into sentences using Intl.Segmenter
|
|
11
|
-
* 3. Merge short fragments (< MIN_SENTENCE_LENGTH) into the next sentence
|
|
12
|
-
*
|
|
13
|
-
* Structural blocks (table rows, headers, list items) are kept as-is since
|
|
14
|
-
* they are already atomic units.
|
|
15
|
-
*
|
|
16
|
-
* @param {string} text Markdown text to split.
|
|
17
|
-
* @returns {string[]} Array of segment strings.
|
|
18
|
-
*/
|
|
19
|
-
export function splitMarkdown(text) {
|
|
20
|
-
const blocks = splitIntoBlocks(text);
|
|
21
|
-
const segments = [];
|
|
22
|
-
|
|
23
|
-
for (const block of blocks) {
|
|
24
|
-
if (isStructuralBlock(block)) {
|
|
25
|
-
if (block.trim().length > 0) {
|
|
26
|
-
segments.push(block);
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
segments.push(...splitProse(block));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return segments;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Splits markdown text into structural blocks.
|
|
38
|
-
* Separates: headers, table rows, list items, horizontal rules, and prose.
|
|
39
|
-
* Prose lines within the same paragraph are joined together.
|
|
40
|
-
*/
|
|
41
|
-
function splitIntoBlocks(text) {
|
|
42
|
-
const lines = text.split('\n');
|
|
43
|
-
const blocks = [];
|
|
44
|
-
let currentProse = '';
|
|
45
|
-
|
|
46
|
-
const flushProse = () => {
|
|
47
|
-
if (currentProse) {
|
|
48
|
-
blocks.push(currentProse);
|
|
49
|
-
currentProse = '';
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
for (const line of lines) {
|
|
54
|
-
const trimmed = line.trimStart();
|
|
55
|
-
|
|
56
|
-
if (isTableRow(trimmed)) {
|
|
57
|
-
flushProse();
|
|
58
|
-
blocks.push(line);
|
|
59
|
-
} else if (isTableSeparator(trimmed)) {
|
|
60
|
-
flushProse();
|
|
61
|
-
// Skip separator rows — not verifiable content
|
|
62
|
-
} else if (isHeader(trimmed)) {
|
|
63
|
-
flushProse();
|
|
64
|
-
blocks.push(line);
|
|
65
|
-
} else if (isHorizontalRule(trimmed)) {
|
|
66
|
-
flushProse();
|
|
67
|
-
// Skip horizontal rules
|
|
68
|
-
} else if (isListItem(trimmed)) {
|
|
69
|
-
flushProse();
|
|
70
|
-
blocks.push(line);
|
|
71
|
-
} else if (trimmed === '') {
|
|
72
|
-
flushProse();
|
|
73
|
-
} else {
|
|
74
|
-
// Prose continuation — join into a single paragraph
|
|
75
|
-
currentProse += (currentProse ? ' ' : '') + line.trim();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
flushProse();
|
|
80
|
-
return blocks;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isTableRow(line) {
|
|
84
|
-
return /^\|.*\|/.test(line) && !isTableSeparator(line);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isTableSeparator(line) {
|
|
88
|
-
return /^\|[\s\-:|]+\|$/.test(line);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isHeader(line) {
|
|
92
|
-
return /^#{1,6}\s/.test(line);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isHorizontalRule(line) {
|
|
96
|
-
return /^(\*{3,}|-{3,}|_{3,})\s*$/.test(line);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function isListItem(line) {
|
|
100
|
-
return /^(\d+\.\s+|[-*+]\s+)/.test(line);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Returns true if a block is structural (table row, header, list item)
|
|
105
|
-
* and should not be further split into sentences.
|
|
106
|
-
*/
|
|
107
|
-
function isStructuralBlock(block) {
|
|
108
|
-
const trimmed = block.trimStart();
|
|
109
|
-
return isTableRow(trimmed) || isHeader(trimmed) || isListItem(trimmed);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Splits a prose paragraph into sentences using sbd (sentence boundary detection),
|
|
114
|
-
* merging short fragments that aren't independently verifiable.
|
|
115
|
-
* sbd handles abbreviations (Dr., Mr., U.S., Ph.D.) correctly.
|
|
116
|
-
*/
|
|
117
|
-
export function splitProse(text, maxSentences = 0) {
|
|
118
|
-
// const segments = segment('en', text);
|
|
119
|
-
const doc = nlp(text);
|
|
120
|
-
const initialSentences = doc.sentences().out('array');
|
|
121
|
-
|
|
122
|
-
// Find each sentence's position in the original text
|
|
123
|
-
const positions = getPositions(text, initialSentences);
|
|
124
|
-
|
|
125
|
-
// Merge short sentences (< MIN_SENTENCE_LENGTH) into the next sentence
|
|
126
|
-
const merged = [];
|
|
127
|
-
const mergedPositions = [];
|
|
128
|
-
let pendingStart = null;
|
|
129
|
-
for (let i = 0; i < initialSentences.length; i++) {
|
|
130
|
-
if (pendingStart === null) {
|
|
131
|
-
pendingStart = positions[i].start;
|
|
132
|
-
}
|
|
133
|
-
if (
|
|
134
|
-
initialSentences[i].replaceAll('\n', '').length < MIN_SENTENCE_LENGTH &&
|
|
135
|
-
i < initialSentences.length - 1
|
|
136
|
-
) {
|
|
137
|
-
// Too short — will be merged with the next sentence
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const s = text.slice(pendingStart, positions[i].end);
|
|
141
|
-
merged.push(s);
|
|
142
|
-
mergedPositions.push({ start: pendingStart, end: positions[i].end });
|
|
143
|
-
pendingStart = null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (maxSentences && merged.length > maxSentences) {
|
|
147
|
-
// Merge groups by slicing original text to preserve separators
|
|
148
|
-
const groupSize = Math.ceil(merged.length / maxSentences);
|
|
149
|
-
const groupedSentences = [];
|
|
150
|
-
for (let i = 0; i < merged.length; i += groupSize) {
|
|
151
|
-
const groupStart = mergedPositions[i].start;
|
|
152
|
-
const groupEnd =
|
|
153
|
-
mergedPositions[Math.min(i + groupSize, merged.length) - 1].end;
|
|
154
|
-
groupedSentences.push(text.slice(groupStart, groupEnd));
|
|
155
|
-
}
|
|
156
|
-
return groupedSentences;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return merged;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function getPositions(text, sentences) {
|
|
163
|
-
const positions = [];
|
|
164
|
-
let searchFrom = 0;
|
|
165
|
-
for (const sentence of sentences) {
|
|
166
|
-
const start = text.indexOf(sentence, searchFrom);
|
|
167
|
-
const end = start + sentence.length;
|
|
168
|
-
positions.push({ start, end });
|
|
169
|
-
searchFrom = end;
|
|
170
|
-
}
|
|
171
|
-
return positions;
|
|
172
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { splitMarkdown } from './markdown-splitter';
|
|
2
|
-
|
|
3
|
-
const DUMMY_LLM_RESPONSE = `## European species most at risk
|
|
4
|
-
|
|
5
|
-
Below is a concise, evidence-based snapshot of the taxonomic groups and flagship species. The list combines information on trend magnitude and risk status.
|
|
6
|
-
|
|
7
|
-
| Taxonomic group | Species at risk | Main drivers | Signal |
|
|
8
|
-
|-----------------|-----------------|--------------|--------|
|
|
9
|
-
| **Birds** | Skylark (*Alauda arvensis*), European turtle-dove | Intensive agriculture, pesticide use | Farmland-bird index down 27% (1990-2019) |
|
|
10
|
-
| **Fish** | Houting (*Coregonus oxyrhynchus*), Danube salmon | River regulation, dam construction | Both listed as critically endangered |
|
|
11
|
-
| **Butterflies** | Large blue (*Phengaris arion*), Adonis blue | Loss of semi-natural grasslands | Grassland-butterfly index down 25% |
|
|
12
|
-
|
|
13
|
-
### Key take-aways
|
|
14
|
-
|
|
15
|
-
1. **Agricultural intensification** is the single biggest pressure across taxa.
|
|
16
|
-
2. **Habitat loss and degradation** underlie the perilous status of many specialists.
|
|
17
|
-
3. **Chemical stressors** affect both insects and the birds that depend on them.
|
|
18
|
-
4. **Climate change** amplifies existing pressures such as drought stress on amphibians.
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
### Conservation priorities
|
|
23
|
-
|
|
24
|
-
- **Protect and restore semi-natural habitats** including high-diversity grasslands and river floodplains.
|
|
25
|
-
- **Implement agri-environment schemes** that limit pesticide use and maintain field margins.
|
|
26
|
-
- **Focus monitoring on flagship species** to gauge the effectiveness of policy actions.
|
|
27
|
-
|
|
28
|
-
If you need a deeper dive into a particular taxon, just let me know!`;
|
|
29
|
-
|
|
30
|
-
describe('splitMarkdown', () => {
|
|
31
|
-
it('should split the dummy LLM response into meaningful segments', () => {
|
|
32
|
-
const segments = splitMarkdown(DUMMY_LLM_RESPONSE);
|
|
33
|
-
|
|
34
|
-
// Headers are separate segments
|
|
35
|
-
expect(segments).toContain('## European species most at risk');
|
|
36
|
-
expect(segments).toContain('### Key take-aways');
|
|
37
|
-
expect(segments).toContain('### Conservation priorities');
|
|
38
|
-
|
|
39
|
-
// Table rows are individual segments (separator skipped)
|
|
40
|
-
const tableRows = segments.filter((s) => s.startsWith('|'));
|
|
41
|
-
expect(tableRows.length).toBe(4); // header row + 3 data rows
|
|
42
|
-
|
|
43
|
-
// Numbered list items are separate
|
|
44
|
-
const numberedItems = segments.filter((s) => /^\d+\./.test(s.trimStart()));
|
|
45
|
-
expect(numberedItems.length).toBe(4);
|
|
46
|
-
|
|
47
|
-
// Bullet list items are separate
|
|
48
|
-
const bulletItems = segments.filter((s) => /^-\s/.test(s.trimStart()));
|
|
49
|
-
expect(bulletItems.length).toBe(3);
|
|
50
|
-
|
|
51
|
-
// Prose paragraphs are split into sentences
|
|
52
|
-
expect(segments).toContainEqual(
|
|
53
|
-
expect.stringContaining('evidence-based snapshot'),
|
|
54
|
-
);
|
|
55
|
-
expect(segments).toContainEqual(expect.stringContaining('trend magnitude'));
|
|
56
|
-
|
|
57
|
-
// Horizontal rules are skipped
|
|
58
|
-
expect(segments).not.toContainEqual(expect.stringMatching(/^-{3,}$/));
|
|
59
|
-
|
|
60
|
-
// No empty segments
|
|
61
|
-
expect(segments.every((s) => s.trim().length > 0)).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should handle plain prose without markdown', () => {
|
|
65
|
-
const text =
|
|
66
|
-
'This is sentence one. This is sentence two. This is sentence three.';
|
|
67
|
-
const segments = splitMarkdown(text);
|
|
68
|
-
expect(segments).toEqual([
|
|
69
|
-
'This is sentence one.',
|
|
70
|
-
'This is sentence two.',
|
|
71
|
-
'This is sentence three.',
|
|
72
|
-
]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should handle headers followed by prose', () => {
|
|
76
|
-
const text = '## My Header\n\nSome prose paragraph here.';
|
|
77
|
-
const segments = splitMarkdown(text);
|
|
78
|
-
expect(segments).toEqual(['## My Header', 'Some prose paragraph here.']);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should skip table separator rows', () => {
|
|
82
|
-
const text = '| A | B |\n|---|---|\n| 1 | 2 |';
|
|
83
|
-
const segments = splitMarkdown(text);
|
|
84
|
-
expect(segments).toEqual(['| A | B |', '| 1 | 2 |']);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should handle numbered lists', () => {
|
|
88
|
-
const text =
|
|
89
|
-
'1. First item with enough text.\n2. Second item also long enough.';
|
|
90
|
-
const segments = splitMarkdown(text);
|
|
91
|
-
expect(segments).toEqual([
|
|
92
|
-
'1. First item with enough text.',
|
|
93
|
-
'2. Second item also long enough.',
|
|
94
|
-
]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should handle bullet lists', () => {
|
|
98
|
-
const text =
|
|
99
|
-
'- First bullet point.\n- Second bullet point.\n- Third bullet point.';
|
|
100
|
-
const segments = splitMarkdown(text);
|
|
101
|
-
expect(segments).toEqual([
|
|
102
|
-
'- First bullet point.',
|
|
103
|
-
'- Second bullet point.',
|
|
104
|
-
'- Third bullet point.',
|
|
105
|
-
]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should skip horizontal rules', () => {
|
|
109
|
-
const text = 'Before the rule.\n\n---\n\nAfter the rule.';
|
|
110
|
-
const segments = splitMarkdown(text);
|
|
111
|
-
expect(segments).toEqual(['Before the rule.', 'After the rule.']);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should return an empty array for empty string', () => {
|
|
115
|
-
expect(splitMarkdown('')).toEqual([]);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should merge short prose fragments', () => {
|
|
119
|
-
const text = 'Hi. This is a longer sentence here.';
|
|
120
|
-
const segments = splitMarkdown(text);
|
|
121
|
-
expect(segments).toEqual(['Hi. This is a longer sentence here.']);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should join multi-line prose into a single paragraph before splitting', () => {
|
|
125
|
-
const text =
|
|
126
|
-
'This is the first line of a paragraph.\nThis is the second line of the same paragraph.';
|
|
127
|
-
const segments = splitMarkdown(text);
|
|
128
|
-
// Should be joined and then split by Intl.Segmenter
|
|
129
|
-
expect(segments.length).toBeGreaterThanOrEqual(1);
|
|
130
|
-
expect(segments.join('')).toContain('first line');
|
|
131
|
-
expect(segments.join('')).toContain('second line');
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gets the claim id from a subsegment.
|
|
3
|
-
* @param subsegment A subsegment string of the form "<|r1|".
|
|
4
|
-
* @returns The numeric claim id.
|
|
5
|
-
*/
|
|
6
|
-
function getClaimIdFromSubsegment(subsegment) {
|
|
7
|
-
const textClaimId = subsegment.split('|')[1];
|
|
8
|
-
const integerClaimId = parseInt(textClaimId.split('r')[1]);
|
|
9
|
-
return integerClaimId;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Gets the citations from a subsegment.
|
|
14
|
-
* @param subsegment A subsegment string of the form "|s1|,|s2|,|s3|,|s4|".
|
|
15
|
-
* @returns The list of numeric citations.
|
|
16
|
-
*/
|
|
17
|
-
function getClaimCitationsFromSubsegment(subsegment) {
|
|
18
|
-
const citationSegments = subsegment.split(',');
|
|
19
|
-
const segments = [];
|
|
20
|
-
for (const citationSegment of citationSegments) {
|
|
21
|
-
const segment = citationSegment.replaceAll('|', '').replaceAll('s', '');
|
|
22
|
-
if (segment.includes('-')) {
|
|
23
|
-
const segmentRange = segment.split('-');
|
|
24
|
-
for (
|
|
25
|
-
let i = parseInt(segmentRange[0].trim());
|
|
26
|
-
i <= parseInt(segmentRange[1].trim());
|
|
27
|
-
i++
|
|
28
|
-
) {
|
|
29
|
-
segments.push(i);
|
|
30
|
-
}
|
|
31
|
-
} else if (segment.includes('to')) {
|
|
32
|
-
const segmentRange = segment.split('to');
|
|
33
|
-
for (
|
|
34
|
-
let i = parseInt(segmentRange[0].trim());
|
|
35
|
-
i <= parseInt(segmentRange[1].trim());
|
|
36
|
-
i++
|
|
37
|
-
) {
|
|
38
|
-
segments.push(i);
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
const segmentInt = parseInt(segment);
|
|
42
|
-
if (!isNaN(segmentInt)) {
|
|
43
|
-
segments.push(parseInt(segment));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return segments;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Gets the support status from a subsegment.
|
|
52
|
-
* @param subsegment A subsegment string of the form "|supported|" or "|unsupported|".
|
|
53
|
-
* @returns True if the claim is supported, false otherwise.
|
|
54
|
-
*/
|
|
55
|
-
function getSupportStatusFromSubsegment(subsegment) {
|
|
56
|
-
return subsegment.startsWith('|supported|');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Gets a claim from a segment string.
|
|
61
|
-
* @param segment A segment string containing all information for claim verification.
|
|
62
|
-
* @returns The claim object with all relevant information.
|
|
63
|
-
*/
|
|
64
|
-
function getClaimFromSegment(segment) {
|
|
65
|
-
const claim_segments = segment.split('><');
|
|
66
|
-
const claimId = getClaimIdFromSubsegment(claim_segments[0]);
|
|
67
|
-
// Strip trailing '>' which can occur in malformed segments without subclaims
|
|
68
|
-
const claimString = claim_segments[1]?.replace(/>$/, '') || '';
|
|
69
|
-
|
|
70
|
-
const subclaims = [];
|
|
71
|
-
let claimProgressIndex = 3; // Start at 3 to skip the claim id, claim string and the subclaims tag
|
|
72
|
-
for (let i = claimProgressIndex; i < claim_segments.length; i++) {
|
|
73
|
-
const subsegment = claim_segments[i];
|
|
74
|
-
if (subsegment.startsWith('end||subclaims')) {
|
|
75
|
-
claimProgressIndex = i + 1;
|
|
76
|
-
break;
|
|
77
|
-
} else {
|
|
78
|
-
subclaims.push(subsegment);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let cite_tag_index = -1;
|
|
83
|
-
let explanation_index = -1;
|
|
84
|
-
let label_index = -1;
|
|
85
|
-
for (let i = claimProgressIndex; i < claim_segments.length; i++) {
|
|
86
|
-
const subsegment = claim_segments[i];
|
|
87
|
-
if (subsegment.startsWith('|cite|')) {
|
|
88
|
-
cite_tag_index = i;
|
|
89
|
-
} else if (subsegment.startsWith('|explain|')) {
|
|
90
|
-
explanation_index = i + 1;
|
|
91
|
-
} else if (
|
|
92
|
-
subsegment.startsWith('|supported|') ||
|
|
93
|
-
subsegment.startsWith('|unsupported|')
|
|
94
|
-
) {
|
|
95
|
-
label_index = i;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const segments =
|
|
100
|
-
cite_tag_index >= 0
|
|
101
|
-
? getClaimCitationsFromSubsegment(claim_segments[cite_tag_index + 1])
|
|
102
|
-
: [];
|
|
103
|
-
const explanation =
|
|
104
|
-
explanation_index >= 0 ? claim_segments[explanation_index] : '';
|
|
105
|
-
|
|
106
|
-
// Default to true if label not found
|
|
107
|
-
const supported =
|
|
108
|
-
label_index >= 0
|
|
109
|
-
? getSupportStatusFromSubsegment(claim_segments[label_index])
|
|
110
|
-
: true;
|
|
111
|
-
|
|
112
|
-
const claim = {
|
|
113
|
-
claimId,
|
|
114
|
-
claimString,
|
|
115
|
-
subclaims,
|
|
116
|
-
segments,
|
|
117
|
-
explanation,
|
|
118
|
-
supported,
|
|
119
|
-
probabilities: new Map(),
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
return claim;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Gets all claims from a response.
|
|
127
|
-
* @param response A string containing all claims and their information.
|
|
128
|
-
* @returns A list of claim objects.
|
|
129
|
-
*/
|
|
130
|
-
export function getClaimsFromResponse(response) {
|
|
131
|
-
// Example response: <|r1|><There is no information about the average lifespan of a giant squid in the deep waters of the Pacific Ocean in the provided document.><|subclaims|><The document contains information about the average lifespan of a giant squid.><The information about giant squid lifespan is related to the Pacific Ocean.><end||subclaims><|cite|><|s1 to s49|><end||cite><|explain|><Upon reviewing the entire document, there is no mention of giant squid or any related topic, including their average lifespan or the Pacific Ocean. The document is focused on international relations, diplomacy, and conflict resolution.><end||explain><|supported|><end||r><|r2|><The document is focused on international relations, diplomacy, and conflict resolution, and does not mention giant squid or any related topic.><|subclaims|><The document is focused on international relations, diplomacy, and conflict resolution.><The document does not mention giant squid or any related topic.><end||subclaims><|cite|><|s1|,|s2|,|s3|,|s4|><end||cite><|explain|><The first four sentences clearly establish the document's focus on international relations, diplomacy, and conflict resolution, and there is no mention of giant squid or any related topic throughout the document.><end||explain><|supported|><end||r><|r3|><The document mentions cats.><|subclaims|><The document makes some mention of cats.><end||subclaims><|cite|><None><end||cite><|explain|><There is no mention of cats anywhere in the document.><end||explain><|unsupported|><end||r>
|
|
132
|
-
|
|
133
|
-
// eslint-disable-next-line no-console
|
|
134
|
-
// console.log('getClaimsFromResponse', response);
|
|
135
|
-
let segments = response.split('<end||r>');
|
|
136
|
-
const claims = [];
|
|
137
|
-
|
|
138
|
-
for (let i = 0; i < segments.length; i++) {
|
|
139
|
-
const segment = segments[i];
|
|
140
|
-
|
|
141
|
-
if (segment.length === 0) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Skip segments that don't start with a valid claim marker <|r*|>
|
|
146
|
-
if (!segment.match(/^<\|r\d+\|>/)) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Check if this segment is well-formed (has subclaims within it)
|
|
151
|
-
// Well-formed: <|r1|><claim><|subclaims|>...<|supported|>...<end||r>
|
|
152
|
-
// Malformed: <|r1|><claim><end||r><|subclaims|>...<|supported|>...<end||r>
|
|
153
|
-
// The malformed case puts verification data AFTER <end||r>, creating orphan segments
|
|
154
|
-
if (segment.includes('<|subclaims|>')) {
|
|
155
|
-
// Well-formed segment - process normally
|
|
156
|
-
const claim = getClaimFromSegment(segment);
|
|
157
|
-
claims.push(claim);
|
|
158
|
-
} else {
|
|
159
|
-
// Malformed segment - look for orphan data in the next segment
|
|
160
|
-
let fullSegment = segment;
|
|
161
|
-
|
|
162
|
-
// Look ahead for orphan segment (starts with <|subclaims|>)
|
|
163
|
-
if (i + 1 < segments.length) {
|
|
164
|
-
const nextSegment = segments[i + 1];
|
|
165
|
-
if (nextSegment.startsWith('<|subclaims|>')) {
|
|
166
|
-
// Merge: segment ends with '>', orphan starts with '<'
|
|
167
|
-
fullSegment = segment + nextSegment;
|
|
168
|
-
i++; // Skip the orphan in next iteration
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const claim = getClaimFromSegment(fullSegment);
|
|
173
|
-
claims.push(claim);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return claims;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function exp(x) {
|
|
181
|
-
return Math.pow(Math.E, x);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function softmax(logits) {
|
|
185
|
-
const softmaxes = [];
|
|
186
|
-
const exp_values = [];
|
|
187
|
-
let total = 0;
|
|
188
|
-
for (let i = 0; i < logits.length; i++) {
|
|
189
|
-
const exponential = exp(logits[i]);
|
|
190
|
-
total += exponential;
|
|
191
|
-
exp_values.push(exponential);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
for (let i = 0; i < exp_values.length; i++) {
|
|
195
|
-
softmaxes.push(exp_values[i] / total);
|
|
196
|
-
}
|
|
197
|
-
return softmaxes;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function getTokenProbabilitiesFromLogit(logit, tokenChoices) {
|
|
201
|
-
const tokenLogits = new Map();
|
|
202
|
-
let smallestLogit = 1000000;
|
|
203
|
-
for (const token of tokenChoices) {
|
|
204
|
-
const tokenLogit = logit.top_logprobs.find(
|
|
205
|
-
(logit) => logit.token === token,
|
|
206
|
-
);
|
|
207
|
-
if (tokenLogit !== undefined) {
|
|
208
|
-
smallestLogit = Math.min(smallestLogit, tokenLogit.logprob);
|
|
209
|
-
tokenLogits.set(token, tokenLogit.logprob);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// If any class tokens aren't present, set them below the smallest logit.
|
|
214
|
-
// Their true value is definitely <= the smallest logit value.
|
|
215
|
-
for (const token of tokenChoices) {
|
|
216
|
-
if (!tokenLogits.has(token)) {
|
|
217
|
-
tokenLogits.set(token, smallestLogit - 1e-6);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const classTokens = Array.from(tokenLogits.keys());
|
|
222
|
-
const logitValues = Array.from(tokenLogits.values());
|
|
223
|
-
const softmaxValues = softmax(logitValues);
|
|
224
|
-
const tokenProbabilities = new Map();
|
|
225
|
-
for (let i = 0; i < classTokens.length; i++) {
|
|
226
|
-
tokenProbabilities.set(classTokens[i], softmaxValues[i]);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return tokenProbabilities;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function getTokenProbabilitiesFromLogits(logits, tokenChoices) {
|
|
233
|
-
const tokenProbabilities = [];
|
|
234
|
-
for (const logit of logits) {
|
|
235
|
-
const tokenPresent = tokenChoices.has(logit.token);
|
|
236
|
-
if (!tokenPresent) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const tokenProbability = getTokenProbabilitiesFromLogit(
|
|
241
|
-
logit,
|
|
242
|
-
tokenChoices,
|
|
243
|
-
);
|
|
244
|
-
tokenProbabilities.push(tokenProbability);
|
|
245
|
-
}
|
|
246
|
-
return tokenProbabilities;
|
|
247
|
-
}
|