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 });
}