Research Agents: Lets you define research agents that chat and answer with citations from SEC filings, your documents, and the web, via Jaspers' research-store service.
| 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 | |
| 6 | export interface CalcLine { |
| 7 | name: string |
| 8 | expression: string |
| 9 | value: number | null |
| 10 | error: string | null |
| 11 | } |
| 12 | |
| 13 | export 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 | |
| 19 | const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/ |
| 20 | |
| 21 | export 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. */ |
| 50 | function format(value: number): string { |
| 51 | return String(Number(value.toPrecision(10))) |
| 52 | } |
| 53 | |
| 54 | type Token = { kind: 'num'; value: number } | { kind: 'name'; value: string } | { kind: 'op'; value: string } |
| 55 | |
| 56 | function 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 | |
| 74 | const 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 | |
| 96 | function 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 | |
| 103 | function 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 | |
| 110 | function 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. */ |
| 115 | function 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)? */ |
| 131 | function 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 | } |