Spaces:
Running
Running
File size: 3,110 Bytes
02a65e1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | // 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<string, unknown> | null }
async function sha256hex(text: string): Promise<string> {
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<string> {
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<string, unknown> | null; error?: string | null; metrics?: Record<string, unknown> } = {};
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 });
}
|