Spaces:
Running
Running
| /* 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 } }); | |
| }, | |
| }; | |
| } | |