@konneal/engine 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.ts +1 -0
- package/dist/ask-47RNGK2R.js +12 -0
- package/dist/chunk-3OXSQH7Y.js +1852 -0
- package/dist/chunk-6GOSMLRH.js +2781 -0
- package/dist/{chunk-EHJEELVB.js → chunk-LNSDBEKS.js} +1 -1
- package/dist/{chunk-35ODH64W.js → chunk-Q327B27J.js} +33 -0
- package/dist/{chunk-OCNLV7Q7.js → chunk-Q6LI4T7M.js} +6 -1
- package/dist/{chunk-ROF3Q7UC.js → chunk-SN3ANQ3Y.js} +2 -2
- package/dist/chunk-VJZLVU3S.js +64 -0
- package/dist/{chunk-CAEHIVG5.js → chunk-WGXATDXY.js} +1 -1
- package/dist/codecs.d.ts +3 -3
- package/dist/completion.d.ts +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/faithfulness.d.ts +1 -0
- package/dist/mcp-proto.d.ts +25 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/openapi-surface.gen.d.ts +9 -0
- package/dist/openapi-types.d.ts +2141 -0
- package/dist/profile.gen.d.ts +1 -0
- package/dist/prompts/system.md +1 -0
- package/dist/quota.d.ts +4 -1
- package/dist/search-OMPBMZT4.js +11 -0
- package/dist/tablecontext.d.ts +7 -0
- package/dist/verdict-parse.d.ts +5 -0
- package/dist/worker_mcp/src/index.js +3 -3
- package/dist/worker_public/src/config.js +4 -2
- package/dist/worker_public/src/index.js +1193 -4932
- package/dist/worker_public/src/profile.js +1 -1
- package/dist/worker_public/src/refusal.js +2 -2
- package/dist/worker_public/src/requestScope.js +3 -3
- package/docs/spec-api.md +27 -13
- package/package.json +12 -3
- package/profile/prompts.yaml +3 -0
- package/workers/shared/router.ts +23 -16
- package/workers/worker_public/migrations/0014_usage_cache.sql +5 -0
- package/workers/worker_public/openapi.yaml +1169 -0
- package/workers/worker_public/prompts/system.md +1 -0
- package/workers/worker_public/schema.sql +3 -1
- package/workers/worker_public/src/admin.ts +51 -7
- package/workers/worker_public/src/ai.ts +10 -2
- package/workers/worker_public/src/ask.ts +94 -22
- package/workers/worker_public/src/codecs.ts +35 -10
- package/workers/worker_public/src/completion.ts +24 -1
- package/workers/worker_public/src/config.ts +10 -0
- package/workers/worker_public/src/faithfulness.ts +10 -17
- package/workers/worker_public/src/grader.ts +2 -2
- package/workers/worker_public/src/index.ts +106 -58
- package/workers/worker_public/src/lib/router.ts +1 -1
- package/workers/worker_public/src/mcp-proto.ts +71 -0
- package/workers/worker_public/src/mcp.ts +47 -0
- package/workers/worker_public/src/openapi-surface.gen.ts +318 -0
- package/workers/worker_public/src/pipeline.ts +4 -2
- package/workers/worker_public/src/profile.gen.ts +1 -0
- package/workers/worker_public/src/projects.ts +4 -2
- package/workers/worker_public/src/quota.ts +4 -2
- package/workers/worker_public/src/research.ts +55 -3
- package/workers/worker_public/src/tablecontext.ts +13 -2
- package/workers/worker_public/src/verdict-parse.ts +60 -0
|
@@ -4,7 +4,7 @@ import { LIMITS, MODELS, DATASETS, THRESHOLDS, processExpansion } from "./config
|
|
|
4
4
|
import systemPromptText from "../prompts/system.md";
|
|
5
5
|
import conversationalPromptText from "../prompts/conversational.md";
|
|
6
6
|
import listwisePromptText from "../prompts/listwise.md";
|
|
7
|
-
import {
|
|
7
|
+
import { tableSelection } from "./tablecontext";
|
|
8
8
|
|
|
9
9
|
// the pinned refusal sentence lives with the canonicalizer in ./refusal
|
|
10
10
|
// (refusals are never cached: a refusal says "retrieval found nothing",
|
|
@@ -337,7 +337,9 @@ export function buildMessages(
|
|
|
337
337
|
const head = `[${usedHits.length + 1}] ${label}${unitTag} ${h.metadata.clause_title ? "— " + h.metadata.clause_title : ""}\n`;
|
|
338
338
|
// tables: schema-aware pruning from the producer payload; the
|
|
339
339
|
// stored text is the fallback (pruning never goes below baseline)
|
|
340
|
-
const
|
|
340
|
+
const tableSel = (h.metadata as any).block === "table" ? tableSelection(h.metadata, query) : null;
|
|
341
|
+
const pruned = tableSel?.text ?? null;
|
|
342
|
+
if (tableSel) (h.metadata as any).table_selection = { cols: tableSel.cols, rowsShown: tableSel.rowsShown, rowsTotal: tableSel.rowsTotal };
|
|
341
343
|
const body = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
|
|
342
344
|
const t = estTokens(head) + estTokens(body);
|
|
343
345
|
if (t <= remain) {
|
|
@@ -104,6 +104,7 @@ export const PROFILE = {
|
|
|
104
104
|
},
|
|
105
105
|
"prompts": {
|
|
106
106
|
"vars": {
|
|
107
|
+
"publisher_identity": "the Fixture Organization — a worldwide organization that publishes the fixture corpus",
|
|
107
108
|
"assistant_identity": "the fixture assistant — a public service answering questions about the fixture publisher's documents",
|
|
108
109
|
"refusal_sentence": "I don't have information on this in the indexed fixture documents.",
|
|
109
110
|
"account_note_source": "the user's own fixture account",
|
|
@@ -30,8 +30,10 @@ export async function handleProjects(env: any, sub: string, req: Request, route:
|
|
|
30
30
|
}
|
|
31
31
|
if (method === "POST") {
|
|
32
32
|
const body: any = await req.json().catch(() => null);
|
|
33
|
-
if (typeof body
|
|
34
|
-
// move a conversation in/out of a project (membership-as-move)
|
|
33
|
+
if (body && (typeof body.project_id === "string" || body.project_id === null)) {
|
|
34
|
+
// move a conversation in/out of a project (membership-as-move) —
|
|
35
|
+
// null UNFILES (the 2026-09-16 bug: the string-only guard let the
|
|
36
|
+
// null case fall through to create-project and 400)
|
|
35
37
|
const convId = String(body.conversation_id ?? "");
|
|
36
38
|
if (!/^[a-zA-Z0-9_-]{8,64}$/.test(convId)) return err(400, "invalid_input", "bad conversation id");
|
|
37
39
|
const target = body.project_id === null ? null : String(body.project_id);
|
|
@@ -39,13 +39,15 @@ export function telemetry(
|
|
|
39
39
|
answerChars: number,
|
|
40
40
|
queryHash: string,
|
|
41
41
|
lang?: string,
|
|
42
|
+
cache?: "exact" | "semantic",
|
|
43
|
+
meta?: { durationMs?: number; keyId?: string | null },
|
|
42
44
|
) {
|
|
43
45
|
const day = today();
|
|
44
46
|
ctx.waitUntil(
|
|
45
47
|
env.DB.batch([
|
|
46
48
|
env.DB.prepare(
|
|
47
|
-
"INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
|
|
48
|
-
).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null),
|
|
49
|
+
"INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang, cache, duration_ms, key_id) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
|
|
50
|
+
).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null, cache ?? null, meta?.durationMs ?? null, meta?.keyId ?? null),
|
|
49
51
|
env.DB.prepare(
|
|
50
52
|
"INSERT INTO spend (day, tier, model, requests) VALUES (?1,?2,?3,1) ON CONFLICT(day, tier, model) DO UPDATE SET requests = requests + 1",
|
|
51
53
|
).bind(day, tier, model ?? "none"),
|
|
@@ -31,6 +31,47 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
|
|
|
31
31
|
if (!q) return err(400, "invalid_input", `query is required (1-${LIMITS.maxInputChars} chars)`);
|
|
32
32
|
const maxIters = Math.min(Math.max(Number(body?.max_iterations) || 3, 1), 3);
|
|
33
33
|
|
|
34
|
+
// the passes, streamed when the caller asks: each retrieval and each
|
|
35
|
+
// sufficiency judgement is an event, so the reader watches the loop
|
|
36
|
+
// work instead of watching a spinner — the final event carries the
|
|
37
|
+
// same result object the JSON path returns
|
|
38
|
+
if (body?.stream === true) {
|
|
39
|
+
const enc = new TextEncoder();
|
|
40
|
+
const started = Date.now();
|
|
41
|
+
const stream = new ReadableStream({
|
|
42
|
+
async start(ctrl) {
|
|
43
|
+
const send = (e: unknown) => ctrl.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
44
|
+
try {
|
|
45
|
+
const out = await run(env, ctx, q, maxIters, send);
|
|
46
|
+
send({ type: "done", ...out });
|
|
47
|
+
} catch (e: any) {
|
|
48
|
+
send({ type: "error", message: String(e?.message ?? e).slice(0, 200) });
|
|
49
|
+
} finally {
|
|
50
|
+
ctrl.close();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return new Response(stream, {
|
|
55
|
+
headers: {
|
|
56
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
57
|
+
"cache-control": "no-cache",
|
|
58
|
+
"x-accel-buffering": "no",
|
|
59
|
+
...corsHeaders(req),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
void started;
|
|
63
|
+
}
|
|
64
|
+
const out = await run(env, ctx, q, maxIters);
|
|
65
|
+
return json({ ...out, ...corsHeaders(req) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function run(
|
|
69
|
+
env: Env,
|
|
70
|
+
ctx: ExecutionContext,
|
|
71
|
+
q: { query: string; lang?: string },
|
|
72
|
+
maxIters: number,
|
|
73
|
+
emit?: (e: unknown) => void,
|
|
74
|
+
): Promise<{ answer: string; citations: unknown[]; model: string; query_hash: string; research: { iterations: number; passages: number; elapsed_ms: number; sufficient: boolean | null } }> {
|
|
34
75
|
const started = Date.now();
|
|
35
76
|
const queryHash = await sha256Hex(q.query);
|
|
36
77
|
const understanding = await understandQuery(portModelRunner(env), MODELS.understand, q.query, [], []);
|
|
@@ -44,6 +85,7 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
|
|
|
44
85
|
|
|
45
86
|
for (let i = 0; i < maxIters; i++) {
|
|
46
87
|
iterations = i + 1;
|
|
88
|
+
emit?.({ type: "pass", n: iterations, of: maxIters, phase: "retrieving" });
|
|
47
89
|
let retrieved: { hits: Hit[] };
|
|
48
90
|
try {
|
|
49
91
|
retrieved = await retrieve(env, q.query, {
|
|
@@ -95,6 +137,15 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
|
|
|
95
137
|
}
|
|
96
138
|
})();
|
|
97
139
|
console.log("research iter", iterations, "passages", passages.length, "sufficient:", judge?.sufficient);
|
|
140
|
+
emit?.({
|
|
141
|
+
type: "pass",
|
|
142
|
+
n: iterations,
|
|
143
|
+
of: maxIters,
|
|
144
|
+
phase: "judged",
|
|
145
|
+
passages: passages.length,
|
|
146
|
+
sufficient: judge?.sufficient ?? null,
|
|
147
|
+
...(judge && !judge.sufficient && judge.missing ? { missing: judge.missing.slice(0, 300) } : {}),
|
|
148
|
+
});
|
|
98
149
|
if (!judge || judge.sufficient || !judge.missing) break;
|
|
99
150
|
// fold, don't accumulate: appending every round's `missing` compounds
|
|
100
151
|
// stale wants; the next retrieval focuses on the ORIGINAL question plus
|
|
@@ -104,14 +155,14 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
|
|
|
104
155
|
|
|
105
156
|
const used = [...accumulated.values()];
|
|
106
157
|
if (!used.length) {
|
|
107
|
-
|
|
158
|
+
throw new Error("Search is briefly busy — please retry in a moment.");
|
|
108
159
|
}
|
|
109
160
|
const { messages, usedHits } = buildMessages(q.query, used, q.lang, [], eNote || undefined, undefined, LIMITS.inputTokenBudget);
|
|
110
161
|
let answer = await generateOnce(env, MODELS.research, messages);
|
|
111
162
|
if (answer === null) answer = await generateOnce(env, MODELS.fallback, messages);
|
|
112
163
|
if (answer === null) {
|
|
113
164
|
telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
|
|
114
|
-
|
|
165
|
+
throw new Error("The generation model is unavailable; please retry.");
|
|
115
166
|
}
|
|
116
167
|
answer = canonicalRefusal(answer);
|
|
117
168
|
const anchors = checkQuoteAnchors(answer, used.map((h: Hit) => h.text));
|
|
@@ -124,5 +175,6 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
|
|
|
124
175
|
research: { iterations, passages: used.length, elapsed_ms: Date.now() - started, sufficient: judge?.sufficient ?? null },
|
|
125
176
|
};
|
|
126
177
|
telemetry(env, ctx, "member", "research", MODELS.research, true, answer.length, queryHash, q.lang);
|
|
127
|
-
|
|
178
|
+
emit?.({ type: "pass", n: iterations, of: maxIters, phase: "writing", passages: used.length });
|
|
179
|
+
return out;
|
|
128
180
|
}
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* selected column labels. Full table stays available for rendering;
|
|
9
9
|
* this only shapes the prompt context, and falls back to the stored
|
|
10
10
|
* text when pruning matches nothing (never worse than baseline). */
|
|
11
|
-
export
|
|
11
|
+
export interface TableSelection {
|
|
12
|
+
text: string;
|
|
13
|
+
cols: string[];
|
|
14
|
+
rowsShown: number;
|
|
15
|
+
rowsTotal: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function tableSelection(meta: any, query: string): TableSelection | null {
|
|
12
19
|
const t: any = meta?.table;
|
|
13
20
|
if (!t || !Array.isArray(t.columns) || !Array.isArray(t.rows) || !t.rows.length) return null;
|
|
14
21
|
const terms = new Set(
|
|
@@ -37,5 +44,9 @@ export function tableContext(meta: any, query: string): string | null {
|
|
|
37
44
|
rowHits.length > CAP || rowHits.length < t.rows.length
|
|
38
45
|
? `\n(${shown.length} of ${t.rows.length} rows shown; ${t.rows.length - rowHits.length} rows did not match the question terms)`
|
|
39
46
|
: "";
|
|
40
|
-
return `${header}\n${lines.join("\n")}${elided}
|
|
47
|
+
return { text: `${header}\n${lines.join("\n")}${elided}`, cols: colKeep.map((i: number) => `${t.columns[i]?.label ?? ""}${t.columns[i]?.unit ? ` [${t.columns[i].unit}]` : ""}`), rowsShown: shown.length, rowsTotal: t.rows.length };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function tableContext(meta: any, query: string): string | null {
|
|
51
|
+
return tableSelection(meta, query)?.text ?? null;
|
|
41
52
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Judge-verdict extraction, kept free of the prompt import so plain
|
|
2
|
+
// node can load it for unit tests.
|
|
3
|
+
export interface Verdict {
|
|
4
|
+
score: number; // 0-1, clamped
|
|
5
|
+
ungrounded_claims: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function coerceVerdict(obj: unknown): Verdict | null {
|
|
9
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) return null;
|
|
10
|
+
const raw = (obj as { score?: unknown }).score;
|
|
11
|
+
const score = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() !== "" ? Number(raw) : NaN;
|
|
12
|
+
if (!Number.isFinite(score)) return null;
|
|
13
|
+
const claims = (obj as { ungrounded_claims?: unknown }).ungrounded_claims;
|
|
14
|
+
return {
|
|
15
|
+
score: Math.max(0, Math.min(1, score)),
|
|
16
|
+
ungrounded_claims: Array.isArray(claims) ? claims.map(String).slice(0, 5) : [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Reasoning models emit drafts, prose, fenced blocks, and several {...}
|
|
21
|
+
// fragments before the verdict; the verdict is the LAST well-formed
|
|
22
|
+
// object carrying a numeric score. Braces can also appear INSIDE claim
|
|
23
|
+
// strings, so the scan tracks string state instead of regexing flat
|
|
24
|
+
// objects (the flat scan silently missed those shapes live).
|
|
25
|
+
export function parseVerdict(text: string): Verdict | null {
|
|
26
|
+
const stripped = text.replace(/```[a-zA-Z]*\n?/g, "").replace(/```/g, "").trim();
|
|
27
|
+
try {
|
|
28
|
+
const whole = coerceVerdict(JSON.parse(stripped));
|
|
29
|
+
if (whole) return whole;
|
|
30
|
+
} catch {
|
|
31
|
+
// not one JSON document — scan candidates
|
|
32
|
+
}
|
|
33
|
+
let verdict: Verdict | null = null;
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let start = -1;
|
|
36
|
+
let inString = false;
|
|
37
|
+
let escaped = false;
|
|
38
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
39
|
+
const ch = stripped[i];
|
|
40
|
+
if (escaped) { escaped = false; continue; }
|
|
41
|
+
if (inString && ch === "\\") { escaped = true; continue; }
|
|
42
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
43
|
+
if (inString) continue;
|
|
44
|
+
if (ch === "{") {
|
|
45
|
+
if (depth === 0) start = i;
|
|
46
|
+
depth++;
|
|
47
|
+
} else if (ch === "}" && depth > 0) {
|
|
48
|
+
depth--;
|
|
49
|
+
if (depth === 0 && start >= 0) {
|
|
50
|
+
try {
|
|
51
|
+
const v = coerceVerdict(JSON.parse(stripped.slice(start, i + 1)));
|
|
52
|
+
if (v) verdict = v;
|
|
53
|
+
} catch {
|
|
54
|
+
// malformed candidate — keep scanning
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return verdict;
|
|
60
|
+
}
|