// SPDX-License-Identifier: MIT // The bankML console's engine for the input field: the console's own same-origin POST /api/ask, which composes bankML's // persona and a SELF block measured at that moment, streams the answer as NDJSON ({"piece"} … then {"done", "receipt", // "answer_sha256_ok", "error"}) and logs the exchange. The receipt's response_sha256 is checked again here, in the // browser, over the text this page received. export type Turn = { role: 'user' | 'assistant'; content: string }; export interface Verdict { ok: boolean; line: string; receipt: Record | null } async function sha256hex(text: string): Promise { const h = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)); return [...new Uint8Array(h)].map(b => b.toString(16).padStart(2, '0')).join(''); } /** `window` is the output field's title: the console keeps a .memory per window under that name (titles survive a reload). */ export async function* askConsole(history: Turn[], message: string, done: (v: Verdict) => void, maxTokens = 256, window?: string): AsyncGenerator { const r = await fetch('/api/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, // up to 40 messages: the console trims them four exchanges at a time (console.py `window`), so the engine's cached // prefix holds from turn to turn; a window cut here would slide on every turn and defeat it body: JSON.stringify({ message, history: history.slice(-40), max_tokens: maxTokens, ...(window ? { window } : {}) }), }); if (!r.ok || !r.body) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`); const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = '', text = ''; let end: { receipt?: Record | null; error?: string | null; metrics?: Record } = {}; for (;;) { const { done: fin, value } = await reader.read(); if (fin) break; buf += dec.decode(value, { stream: true }); let i: number; while ((i = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1); if (!line) continue; const d = JSON.parse(line); if (d.piece) { text += d.piece; yield d.piece; } if (d.done) end = d; } } if (end.error) throw new Error(String(end.error)); const rc = end.receipt || null; const ok = !!rc && (await sha256hex(text)) === rc.response_sha256; const m = end.metrics || {}; const num = (v: unknown, d: number, unit: string) => (typeof v === 'number' ? v.toFixed(d) + unit : 'not measured'); done({ ok, receipt: rc, line: rc ? `${ok ? '✓' : '✗'} receipt · bankML ${rc.bankml} · model sha256 ${String(rc.model_sha256 || '').slice(0, 12)}… · answer sha256 ${ok ? 'matches the text received' : 'does NOT match'} · TTFT ${num(m.ttft_ms, 0, ' ms')} · ${num(m.eval_tps, 2, ' tok/s')}` : '✗ no receipt came with this answer', }); history.push({ role: 'user', content: message }, { role: 'assistant', content: text }); }