Jaspers Hub

jaspers / agents

pluginOfficialReviewed★ 0↓ 1

Research Agents: Lets users define research agents that answer chat questions from SEC filings, uploaded documents, and the web, with every figure cited.

6.6 KB
1// The calculate tool's arithmetic: lines of `name = expression`, each able to use the names before it,
2// worked out exactly as written so a model never does sums in its head. Numbers, + - * / and ^ (or
3// **), parentheses, and a few functions, npv and irr among them. Read by a small parser, never
4// evaluated as code. Pure, so it is tested alone.
5
6export interface CalcLine {
7 name: string
8 expression: string
9 value: number | null
10 error: string | null
11}
12
13export interface Calculation {
14 lines: CalcLine[]
15 /** What the model reads: each line as written and its value, or why it has none. */
16 text: string
17}
18
19const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
20
21export function calculate(source: string): Calculation {
22 const known = new Map<string, number>()
23 const lines: CalcLine[] = []
24 const written = source
25 .split(/[\n;]/)
26 .map((l) => l.trim())
27 .filter(Boolean)
28 written.forEach((raw, i) => {
29 const eq = raw.indexOf('=')
30 const named = eq > 0 && NAME.test(raw.slice(0, eq).trim())
31 const name = named ? raw.slice(0, eq).trim() : `line ${i + 1}`
32 const expression = named ? raw.slice(eq + 1).trim() : raw
33 try {
34 const value = evaluate(expression, known)
35 if (!Number.isFinite(value)) throw new Error(`${expression} is not a finite number.`)
36 known.set(name, value)
37 lines.push({ name, expression, value, error: null })
38 } catch (err) {
39 lines.push({ name, expression, value: null, error: err instanceof Error ? err.message : String(err) })
40 }
41 })
42 const text =
43 lines.length === 0
44 ? 'Nothing to calculate: write one expression a line, like gm = (rev - cost) / rev.'
45 : written.map((raw, i) => `${raw} → ${lines[i]!.error ?? format(lines[i]!.value!)}`).join('\n')
46 return { lines, text }
47}
48
49/** Ten significant digits: enough for any rounding a question asks, short enough to read. */
50function format(value: number): string {
51 return String(Number(value.toPrecision(10)))
52}
53
54type Token = { kind: 'num'; value: number } | { kind: 'name'; value: string } | { kind: 'op'; value: string }
55
56function tokens(expression: string): Token[] {
57 const out: Token[] = []
58 const re =
59 /\s*(?:(\d+(?:\.\d*)?(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?)|([A-Za-z_][A-Za-z0-9_]*)|(\*\*|[-+*/^(),]))/y
60 let at = 0
61 while (at < expression.length) {
62 if (/^\s*$/.test(expression.slice(at))) break
63 re.lastIndex = at
64 const m = re.exec(expression)
65 if (!m) throw new Error(`${expression.slice(at).trim()} is not something to calculate.`)
66 if (m[1] !== undefined) out.push({ kind: 'num', value: Number(m[1]) })
67 else if (m[2] !== undefined) out.push({ kind: 'name', value: m[2] })
68 else out.push({ kind: 'op', value: m[3] === '**' ? '^' : m[3]! })
69 at = re.lastIndex
70 }
71 return out
72}
73
74const FUNCTIONS: Record<string, (args: number[]) => number> = {
75 abs: one(Math.abs),
76 sqrt: one(Math.sqrt),
77 ln: one(Math.log),
78 log10: one(Math.log10),
79 exp: one(Math.exp),
80 round: (a) => {
81 if (a.length < 1 || a.length > 2) throw new Error('round takes a number and, if you like, its decimals.')
82 const f = 10 ** (a[1] ?? 0)
83 return Math.round(a[0]! * f) / f
84 },
85 min: many(Math.min),
86 max: many(Math.max),
87 sum: many((...a) => a.reduce((s, x) => s + x, 0)),
88 avg: many((...a) => a.reduce((s, x) => s + x, 0) / a.length),
89 npv: (a) => {
90 if (a.length < 2) throw new Error('npv takes a rate, then the cash flows from time zero.')
91 return npv(a[0]!, a.slice(1))
92 },
93 irr: (a) => irr(a),
94}
95
96function one(f: (x: number) => number): (args: number[]) => number {
97 return (a) => {
98 if (a.length !== 1) throw new Error('This function takes one number.')
99 return f(a[0]!)
100 }
101}
102
103function many(f: (...xs: number[]) => number): (args: number[]) => number {
104 return (a) => {
105 if (a.length === 0) throw new Error('This function takes at least one number.')
106 return f(...a)
107 }
108}
109
110function npv(rate: number, flows: number[]): number {
111 return flows.reduce((s, cf, t) => s + cf / (1 + rate) ** t, 0)
112}
113
114/** The rate between -99% and 10,000% at which the flows' npv is zero, found by halving. */
115function irr(flows: number[]): number {
116 if (flows.length < 2) throw new Error('irr takes the cash flows from time zero, at least two.')
117 let lo = -0.99
118 let hi = 100
119 if (Math.sign(npv(lo, flows)) === Math.sign(npv(hi, flows))) {
120 throw new Error('There is no rate that makes these cash flows worth zero: they need a change of sign.')
121 }
122 for (let i = 0; i < 200; i++) {
123 const mid = (lo + hi) / 2
124 if (Math.sign(npv(mid, flows)) === Math.sign(npv(lo, flows))) lo = mid
125 else hi = mid
126 }
127 return (lo + hi) / 2
128}
129
130/** Recursive descent: sum := product (± product)*; product := unary (*|/ unary)*; unary := -unary | power; power := atom (^ unary)? */
131function evaluate(expression: string, known: Map<string, number>): number {
132 const list = tokens(expression)
133 let i = 0
134 const peek = (): Token | undefined => list[i]
135 const isOp = (v: string): boolean => peek()?.kind === 'op' && peek()!.value === v
136 const expect = (v: string): void => {
137 if (!isOp(v)) throw new Error(`${expression}: expected "${v}".`)
138 i++
139 }
140 const sum = (): number => {
141 let v = product()
142 while (isOp('+') || isOp('-')) v = list[i++]!.value === '+' ? v + product() : v - product()
143 return v
144 }
145 const product = (): number => {
146 let v = unary()
147 while (isOp('*') || isOp('/')) v = list[i++]!.value === '*' ? v * unary() : v / unary()
148 return v
149 }
150 const unary = (): number => {
151 if (isOp('-')) {
152 i++
153 return -unary()
154 }
155 if (isOp('+')) {
156 i++
157 return unary()
158 }
159 return power()
160 }
161 const power = (): number => {
162 const base = atom()
163 if (!isOp('^')) return base
164 i++
165 return base ** unary()
166 }
167 const atom = (): number => {
168 const t = list[i++]
169 if (!t) throw new Error(`${expression}: expected a number.`)
170 if (t.kind === 'num') return t.value
171 if (t.kind === 'op' && t.value === '(') {
172 const v = sum()
173 expect(')')
174 return v
175 }
176 if (t.kind === 'name') {
177 if (isOp('(')) {
178 const f = FUNCTIONS[t.value.toLowerCase()]
179 if (!f) throw new Error(`${t.value} is not a function: the functions are ${Object.keys(FUNCTIONS).join(', ')}.`)
180 i++
181 const args: number[] = []
182 if (!isOp(')')) {
183 args.push(sum())
184 while (isOp(',')) {
185 i++
186 args.push(sum())
187 }
188 }
189 expect(')')
190 return f(args)
191 }
192 const v = known.get(t.value)
193 if (v === undefined) throw new Error(`${t.value} is not defined above.`)
194 return v
195 }
196 throw new Error(`${expression}: expected a number.`)
197 }
198 const v = sum()
199 if (i < list.length) throw new Error(`${expression}: did not expect "${String(list[i]!.value)}".`)
200 return v
201}