Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ requests and the same answers, with no key needed from the caller.
- **Link calls.** Every answer comes back with an event id in `events`. When a later request follows from one of
those answers, send its event id in a `Jeview-Trigger` header, and the map grows that request off the answer.
Jeview drops its own headers before calling Jev and sends the body on unchanged.
- **Show names, not keys.** The map labels each answer with its option's key, such as `c14`. When the criteria behind
the keys are objects, a `Jeview-Display` header says which part to show instead: `Jeview-Display: name`, or a field
per question, `Jeview-Display: category=name, kind=title`. An option without that field keeps its key, and so does
everything sent without the header. The header is only for show: one that cannot be read is ignored, never refused.

Agents can read `http://127.0.0.1:4777/llms.txt`: a running Jeview serves it with its own address and whether a key is
set. [llms.txt](llms.txt) here is the same text, for the default address.
Expand Down
7 changes: 7 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ When a later request follows from one of those answers, send its event id in a h
The viewer then grows that request's questions as a branch off the answer that triggered them. Jeview drops its own
headers before calling Jev, and sends the body on exactly as it came.

## Show an option by its name, not its key

The viewer labels each answer with the option's key, such as "c14". When the criteria behind the keys are objects, a
header says which part to show instead: Jeview-Display: name, or a field per question: Jeview-Display: category=name,
kind=title. A field may be a path, such as meta.title. An option without that field keeps its key, and a header that
cannot be read is ignored: it never stops a call.

## When a call is refused

Whatever Jev answers, a refusal included, comes back as Jev sent it. Jeview's own refusals are JSON, { "error": "..." }:
Expand Down
30 changes: 27 additions & 3 deletions src/jeview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const OPTIONS_LISTED = 60;
/** One recorded Jev call, as listed. `key` is the sha256 of the exact body sent to Jev: the same request always has
* the same key, so a client that caches Jev answers by that hash can find the call here. */
export type JeviewSummary = {
id: number; at: string; label: string; trigger: string | null;
id: number; at: string; label: string; trigger: string | null; display?: Record<string, string>;
key: string; stateKey: string | null; status: number; elapsedMs: number; bytes: number;
model: string | null; answeredBy: string | null; inputTokens: number | null; cost: number | null; questions: AnswerSummary[]; error?: string;
};
Expand All @@ -63,13 +63,29 @@ export function requestTrigger(value: string | null): string | null {
return value.trim();
}

/** Which part of an option's criteria the viewer shows for it, from a Jeview-Display header: one field for every question
* ("name"), or a field per question ("category=name, kind=title"); a field may be a path ("meta.title"). "*" holds the
* field for every question. Without the header an option is shown by its key. The header is only for show, so one that
* cannot be read is dropped: it is never a reason to refuse a call. */
const DISPLAY_FIELD = /^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/;
export function requestDisplay(value: string | null): Record<string, string> | null {
if (!value || value.length > 500) return null;
const display: Record<string, string> = {};
for (const part of value.split(",")) {
const [left = "", right] = part.split("=").map((word) => word.trim());
if (right === undefined) { if (DISPLAY_FIELD.test(left)) display["*"] = left; }
else if (left && left.length <= 100 && DISPLAY_FIELD.test(right)) display[left] = right;
}
return Object.keys(display).length ? display : null;
}

/** The question's own sentence: its instructions, or their `question` field when they carry context beside it. */
export function asks(instructions: unknown): string {
const text = typeof instructions === "string" ? instructions : object(instructions) && typeof instructions.question === "string" ? instructions.question : JSON.stringify(instructions) ?? "";
return text.length > 300 ? text.slice(0, 297) + "..." : text;
}

export function summarize(call: { id: number; at: string; label: string; trigger: string | null; status: number; elapsedMs: number; error?: string }, body: Buffer, request: unknown, response: unknown): JeviewSummary {
export function summarize(call: { id: number; at: string; label: string; trigger: string | null; display?: Record<string, string>; status: number; elapsedMs: number; error?: string }, body: Buffer, request: unknown, response: unknown): JeviewSummary {
const questions = object(request) && object(request.questions) ? request.questions : {};
const answers = object(response) && object(response.answers) ? response.answers : {};
const usage = object(response) && object(response.usage) ? response.usage : {};
Expand Down Expand Up @@ -224,6 +240,13 @@ When a later request follows from one of those answers, send its event id in a h
The viewer then grows that request's questions as a branch off the answer that triggered them. Jeview drops its own
headers before calling Jev, and sends the body on exactly as it came.

## Show an option by its name, not its key

The viewer labels each answer with the option's key, such as "c14". When the criteria behind the keys are objects, a
header says which part to show instead: Jeview-Display: name, or a field per question: Jeview-Display: category=name,
kind=title. A field may be a path, such as meta.title. An option without that field keeps its key, and a header that
cannot be read is ignored: it never stops a call.

## When a call is refused

Whatever Jev answers, a refusal included, comes back as Jev sent it. Jeview's own refusals are JSON, { "error": "..." }:
Expand Down Expand Up @@ -269,11 +292,12 @@ export function createJeview(options: JeviewOptions): Jeview {
const triggerHeader = req.headers[`${OWN_HEADER}trigger`];
let trigger: string | null;
try { trigger = requestTrigger(triggerHeader === undefined ? null : String(triggerHeader)); } catch (error) { return send(res, 400, { error: `jeview: ${(error as Error).message}` }); }
const displayHeader = req.headers[`${OWN_HEADER}display`], display = requestDisplay(displayHeader === undefined ? null : String(displayHeader));
const sent = body, requestValue = parse(body.toString("utf8")); // sent on as it came
const id = store.allocate(), at = new Date().toISOString(), started = Date.now();
const keep = (status: number, text: string, error?: string) => {
const responseValue = text ? parse(text) : null;
store.save({ summary: summarize({ id, at, label, trigger, status, elapsedMs: Date.now() - started, ...(error ? { error } : {}) }, sent, requestValue, responseValue), request: requestValue, response: responseValue });
store.save({ summary: summarize({ id, at, label, trigger, ...(display ? { display } : {}), status, elapsedMs: Date.now() - started, ...(error ? { error } : {}) }, sent, requestValue, responseValue), request: requestValue, response: responseValue });
};
const fail = (status: number, message: string) => { keep(status, "", message); return send(res, status, { error: message }); };
const key = jevKey();
Expand Down
14 changes: 13 additions & 1 deletion test/jeview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";

import { createJeview, DATABASE, JEV_USD_PER_INPUT_TOKEN, llmsText, summarize, type JeviewSummary } from "../src/jeview.ts";
import { createJeview, DATABASE, JEV_USD_PER_INPUT_TOKEN, llmsText, requestDisplay, summarize, type JeviewSummary } from "../src/jeview.ts";

type Seen = { method: string; url: string; headers: IncomingMessage["headers"]; body: string };
type Hooks = { after(fn: () => unknown): void };
Expand Down Expand Up @@ -110,6 +110,18 @@ test("a Jeview-Trigger header names the answer a request follows from: recorded
assert.deepEqual([labelled.label, labelled.trigger], ["june", "1:kind"]);
});

test("a Jeview-Display header says which part of an option to show: kept with the call, never sent on, and never a reason to refuse one", async (t) => {
const upstream = await jev(t, () => ({ body: jevAnswer }));
const { base } = await proxy(t, upstream.url);
const ask = (display?: string) => fetch(`${base}/v1/systemone`, { method: "POST", headers: display === undefined ? {} : { "Jeview-Display": display }, body: jevBody });
for (const display of [undefined, "name", "kind=name, is_urgent=meta.title", " kind = label ,name ", "no spaces allowed in a field", "=name", "x".repeat(501), "kind=na me, name"]) assert.equal((await ask(display)).status, 200);
assert.deepEqual((await records(base)).records.map((r) => r.display ?? null), [null, { "*": "name" }, { kind: "name", is_urgent: "meta.title" }, { kind: "label", "*": "name" }, null, null, null, { "*": "name" }]);
// Jev never hears of it, and the body is the caller's
assert.deepEqual(upstream.seen.flatMap((seen) => Object.keys(seen.headers).filter((name) => name.startsWith("jeview-"))), []);
assert.deepEqual([...new Set(upstream.seen.map((seen) => seen.body))], [jevBody]);
assert.deepEqual([requestDisplay(null), requestDisplay(""), requestDisplay("a.b-c_d"), requestDisplay("a..b"), requestDisplay("q=a=b")], [null, null, { "*": "a.b-c_d" }, null, { q: "a" }]);
});

test("without a key a Jev request is refused and recorded; Jev's own refusals reach the caller as sent; nothing else is accepted", async (t) => {
const upstream = await jev(t, () => ({ status: 429, headers: { "content-type": "application/json", "retry-after": "2" }, body: JSON.stringify({ error: "slow down" }) }));
const { base, store } = await proxy(t, upstream.url, { key: null });
Expand Down
Loading
Loading