The Assistant pane talks to a model on this machine: any server with the chat-completions interface and tool calls (the JouleClaw runtime, a llama.cpp server). Its address goes under "Model…", and nothing leaves the machine unless that address points elsewhere. In the desktop app the app itself makes the call (`chat` and `models` commands, http and https only).
- The model's tools are the Studio's engine: read the program, lint, draw, plan and run. Its first turn must call one, so every answer starts from the engine.
- Every program it proposes is read by the engine here. One that does not parse goes back once with the engine's reason. One that parses is run with the settings the model ran it with, so the counts it quoted are the counts shown, and then offered with Apply. A program the model ran through a tool but did not show is offered the same way.
- A figure in its prose that no tool returned is marked "Not from a run". LaTeX is shown as Unicode, and answers are plain text with bold and code built as nodes, never HTML.
- test/studio.test.mjs drives it against a scripted model server: the tool call run on the engine, a broken proposal sent back with its reason, the corrected one checked and run, the tools and the grounding rule in every call, and Apply.
- test/assistant-live.mjs drives it against a real server. Against a local 35B mixture-of-experts model (about 3B parameters active per token), every answer began with an engine call. The GHZ program it wrote was checked and run, with the counts it quoted equal to the counts shown, and a "50/50" it gave itself was marked as not from a run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 82269e7f4d)
57 lines
3.7 KiB
JavaScript
57 lines
3.7 KiB
JavaScript
// The assistant against a real model server, as a reader uses it: not part of CI, which
|
|
// has no model. Prints what the model did and how long it took.
|
|
//
|
|
// node build.mjs --pkg PATH/TO/PKG && node test/assistant-live.mjs http://127.0.0.1:8080/v1
|
|
//
|
|
// Asks it to explain the Bell example, then to write a three-qubit GHZ program; each
|
|
// program it proposes is checked and run by the engine before it is shown, as always.
|
|
import { createServer } from 'node:http';
|
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
import { dirname, extname, join, normalize, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const endpoint = process.argv[2] ?? 'http://127.0.0.1:8080/v1';
|
|
const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default;
|
|
const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm' };
|
|
const server = createServer((req, res) => {
|
|
let file = normalize(join(DIST, decodeURIComponent(new URL(req.url, 'http://x').pathname)));
|
|
if (!file.startsWith(DIST)) { res.writeHead(403).end(); return; }
|
|
if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html');
|
|
if (!existsSync(file)) { res.writeHead(404).end(); return; }
|
|
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' });
|
|
res.end(readFileSync(file));
|
|
});
|
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
|
|
const page = await (await browser.newContext({ viewport: { width: 1400, height: 1000 } })).newPage();
|
|
await page.goto(`http://127.0.0.1:${server.address().port}/`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: 120_000 });
|
|
await page.selectOption('#qs-example', 'bell');
|
|
await page.click('#qs-ai-gear');
|
|
await page.fill('#qs-ai-endpoint', endpoint);
|
|
await page.click('#qs-ai-connect');
|
|
await page.waitForFunction(() => !/connecting/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: 60_000 });
|
|
console.log('model:', await page.textContent('#qs-ai-state'));
|
|
|
|
async function ask(q) {
|
|
const before = await page.$$eval('.qs-ai-answer, .qs-ai-error', (a) => a.length);
|
|
const t0 = Date.now();
|
|
await page.fill('#qs-ai-input', q);
|
|
await page.click('#qs-ai-send');
|
|
await page.waitForFunction((n) => document.querySelectorAll('.qs-ai-answer, .qs-ai-error').length > n, before, { timeout: 600_000 });
|
|
const items = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => `[${l.className}] ${l.textContent.replace(/\s+/g, ' ').slice(0, 900)}`));
|
|
console.log(`\n=== ${q} (${((Date.now() - t0) / 1000).toFixed(1)} s)`);
|
|
for (const i of items.slice(-8)) console.log(i);
|
|
}
|
|
await ask('Explain this circuit.');
|
|
await ask('Write a three-qubit GHZ program, measured, and tell me its counts.');
|
|
const proposals = await page.$$eval('.qs-ai-proposal', (p) => p.map((x) => x.textContent));
|
|
console.log('\nproposals checked:', proposals.length, proposals.join(' | '));
|
|
const answers = await page.$$eval('.qs-ai-answer', (a) => a.map((x) => x.innerText));
|
|
for (const a of answers) console.log('\n--- answer as shown ---\n' + a);
|
|
const { mkdirSync } = await import('node:fs');
|
|
mkdirSync(join(dirname(fileURLToPath(import.meta.url)), 'screenshots'), { recursive: true });
|
|
await page.locator('.qs-ai').screenshot({ path: join(dirname(fileURLToPath(import.meta.url)), 'screenshots', 'assistant-live.png') });
|
|
await browser.close();
|
|
server.close();
|