Jaspers Hub

jaspers / agents

pluginOfficialReviewed★ 0↓ 1

Research Agents: Agents you define that answer research chats from SEC filings, your documents, and the web, citing every passage.

3.2 KB
1// What an answer's citations owe: every sentence that states something cites the passage it rests on,
2// and every ref it cites is one a search gave. Pure, so the reading of an answer is tested.
3// Headings, table rows, code, and short fragments state nothing to cite; a sentence saying the passages
4// do not hold something is what the prompt asks for when they do not, and cites nothing.
5
6const CITE = /\[(p\d+)\]/g
7const SAYS_NOT_FOUND =
8 /\b(no passage|not (?:find|found)|couldn['’]?t find|could not find|did not find|didn['’]?t find|do(?:es)? not (?:say|hold|mention|contain)|not in the (?:passages|filings|store))\b/i
9const CLAIM_WORDS = 6
10
11/** The sentences of an answer that state something and cite nothing. */
12export function uncited(answer: string): string[] {
13 const out: string[] = []
14 let fenced = false
15 for (const raw of answer.split('\n')) {
16 const line = raw.trim()
17 if (line.startsWith('```')) {
18 fenced = !fenced
19 continue
20 }
21 if (fenced || !line || line.startsWith('#') || line.startsWith('|') || line.startsWith('>')) continue
22 const body = line.replace(/^(?:[-*+]|\d+[.)])\s+/, '')
23 for (const sentence of body.split(/(?<=[.!?])\s+(?=["“(*\p{Lu}\d])/u)) {
24 const plain = sentence.replace(/\*\*?|__?|`/g, '').trim()
25 if (CITE.test(sentence) || SAYS_NOT_FOUND.test(plain)) {
26 CITE.lastIndex = 0
27 continue
28 }
29 CITE.lastIndex = 0
30 const words = plain.split(/\s+/).filter(Boolean).length
31 if (words >= CLAIM_WORDS || (/\d/.test(plain) && words >= 3)) out.push(plain)
32 }
33 }
34 return out
35}
36
37/** The refs an answer cites that no search gave. */
38export function unknownRefs(answer: string, known: Set<string>): string[] {
39 const out = new Set<string>()
40 for (const match of answer.matchAll(CITE)) if (!known.has(match[1]!)) out.add(match[1]!)
41 return [...out]
42}
43
44/** What the model is told when its answer owes citations. */
45export function repairText(missing: string[], unknown: string[], refs: string[]): string {
46 const lines = [
47 'Every sentence that states a fact must cite the passage it rests on, like [p2]. This answer does not yet:',
48 ...missing.map((s) => `- Not cited: "${s}"`),
49 ...unknown.map((r) => `- [${r}] is not a passage you were given.`),
50 `Write the whole answer again. Cite each of these from the passages you have (${refs.join(', ') || 'none yet'}), search for what they rest on, or leave them out.`,
51 'Reply with the answer alone, as the user will read it: say nothing about this rewrite, what you changed, or what you left out.',
52 ]
53 return lines.join('\n')
54}
55
56/**
57 * An answer as it is copied: its words, then the passages it cites in the order it first cites them,
58 * each with where it is from and its address, so a ref still says something once pasted elsewhere.
59 */
60export function withSources(answer: string, passages: { ref: string; label: string; url: string | null }[]): string {
61 const byRef = new Map(passages.map((p) => [p.ref, p]))
62 const cited = [...new Set([...answer.matchAll(CITE)].map((m) => m[1]!))].flatMap((ref) => byRef.get(ref) ?? [])
63 if (cited.length === 0) return answer.trim()
64 const lines = cited.map((p) => `[${p.ref}] ${p.label}${p.url ? ` ${p.url}` : ''}`)
65 return `${answer.trim()}\n\nSources\n${lines.join('\n')}`
66}