veriquote 0.2.2 → 0.3.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/CITATION.cff +2 -2
- package/README.md +63 -32
- package/dist/cli/main.js +12 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/gate.d.ts +1 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +14 -0
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/match/fuzzy.d.ts +10 -2
- package/dist/match/fuzzy.d.ts.map +1 -1
- package/dist/match/fuzzy.js +87 -11
- package/dist/match/fuzzy.js.map +1 -1
- package/dist/protocol/evi1.d.ts +21 -1
- package/dist/protocol/evi1.d.ts.map +1 -1
- package/dist/protocol/evi1.js +169 -28
- package/dist/protocol/evi1.js.map +1 -1
- package/dist/protocol/prompt.d.ts +6 -0
- package/dist/protocol/prompt.d.ts.map +1 -1
- package/dist/protocol/prompt.js +35 -10
- package/dist/protocol/prompt.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +52 -15
- package/dist/report.js.map +1 -1
- package/dist/types.d.ts +23 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/cli/main.ts +13 -2
- package/src/gate.ts +16 -1
- package/src/index.ts +1 -0
- package/src/match/fuzzy.ts +98 -11
- package/src/protocol/evi1.ts +174 -29
- package/src/protocol/prompt.ts +42 -9
- package/src/report.ts +53 -21
- package/src/types.ts +20 -2
package/src/protocol/prompt.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface CitationPromptOptions {
|
|
|
13
13
|
quoteLengthRange?: [number, number];
|
|
14
14
|
/** Minimum quote length in characters (with stated exceptions). Default 60. */
|
|
15
15
|
minQuoteLength?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Put the EVI1 block before the answer: the model selects its passages
|
|
18
|
+
* first, then writes only what they support. The parser accepts either
|
|
19
|
+
* position; a streaming client shows nothing until END_EVI1. Default false.
|
|
20
|
+
*/
|
|
21
|
+
evidenceFirst?: boolean;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/** Build the EVI1 citation instruction block for the answering model. */
|
|
@@ -22,8 +28,25 @@ export function buildCitationInstructions(options: CitationPromptOptions = {}):
|
|
|
22
28
|
maxCitationsPerClaim = 2,
|
|
23
29
|
quoteLengthRange = [80, 240],
|
|
24
30
|
minQuoteLength = 60,
|
|
31
|
+
evidenceFirst = false,
|
|
25
32
|
} = options;
|
|
26
33
|
|
|
34
|
+
const appendixIntro = evidenceFirst
|
|
35
|
+
? `Before you write the answer, output the evidence block. Select the passages
|
|
36
|
+
you will rely on first, one line per planned claim and source, then write the
|
|
37
|
+
answer so that every cited sentence states only what its lines support.`
|
|
38
|
+
: 'After you finish the answer, append an appendix:';
|
|
39
|
+
const afterBlock = evidenceFirst
|
|
40
|
+
? '- After END_EVI1, write the user-facing answer. The claim ids {cX} in the answer must be the ones used in the block.'
|
|
41
|
+
: '- Do NOT output anything after END_EVI1.';
|
|
42
|
+
const important = evidenceFirst
|
|
43
|
+
? `- The user-facing answer starts AFTER END_EVI1. Nothing but the block comes before it.
|
|
44
|
+
- The block is required only when you use citations. If you use no
|
|
45
|
+
citations, do NOT output EVI1/END_EVI1.`
|
|
46
|
+
: `- The user-facing answer must end BEFORE the EVI1 appendix starts.
|
|
47
|
+
- The appendix is required only when you used citations. If you used no
|
|
48
|
+
citations, do NOT output EVI1/END_EVI1.`;
|
|
49
|
+
|
|
27
50
|
return `
|
|
28
51
|
[CITATION BUDGET]
|
|
29
52
|
- Keep citations sparse. Only cite new, load-bearing information.
|
|
@@ -50,17 +73,20 @@ export function buildCitationInstructions(options: CitationPromptOptions = {}):
|
|
|
50
73
|
Example: "...water expands when freezing.[2][5]{c1}"
|
|
51
74
|
Bad: "...freezing.[2] {c1}"
|
|
52
75
|
Bad: "...freezing.[2].{c1}"
|
|
53
|
-
- Every sentence OR bullet that contains citations MUST end with exactly ONE claim marker {cX}
|
|
76
|
+
- Every sentence OR bullet that contains citations MUST end with exactly ONE claim marker {cX},
|
|
77
|
+
directly after its [n] group. Never write {cX} without the [n] group, even though the
|
|
78
|
+
appendix names the source as well.
|
|
54
79
|
- X starts at 1 and increases by 1 for each new cited sentence/bullet in THIS answer.
|
|
55
80
|
- Do NOT place {cX} anywhere else. Uncited sentences get no claim marker.
|
|
56
81
|
|
|
57
|
-
[QUOTE-BASED EVIDENCE APPENDIX (MANDATORY)]
|
|
58
|
-
|
|
82
|
+
[QUOTE-BASED EVIDENCE ${evidenceFirst ? 'BLOCK' : 'APPENDIX'} (MANDATORY)]
|
|
83
|
+
${appendixIntro}
|
|
59
84
|
- Start with a line containing exactly: EVI1
|
|
60
85
|
- Then output one line per (claim, source) pair in this exact format:
|
|
61
86
|
cX|n|"QUOTE"
|
|
87
|
+
If one passage does not cover the claim, add further lines for the same pair.
|
|
62
88
|
- End with a line containing exactly: END_EVI1
|
|
63
|
-
|
|
89
|
+
${afterBlock}
|
|
64
90
|
- Do NOT wrap EVI1..END_EVI1 in code fences or markdown.
|
|
65
91
|
|
|
66
92
|
[COMPLETENESS RULES (MANDATORY)]
|
|
@@ -71,10 +97,19 @@ After you finish the answer, append an appendix:
|
|
|
71
97
|
- If you cannot provide a verbatim quote for a citation, REMOVE that citation
|
|
72
98
|
from the answer. Never leave a citation unverified.
|
|
73
99
|
|
|
100
|
+
[SUFFICIENCY RULES (MANDATORY)]
|
|
101
|
+
- The quotes of a cited sentence must together contain EVERY detail it states:
|
|
102
|
+
each number, population, time frame, place, condition, comparison and hedge.
|
|
103
|
+
- If one passage does not cover everything, add another line for the same claim
|
|
104
|
+
(same or other cited source). Otherwise remove the unquoted detail from the
|
|
105
|
+
sentence. Never add detail that no quote contains.
|
|
106
|
+
|
|
74
107
|
Rules for QUOTE:
|
|
75
108
|
- QUOTE must be copied verbatim from the provided source TEXT (not paraphrased).
|
|
76
|
-
-
|
|
77
|
-
|
|
109
|
+
- Prefer the shortest quote that still covers its part of the claim
|
|
110
|
+
(typically ${quoteLengthRange[0]}-${quoteLengthRange[1]} characters).
|
|
111
|
+
- Copy one contiguous passage per line. Do NOT shorten a quote with "..." and do
|
|
112
|
+
NOT join separate passages into one quote; write each passage on its own line.
|
|
78
113
|
- QUOTE must be at least ${minQuoteLength} characters unless it contains a numeric result or is
|
|
79
114
|
a complete standalone sentence that uniquely supports the claim.
|
|
80
115
|
- QUOTE must be a single line: escape newlines as \\n and double quotes as \\".
|
|
@@ -92,8 +127,6 @@ Rules for QUOTE:
|
|
|
92
127
|
claim; when only conclusions are visible, use cautious language.
|
|
93
128
|
|
|
94
129
|
IMPORTANT:
|
|
95
|
-
|
|
96
|
-
- The appendix is required only when you used citations. If you used no
|
|
97
|
-
citations, do NOT output EVI1/END_EVI1.
|
|
130
|
+
${important}
|
|
98
131
|
`.trim();
|
|
99
132
|
}
|
package/src/report.ts
CHANGED
|
@@ -40,39 +40,54 @@ export async function verifyAnswer(options: VerifyOptions): Promise<Verification
|
|
|
40
40
|
const warnings = [...parsed.warnings];
|
|
41
41
|
const claimById = new Map(parsed.claims.map((c) => [c.id, c]));
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// One citation per (claim, source) pair; a pair may carry several quotes.
|
|
44
|
+
const pairs = new Map<string, { claimId: string; sourceIndex: number; quotes: string[] }>();
|
|
44
45
|
for (const item of parsed.evidence) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
46
|
+
const key = `${item.claimId}|${item.sourceIndex}`;
|
|
47
|
+
const pair = pairs.get(key) ?? { claimId: item.claimId, sourceIndex: item.sourceIndex, quotes: [] };
|
|
48
|
+
pair.quotes.push(item.quote);
|
|
49
|
+
pairs.set(key, pair);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const citations: CitationVerification[] = [];
|
|
53
|
+
for (const [key, pair] of pairs) {
|
|
54
|
+
const source = sources[pair.sourceIndex - 1];
|
|
47
55
|
if (!source) {
|
|
48
|
-
warnings.push(
|
|
49
|
-
`EVI1: evidence ${item.claimId}|${item.sourceIndex} references a source that was not provided.`,
|
|
50
|
-
);
|
|
56
|
+
warnings.push(`EVI1: evidence ${key} references a source that was not provided.`);
|
|
51
57
|
continue;
|
|
52
58
|
}
|
|
53
|
-
const textMatch:
|
|
59
|
+
const parts = pair.quotes.map((quote) => ({ quote, textMatch: matchQuoteAgainstSource(quote, source, match) }));
|
|
60
|
+
const textMatch = combineMatches(parts.map((p) => p.textMatch));
|
|
61
|
+
for (const cue of textMatch.omittedCues ?? []) {
|
|
62
|
+
warnings.push(`Citation ${key}: the ellipsis in the quote leaves out "${cue}".`);
|
|
63
|
+
}
|
|
54
64
|
citations.push({
|
|
55
|
-
claimId:
|
|
56
|
-
sourceIndex:
|
|
57
|
-
claimText:
|
|
58
|
-
quote:
|
|
65
|
+
claimId: pair.claimId,
|
|
66
|
+
sourceIndex: pair.sourceIndex,
|
|
67
|
+
claimText: claimById.get(pair.claimId)?.text ?? '',
|
|
68
|
+
quote: pair.quotes.join(' […] '),
|
|
59
69
|
textMatch,
|
|
70
|
+
...(parts.length > 1 ? { parts } : {}),
|
|
60
71
|
score: textMatch.score,
|
|
61
72
|
});
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
if (judge) {
|
|
65
76
|
const judgeable = citations.filter((c) => c.claimText && c.quote);
|
|
66
|
-
const inputs: EntailmentInput[] = judgeable.map((c) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
const inputs: EntailmentInput[] = judgeable.map((c) => {
|
|
78
|
+
const source = sources[c.sourceIndex - 1];
|
|
79
|
+
const parts = c.parts ?? [{ quote: c.quote, textMatch: c.textMatch }];
|
|
80
|
+
return {
|
|
81
|
+
id: `${c.claimId}|${c.sourceIndex}`,
|
|
82
|
+
claim: c.claimText,
|
|
83
|
+
// An ellipsis must not hide what it left out (a "not", a qualifier):
|
|
84
|
+
// the judge rates the passage as the source has it.
|
|
85
|
+
quote: parts
|
|
86
|
+
.map((p) => (p.textMatch.method === 'elided' ? matchedText(source, p.textMatch) ?? p.quote : p.quote))
|
|
87
|
+
.join(' […] '),
|
|
88
|
+
context: parts.map((p) => contextWindow(source, p.textMatch, contextWindowChars)).join(' […] '),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
76
91
|
const results = await judge.judge(inputs, { signal });
|
|
77
92
|
if (results.length !== inputs.length) {
|
|
78
93
|
warnings.push(
|
|
@@ -109,6 +124,23 @@ export async function verifyAnswer(options: VerifyOptions): Promise<Verification
|
|
|
109
124
|
};
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
/** Several quotes for one pair count as their weakest match, carrying every cue. */
|
|
128
|
+
function combineMatches(matches: QuoteMatch[]): QuoteMatch {
|
|
129
|
+
if (matches.length === 1) return matches[0];
|
|
130
|
+
const weakest = matches.reduce((a, b) => (b.score < a.score ? b : a));
|
|
131
|
+
const cues = [...new Set(matches.flatMap((m) => m.omittedCues ?? []))];
|
|
132
|
+
const { omittedCues: _, ...rest } = weakest;
|
|
133
|
+
return cues.length ? { ...rest, omittedCues: cues } : rest;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The matched region of the source field, if its offsets are known. */
|
|
137
|
+
function matchedText(source: SourceDocument | undefined, m: QuoteMatch): string | undefined {
|
|
138
|
+
if (!source || m.start === undefined || m.end === undefined) return undefined;
|
|
139
|
+
const extra = /^extraTexts\[(\d+)\]$/.exec(m.field);
|
|
140
|
+
const text = extra ? source.extraTexts?.[Number(extra[1])] : source.text;
|
|
141
|
+
return text?.slice(m.start, m.end);
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
/** Source text around the matched region, for judge disambiguation. */
|
|
113
145
|
function contextWindow(
|
|
114
146
|
source: SourceDocument | undefined,
|
package/src/types.ts
CHANGED
|
@@ -57,6 +57,13 @@ export type MatchMethod =
|
|
|
57
57
|
| 'exact'
|
|
58
58
|
/** Substring after Unicode/typography normalization and case folding. */
|
|
59
59
|
| 'normalized'
|
|
60
|
+
/**
|
|
61
|
+
* The quote omits text, marked with an ellipsis ("…", "...", "[…]"): every
|
|
62
|
+
* fragment occurs literally, in order and close together. `start`/`end`
|
|
63
|
+
* cover the whole passage, omitted text included, and that passage is what
|
|
64
|
+
* the judge sees.
|
|
65
|
+
*/
|
|
66
|
+
| 'elided'
|
|
60
67
|
/** Best fuzzy window by character-trigram Dice similarity. */
|
|
61
68
|
| 'fuzzy'
|
|
62
69
|
/** No window reached the fuzzy score threshold. */
|
|
@@ -66,8 +73,9 @@ export type MatchMethod =
|
|
|
66
73
|
export interface QuoteMatch {
|
|
67
74
|
method: MatchMethod;
|
|
68
75
|
/**
|
|
69
|
-
* Similarity in [0, 1]. `1` only for exact/normalized hits;
|
|
70
|
-
* are capped at 0.99 so a perfect score always implies a
|
|
76
|
+
* Similarity in [0, 1]. `1` only for exact/normalized hits; elided and
|
|
77
|
+
* fuzzy scores are capped at 0.99 so a perfect score always implies a
|
|
78
|
+
* literal hit.
|
|
71
79
|
*/
|
|
72
80
|
score: number;
|
|
73
81
|
/** Start offset of the matched region in the original source field, if known. */
|
|
@@ -76,6 +84,12 @@ export interface QuoteMatch {
|
|
|
76
84
|
end?: number;
|
|
77
85
|
/** Which source field matched: `"text"` or `"extraTexts[i]"`. */
|
|
78
86
|
field: string;
|
|
87
|
+
/**
|
|
88
|
+
* `elided` only: words in the omitted text that can reverse or limit a
|
|
89
|
+
* statement ("not", "except", "only", "however", …). A hint, not a verdict:
|
|
90
|
+
* the judge sees the full passage and decides.
|
|
91
|
+
*/
|
|
92
|
+
omittedCues?: string[];
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
/** Qualitative entailment classes produced by the LLM judge. */
|
|
@@ -122,8 +136,12 @@ export interface CitationVerification {
|
|
|
122
136
|
claimId: string;
|
|
123
137
|
sourceIndex: number;
|
|
124
138
|
claimText: string;
|
|
139
|
+
/** The quote; several quotes for the same (claim, source) pair are joined with " […] ". */
|
|
125
140
|
quote: string;
|
|
141
|
+
/** The match of the quote; with several quotes, the weakest one (cues of all of them). */
|
|
126
142
|
textMatch: QuoteMatch;
|
|
143
|
+
/** One entry per quote, present only when the pair has more than one. */
|
|
144
|
+
parts?: { quote: string; textMatch: QuoteMatch }[];
|
|
127
145
|
/** Absent when verification ran without a judge. */
|
|
128
146
|
entailment?: EntailmentResult;
|
|
129
147
|
/**
|