Spaces:
Running
Running
File size: 3,631 Bytes
7ee3316 | 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | /* Server backend — Swan running natively, reached through server.js's /api proxy.
*
* The proxy hop exists for one reason: the Anthropic API key. The natural
* language step needs credentials, and a key shipped to the browser is a key
* published. server.js forwards to tools/swan_sidecar.py on loopback, and the
* key never leaves the machine running the sidecar.
*/
const BASE = '/api/swan';
async function call(path, { method = 'GET', body } = {}) {
let res;
try {
res = await fetch(BASE + path, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
} catch (e) {
// fetch only rejects on a transport failure, which here almost always means
// server.js is not running at all — worth saying rather than "Failed to fetch".
throw new Error(`cannot reach the dev server (${String(e && e.message || e)})`);
}
const text = await res.text();
let payload = null;
try { payload = text ? JSON.parse(text) : null; } catch { /* non-JSON body handled below */ }
if (!res.ok) {
/* Only a JSON body is quoted. A non-JSON one means something other than the
* sidecar answered — most often a static host with no /api at all, whose
* 404 is a full HTML page. Pasting that into the error puts a document in a
* banner; the status code says the same thing in four characters. */
const err = new Error((payload && payload.error)
|| `no sidecar behind ${BASE} (HTTP ${res.status})`);
err.status = res.status;
// The NL path returns the rejected code and the translated failure alongside
// the message; carrying them lets the UI show what was tried.
if (payload && payload.code) err.code = payload.code;
if (payload && payload.detail) err.detail = payload.detail;
throw err;
}
if (payload === null) throw new Error('the sidecar returned a non-JSON response');
return payload;
}
export function createServerEngine() {
return {
name: 'server',
async ready() {
const health = await call('/health');
if (!health.ok) throw new Error('sidecar reported unhealthy');
return {
swan: health.swan,
duckdb: health.duckdb,
model: health.model,
nl: health.nl,
anthropicModel: health.anthropicModel,
// Rules the sidecar could not evaluate, and therefore skipped. A model
// serving 112 of its 117 rule statements is a materially different thing
// to answer questions against than one serving all of them, so this is
// shown rather than logged — an answer that silently omits a derived
// property is worse than a warning.
ruleFailures: health.ruleFailures || [],
// Surfaced in the HUD: with this backend, questions leave the browser.
note: 'PyRel executes on the sidecar, not in the browser',
};
},
schema() { return call('/schema'); },
pyrel(code) { return call('/pyrel', { method: 'POST', body: { code } }); },
ask(question) { return call('/nl', { method: 'POST', body: { question } }); },
/**
* Like ask(), with the conversation so far.
*
* `history` is [{question, code}] and is supplied by the caller rather than
* held on the sidecar. Keeping it client-side means two tabs cannot capture
* each other's context, restarting the sidecar does not silently lose the
* thread, and "ask again from here" is just a shorter list.
*/
chat(question, history = []) {
return call('/chat', { method: 'POST', body: { question, history } });
},
};
}
|