@konneal/engine 0.2.104 → 0.2.109
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/ablate.d.ts +31 -0
- package/dist/ask-EAKLN77G.js +15 -0
- package/dist/{chunk-25ECO5M7.js → chunk-2R7OVYAO.js} +377 -185
- package/dist/{chunk-U4ERFUOK.js → chunk-4NXH5ZI6.js} +1 -1
- package/dist/{chunk-DTW4UW2E.js → chunk-DJ65EF5G.js} +1 -1
- package/dist/{chunk-NCZHN6QW.js → chunk-G4KBK5XZ.js} +175 -22
- package/dist/{chunk-ANUHRJ4Z.js → chunk-G56TYXNN.js} +3 -3
- package/dist/{chunk-TFGJ6NYS.js → chunk-HAU5INEJ.js} +5 -5
- package/dist/{chunk-KNCRYAX3.js → chunk-TH5XGPD5.js} +1 -1
- package/dist/{chunk-BOAITSVV.js → chunk-VT7DR6NQ.js} +11 -1
- package/dist/config.d.ts +6 -0
- package/dist/openapi-types.d.ts +24 -0
- package/dist/prompts/doc-summary.md +1 -0
- package/dist/prompts/speculative-verify.md +9 -0
- package/dist/route.d.ts +29 -0
- package/dist/search-HKGN7F3O.js +13 -0
- package/dist/speculative.d.ts +9 -0
- package/dist/{tools-EXDADNWX.js → tools-WSWEGJVQ.js} +4 -4
- package/dist/worker_mcp/src/index.js +2 -2
- package/dist/worker_public/src/config.js +1 -1
- package/dist/worker_public/src/index.js +20 -16
- package/dist/worker_public/src/requestScope.js +2 -2
- package/docs/spec-pipeline.md +12 -1
- package/package.json +1 -1
- package/workers/worker_public/openapi.yaml +53 -0
- package/workers/worker_public/prompts/doc-summary.md +1 -0
- package/workers/worker_public/prompts/speculative-verify.md +9 -0
- package/workers/worker_public/src/ablate.ts +97 -0
- package/workers/worker_public/src/admin.ts +14 -6
- package/workers/worker_public/src/ask.ts +120 -16
- package/workers/worker_public/src/config.ts +10 -0
- package/workers/worker_public/src/pipeline.ts +5 -2
- package/workers/worker_public/src/route.ts +77 -0
- package/workers/worker_public/src/speculative.ts +39 -0
- package/workers/worker_public/src/stages/graphPpr.ts +189 -0
- package/workers/worker_public/src/stages/index.ts +19 -0
- package/workers/worker_public/src/stages/sectionDescent.ts +91 -7
- package/workers/worker_public/src/stages/types.ts +6 -0
- package/dist/ask-M3VRAIJB.js +0 -15
- package/dist/search-5SNP5CHL.js +0 -13
|
@@ -4,15 +4,15 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-G4KBK5XZ.js";
|
|
8
8
|
import {
|
|
9
9
|
TOOLS_REGISTRY,
|
|
10
10
|
bindModelNode,
|
|
11
11
|
modelGroundingBlock
|
|
12
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-G56TYXNN.js";
|
|
13
13
|
import {
|
|
14
14
|
handleSearch
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-HAU5INEJ.js";
|
|
16
16
|
import {
|
|
17
17
|
buildMessages,
|
|
18
18
|
citations,
|
|
@@ -34,13 +34,13 @@ import {
|
|
|
34
34
|
sessionFrom,
|
|
35
35
|
telemetry,
|
|
36
36
|
understandQuery
|
|
37
|
-
} from "../../chunk-
|
|
37
|
+
} from "../../chunk-2R7OVYAO.js";
|
|
38
38
|
import {
|
|
39
39
|
cfBlobs,
|
|
40
40
|
embed,
|
|
41
41
|
generateOnce,
|
|
42
42
|
portModelRunner
|
|
43
|
-
} from "../../chunk-
|
|
43
|
+
} from "../../chunk-TH5XGPD5.js";
|
|
44
44
|
import {
|
|
45
45
|
authenticate,
|
|
46
46
|
corsHeaders,
|
|
@@ -49,14 +49,14 @@ import {
|
|
|
49
49
|
readJson,
|
|
50
50
|
validateQuery,
|
|
51
51
|
withCors
|
|
52
|
-
} from "../../chunk-
|
|
52
|
+
} from "../../chunk-DJ65EF5G.js";
|
|
53
53
|
import {
|
|
54
54
|
canonicalRefusal
|
|
55
55
|
} from "../../chunk-A3QHHUN5.js";
|
|
56
56
|
import {
|
|
57
57
|
entitlementScope,
|
|
58
58
|
standardKeysFrom
|
|
59
|
-
} from "../../chunk-
|
|
59
|
+
} from "../../chunk-4NXH5ZI6.js";
|
|
60
60
|
import {
|
|
61
61
|
LIMITS,
|
|
62
62
|
MODELS,
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
roleModel,
|
|
68
68
|
sha256Hex,
|
|
69
69
|
today
|
|
70
|
-
} from "../../chunk-
|
|
70
|
+
} from "../../chunk-VT7DR6NQ.js";
|
|
71
71
|
import "../../chunk-JZFXSTYX.js";
|
|
72
72
|
import {
|
|
73
73
|
P,
|
|
@@ -347,6 +347,9 @@ async function handleGetShared(env, slug) {
|
|
|
347
347
|
// workers/worker_public/prompts/enrichment.md
|
|
348
348
|
var enrichment_default = "You write a retrieval context for a passage from {{CORPUS_KIND}}. The context is prepended to the passage before embedding so a semantic search can locate the passage even when the query uses different vocabulary than the passage itself.\n\nWrite ONE concise sentence (at most 40 words) that situates the passage: name the publication by its exact {{PUBLISHER_NAME}} identifier (including part or annex when applicable) and what the passage covers \u2014 paraphrasing the topic in words DIFFERENT from the passage's own. Do not copy the passage verbatim, do not add facts that are not derivable from the passage or its header, do not answer or explain the content. Reply with the context sentence only \u2014 no quotes, no preamble.\n";
|
|
349
349
|
|
|
350
|
+
// workers/worker_public/prompts/doc-summary.md
|
|
351
|
+
var doc_summary_default = "You write the document-level summary for a legal-metrology publication in {{PUBLISHER_NAME}}'s retrieval index. The section summaries below cover the publication clause by clause. Write one compact abstract, three to five sentences: what the publication regulates or provides, its scope and applicability, and the structure a reader should know (parts, annexes, where the normative core sits). Use the publication's own terms exactly. No preamble, no headings.\n";
|
|
352
|
+
|
|
350
353
|
// workers/worker_public/prompts/section-summary.md
|
|
351
354
|
var section_summary_default = "You summarize one numbered clause of a metrology publication for a retrieval index. You are given the publication, the clause number, and excerpts of its sub-clauses.\n\nWrite a dense summary of 3 to 5 sentences stating what the clause governs and how its sub-clauses divide the subject. Name each sub-clause number together with its topic, in document order.\n\nPlain factual prose. No preamble, no headings, no bullet list, no quotation marks around the whole text. Write in the same language as the excerpts.\n";
|
|
352
355
|
|
|
@@ -451,18 +454,19 @@ async function handleSectionUnit(env, ctx, req) {
|
|
|
451
454
|
return { id: u?.id ?? null, ok: false, error: "invalid unit (id, metadata.doc_id, metadata.clause_anchor, children required)" };
|
|
452
455
|
}
|
|
453
456
|
try {
|
|
457
|
+
const level = u?.level === 2 ? 2 : 1;
|
|
454
458
|
const cacheKey = `s:${u.id}`;
|
|
455
459
|
let summary = body?.force === true ? null : await env.CACHE.get(cacheKey);
|
|
456
460
|
const cached = !!summary;
|
|
457
461
|
if (!summary) {
|
|
458
|
-
const head = `${m.docidentifier ?? m.doc_id} \xA7${m.clause_anchor}${m.clause_title ? " \u2014 " + m.clause_title : ""}`;
|
|
459
|
-
const listing = u.children.slice(0, 12).map((c) => `\xA7${c.anchor ?? ""}${c.title ? " " + c.title : ""} \u2014 ${String(c.excerpt ?? "").slice(0, 260)}`).join("\n");
|
|
462
|
+
const head = level === 2 ? `${m.docidentifier ?? m.doc_id} \u2014 document summary` : `${m.docidentifier ?? m.doc_id} \xA7${m.clause_anchor}${m.clause_title ? " \u2014 " + m.clause_title : ""}`;
|
|
463
|
+
const listing = u.children.slice(0, 12).map((c) => level === 2 ? `- ${c.title ?? c.anchor ?? ""} \u2014 ${String(c.excerpt ?? "").slice(0, 300)}` : `\xA7${c.anchor ?? ""}${c.title ? " " + c.title : ""} \u2014 ${String(c.excerpt ?? "").slice(0, 260)}`).join("\n");
|
|
460
464
|
const res = await env.AI.run(model, {
|
|
461
465
|
messages: [
|
|
462
|
-
{ role: "system", content: section_summary_default.trimEnd() },
|
|
466
|
+
{ role: "system", content: (level === 2 ? doc_summary_default : section_summary_default).trimEnd() },
|
|
463
467
|
{ role: "user", content: `${head}
|
|
464
468
|
|
|
465
|
-
Sub-clauses:
|
|
469
|
+
${level === 2 ? "Section summaries:" : "Sub-clauses:"}
|
|
466
470
|
${listing}` }
|
|
467
471
|
],
|
|
468
472
|
max_tokens: 1600,
|
|
@@ -487,9 +491,9 @@ Covers: ${childAnchors}`;
|
|
|
487
491
|
const vectorText = `${m.docidentifier ?? m.doc_id} \xA7${m.clause_anchor} ${text}`.slice(0, 2e3);
|
|
488
492
|
const vector = await embed(portModelRunner(env), MODELS.embed, vectorText);
|
|
489
493
|
await env.VECTORIZE.upsert([
|
|
490
|
-
{ id: u.id, values: vector, metadata: { ...m, chunk_text: text, section_summary: "1", child_anchors: childAnchors, ctx: "1" } }
|
|
494
|
+
{ id: u.id, values: vector, metadata: { ...m, chunk_text: text, section_summary: "1", ...level === 2 ? { summary_level: "2" } : {}, child_anchors: childAnchors, ctx: "1" } }
|
|
491
495
|
]);
|
|
492
|
-
return { id: u.id, ok: true, cached, children: u.children.length };
|
|
496
|
+
return { id: u.id, ok: true, cached, level, children: u.children.length };
|
|
493
497
|
} catch (e) {
|
|
494
498
|
return { id: u.id, ok: false, error: String(e?.message ?? e).slice(0, 200) };
|
|
495
499
|
}
|
|
@@ -990,13 +994,13 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
990
994
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
991
995
|
body: JSON.stringify({ ...args, stream: false })
|
|
992
996
|
});
|
|
993
|
-
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-
|
|
997
|
+
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-WSWEGJVQ.js");
|
|
994
998
|
let payload;
|
|
995
999
|
if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
|
|
996
1000
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
997
1001
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
998
1002
|
} else {
|
|
999
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1003
|
+
const res = name === "ask" ? await (await import("../../ask-EAKLN77G.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-HKGN7F3O.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1000
1004
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1001
1005
|
}
|
|
1002
1006
|
telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
requestSalt,
|
|
5
5
|
resolveRequestScope,
|
|
6
6
|
standardKeysFrom
|
|
7
|
-
} from "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
7
|
+
} from "../../chunk-4NXH5ZI6.js";
|
|
8
|
+
import "../../chunk-VT7DR6NQ.js";
|
|
9
9
|
import "../../chunk-3FYJM7LH.js";
|
|
10
10
|
export {
|
|
11
11
|
entitlementScope,
|
package/docs/spec-pipeline.md
CHANGED
|
@@ -26,6 +26,16 @@ edits to existing stages, the runner, or `retrieve()`. A stage that
|
|
|
26
26
|
needs a new context field declares it on `PipelineContext` (the field's
|
|
27
27
|
owner is the stage that writes it).
|
|
28
28
|
|
|
29
|
+
The registry is also the ablation switch (TODO.sota/09):
|
|
30
|
+
`projectStages(names)` projects it to a named subset — registry order
|
|
31
|
+
preserved, unknown names thrown — and `RetrieveOptions.stageSubset`
|
|
32
|
+
carries the projection. The ask boundary (`src/ablate.ts`) gates the
|
|
33
|
+
`ablate_stages` / `ablate_no_verdict` body fields behind the admin
|
|
34
|
+
credential (`x-admin-token`); an ablated ask bypasses every answer-cache
|
|
35
|
+
read and write and echoes its configuration in the response. A new
|
|
36
|
+
stage updates the pinned vocabulary in `tests/ablate.test.ts`
|
|
37
|
+
consciously — that list is the runner's contract.
|
|
38
|
+
|
|
29
39
|
## Registry order and invariants
|
|
30
40
|
|
|
31
41
|
Order is load-bearing. The phase structure:
|
|
@@ -41,6 +51,7 @@ candidate lanes → pool open → pool-level merges → refinement → window as
|
|
|
41
51
|
| 3 | `glossary` | `env.GLOSSARY && vector` | additive | `glossary` | Vocabulary link: dense glossary candidates + cross-encoder rerank; top-3 distinct concepts. MUST run before the concept-graph and steering stages — they consume `glossary`. |
|
|
42
52
|
| 4 | `concept-graph` | `glossary.length && env.DB && vector` | additive | appends `matches` | Linked terms → defining doc numbers (D1, parallel per-term) → metadata-filtered dense merge at `conceptGraphDiscount`. |
|
|
43
53
|
| 5 | `graph-lane` | `opts.graphDocNumbers && vector` | additive | appends `matches` | Caller-resolved family/successor doc numbers merged at `graphLaneDiscount`. |
|
|
54
|
+
| 5b | `graph-ppr` | `(glossary.length \|\| u.docidentifier) && env.DB && vector` | additive | appends `matches` | HippoRAG-style multi-hop memory (TODO.sota/06): Personalized-PageRank over the D1 graph (defines/successor/part_of/variant_of/amends/cites, undirected spread) seeded by the vocabulary link's concepts and the understood publication's family node; documents ranked by stationary mass merge BM25-within-set candidates at `pprDiscount` (the narrowed-lane law). Two bounded hops, deterministic iteration — same seeds, same ranks. |
|
|
44
55
|
| 6 | `multi-query` | `u.query_variants` | blocking (per-variant catches inside) | REPLACES `matches` | RRF-fuses the primary ranking with each variant's (k=60, top `retrieveK`). |
|
|
45
56
|
| 7 | `sub-query` | `u.complexity === "complex" && u.sub_queries` | blocking (per-sub catches inside) | appends `matches` | Complementary-perspective union at `subQueryDiscount` (no RRF). |
|
|
46
57
|
| 8 | `pool-open` | — | blocking | `hits` | Converts the candidate pool to the `Hit` shape. Last writer of `matches`; every later stage operates on `hits`. |
|
|
@@ -62,7 +73,7 @@ candidate lanes → pool open → pool-level merges → refinement → window as
|
|
|
62
73
|
| 23 | `structural-propagate` | — | blocking | REPLACES `hits` | FABLE TreeExpansion: score blends along the clause tree. |
|
|
63
74
|
| 24 | `diversity` | — | blocking | `finalHits` (from `hits`) | Per-publication caps (1 overview / 2–3 clauses; global overview cap 2/6); window cut to `rerankKeep`. FIRST writer of `finalHits`. |
|
|
64
75
|
| 25 | `typed-pin` | pin families resolvable | blocking (inner parent-fetch additive) | `finalHits` | Answer-contract v2: one typed unit guaranteed a slot (+ small-to-big parent fetch at `smallToBigDiscount`). |
|
|
65
|
-
| 26 | `section-descent` | a ranked
|
|
76
|
+
| 26 | `section-descent` | a ranked summary has children | additive | `finalHits` | Summary node → child clauses at `sectionDescentDiscount`; the summary retires when children answer. Two levels (the RAPTOR extension, TODO.sota/06): a level-1 section summary's children are CLAUSE anchors found by the same-query filtered search; a level-2 DOCUMENT summary's children are LEVEL-1 UNIT ids — read by id (the ≤20 getByIds law), their clauses from the D1 corpus directly (the l6a law — the index metadata filters are dead), the two leading sections (reading order) consumed, the synthetic nodes retired. |
|
|
66
77
|
| 27 | `dedup` | — | blocking | `finalHits` | FABLE ancestor-descendant same-chain collapse (≥0.5 text overlap). |
|
|
67
78
|
| 28 | `window-floor` | — | blocking | filters `finalHits` | Evidence-budget cut at `windowFloorFraction` of top; typed/family/unscored exempt; never fewer than two. |
|
|
68
79
|
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.109",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
|
@@ -1154,6 +1154,46 @@ components:
|
|
|
1154
1154
|
response also carries the grounding passages in context.
|
|
1155
1155
|
Evaluation callers use it to observe what the answer was
|
|
1156
1156
|
built from.
|
|
1157
|
+
ablate_stages:
|
|
1158
|
+
type: array
|
|
1159
|
+
items:
|
|
1160
|
+
type: string
|
|
1161
|
+
description: >-
|
|
1162
|
+
Research-only (TODO.sota/09): run the retrieval registry
|
|
1163
|
+
projected to these stage names (registry order preserved;
|
|
1164
|
+
unknown names are a 400). Requires the x-admin-token header —
|
|
1165
|
+
a request naming a configuration without it is a 403.
|
|
1166
|
+
Ablated answers bypass every answer cache read and write and
|
|
1167
|
+
echo the active configuration in the response's ablate field.
|
|
1168
|
+
ablate_no_verdict:
|
|
1169
|
+
type: boolean
|
|
1170
|
+
default: false
|
|
1171
|
+
description: >-
|
|
1172
|
+
Research-only: skip the verdict engine (machine verdicts and
|
|
1173
|
+
condition-set evaluation); the answer model narrates from
|
|
1174
|
+
passages alone. Same admin credential and cache law as
|
|
1175
|
+
ablate_stages.
|
|
1176
|
+
ablate_speculative:
|
|
1177
|
+
type: boolean
|
|
1178
|
+
default: false
|
|
1179
|
+
description: >-
|
|
1180
|
+
Research-only (TODO.sota/02 row 2): arm speculative
|
|
1181
|
+
draft-verify — the cheap model drafts an answer per
|
|
1182
|
+
diversified subset of the window in parallel, one strong
|
|
1183
|
+
call verifies every candidate against the full passages and
|
|
1184
|
+
produces the final answer. Same admin credential and cache
|
|
1185
|
+
law as ablate_stages.
|
|
1186
|
+
ablate_route:
|
|
1187
|
+
type: string
|
|
1188
|
+
enum: [fast, deep, adaptive]
|
|
1189
|
+
default: adaptive
|
|
1190
|
+
description: >-
|
|
1191
|
+
Research-only: force the route (TODO.sota/05). fast drops the
|
|
1192
|
+
candidate-expansion lanes (hyde, multi-query, sub-query) and
|
|
1193
|
+
floors generation effort at low; deep keeps the full
|
|
1194
|
+
registry. adaptive (the default) lets the router decide from
|
|
1195
|
+
understanding's features. Same admin credential and cache
|
|
1196
|
+
law as ablate_stages.
|
|
1157
1197
|
lang:
|
|
1158
1198
|
type: string
|
|
1159
1199
|
description: Forces the answer language, for example en or fr.
|
|
@@ -1224,6 +1264,19 @@ components:
|
|
|
1224
1264
|
intent: { type: string }
|
|
1225
1265
|
doc: { type: string, nullable: true }
|
|
1226
1266
|
edition: { type: string, nullable: true }
|
|
1267
|
+
route:
|
|
1268
|
+
type: object
|
|
1269
|
+
description: >-
|
|
1270
|
+
The adaptive route the ask rode (TODO.sota/05): fast (the
|
|
1271
|
+
registry minus the candidate-expansion lanes, generation
|
|
1272
|
+
effort floored at low) or deep (the full registry), with the
|
|
1273
|
+
features that fired — the telemetry the routing-accuracy
|
|
1274
|
+
analysis reads.
|
|
1275
|
+
properties:
|
|
1276
|
+
route: { type: string, enum: [fast, deep] }
|
|
1277
|
+
features:
|
|
1278
|
+
type: array
|
|
1279
|
+
items: { type: string }
|
|
1227
1280
|
term: { type: string, nullable: true }
|
|
1228
1281
|
terms:
|
|
1229
1282
|
type: array
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You write the document-level summary for a legal-metrology publication in {{PUBLISHER_NAME}}'s retrieval index. The section summaries below cover the publication clause by clause. Write one compact abstract, three to five sentences: what the publication regulates or provides, its scope and applicability, and the structure a reader should know (parts, annexes, where the normative core sits). Use the publication's own terms exactly. No preamble, no headings.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
You answer questions about legal-metrology publications for {{PUBLISHER_NAME}}. Candidate draft answers were prepared in parallel, each from a PARTIAL view of the evidence (a subset of the passages); you see every candidate beside the FULL passages. Produce the final answer.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
1. Compare the candidates against the full passages. Adopt the candidate best supported by the evidence as your starting point; correct every claim the full passages contradict or fail to support, and fold in anything the other candidates got right that the adopted one missed.
|
|
5
|
+
2. Ground every factual claim in the numbered passages and cite them inline as [1], [2], … exactly as the passages are numbered below. Never cite a passage that is not there.
|
|
6
|
+
3. Quote verbatim — with quotation marks — when a clause's exact wording matters (definitions, thresholds, requirements); a quotation must appear word-for-word in the passages.
|
|
7
|
+
4. Numeric and table values come from the passages exactly; never retype a table as markdown — refer to the table and cite it.
|
|
8
|
+
5. If the passages do not answer the question, say so plainly instead of guessing.
|
|
9
|
+
6. Be complete but compact: answer the question, cite, stop.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ── The ablation request boundary (TODO.sota/09) ──
|
|
2
|
+
//
|
|
3
|
+
// A runner (scripts/ablate.mjs in the deployment repo) scores pipeline
|
|
4
|
+
// configurations — stage lists, verdict-engine off — over the dev half
|
|
5
|
+
// of the benchmark. The configuration rides the /v1/ask body:
|
|
6
|
+
//
|
|
7
|
+
// ablate_stages: ["dense", "poolOpen", ...] // registry projection
|
|
8
|
+
// ablate_no_verdict: true // verdict engine off
|
|
9
|
+
//
|
|
10
|
+
// gated by the ADMIN credential in the x-admin-token header (the same
|
|
11
|
+
// secret the /admin routes carry; the ask bearer stays a normal tier
|
|
12
|
+
// key, so quota and telemetry behave exactly as for a real ask).
|
|
13
|
+
//
|
|
14
|
+
// The cache law: an ablated answer is request-scoped context — it must
|
|
15
|
+
// never be served to a later caller nor written for one. ask.ts honors
|
|
16
|
+
// that by bypassing every answer-cache read and write when a
|
|
17
|
+
// configuration is active.
|
|
18
|
+
//
|
|
19
|
+
// Self-contained (no imports) so the unit tests run on plain node type
|
|
20
|
+
// stripping, like anchors/refusal/verdict.
|
|
21
|
+
|
|
22
|
+
export interface AblateConfig {
|
|
23
|
+
/** Registry projection (names; projectStages applies them). Null/absent
|
|
24
|
+
* = the serving default (adaptive routing decides). */
|
|
25
|
+
stages: string[] | null;
|
|
26
|
+
/** Skip the verdict engine (machineVerdict + condition sets): the
|
|
27
|
+
* answer model narrates from passages alone. */
|
|
28
|
+
noVerdict: boolean;
|
|
29
|
+
/** Force a route: "fast" drops the expansion lanes, "deep" keeps the
|
|
30
|
+
* full registry, "adaptive" (the default) lets the router decide
|
|
31
|
+
* (TODO.sota/05). The grid measures the routes by forcing them. */
|
|
32
|
+
route: "fast" | "deep" | "adaptive";
|
|
33
|
+
/** Speculative draft-verify (TODO.sota/02 row 2): cheap drafts per
|
|
34
|
+
* diversified subset + one strong verifier. Off in serving until the
|
|
35
|
+
* gate promotes it; the grid arms it per ask. */
|
|
36
|
+
speculative: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type AblateParse =
|
|
40
|
+
| { ok: true; config: AblateConfig | null }
|
|
41
|
+
| { ok: false; status: number; code: string; message: string };
|
|
42
|
+
|
|
43
|
+
/** Parse and authorize an ablation configuration. Non-null config
|
|
44
|
+
* requires the admin token; a request that NAMES a configuration
|
|
45
|
+
* without the credential is 403 — never silently ignored, or a
|
|
46
|
+
* misconfigured runner would measure the full pipeline believing it
|
|
47
|
+
* measured its ablation. */
|
|
48
|
+
export function parseAblate(
|
|
49
|
+
body: any,
|
|
50
|
+
adminToken: string | undefined,
|
|
51
|
+
presentedToken: string | null,
|
|
52
|
+
knownStages: readonly string[],
|
|
53
|
+
): AblateParse {
|
|
54
|
+
const rawStages = body?.ablate_stages;
|
|
55
|
+
const noVerdict = body?.ablate_no_verdict === true;
|
|
56
|
+
const rawRoute = body?.ablate_route;
|
|
57
|
+
const speculative = body?.ablate_speculative === true;
|
|
58
|
+
const namesAblation = rawStages !== undefined && rawStages !== null;
|
|
59
|
+
const routeAblation = rawRoute !== undefined && rawRoute !== null && rawRoute !== "adaptive";
|
|
60
|
+
|
|
61
|
+
if (!namesAblation && !noVerdict && !routeAblation && !speculative) return { ok: true, config: null };
|
|
62
|
+
|
|
63
|
+
if (!adminToken || presentedToken !== adminToken) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
status: 403,
|
|
67
|
+
code: "ablation_forbidden",
|
|
68
|
+
message: "ablation fields require the admin credential (x-admin-token)",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let route: AblateConfig["route"] = "adaptive";
|
|
73
|
+
if (routeAblation) {
|
|
74
|
+
if (rawRoute !== "fast" && rawRoute !== "deep") {
|
|
75
|
+
return { ok: false, status: 400, code: "invalid_ablation", message: "ablate_route must be \"fast\", \"deep\" or \"adaptive\"" };
|
|
76
|
+
}
|
|
77
|
+
route = rawRoute;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let stages: string[] | null = null;
|
|
81
|
+
if (namesAblation) {
|
|
82
|
+
if (!Array.isArray(rawStages) || rawStages.length === 0) {
|
|
83
|
+
return { ok: false, status: 400, code: "invalid_ablation", message: "ablate_stages must be a non-empty array of stage names" };
|
|
84
|
+
}
|
|
85
|
+
if (rawStages.some((n: unknown) => typeof n !== "string")) {
|
|
86
|
+
return { ok: false, status: 400, code: "invalid_ablation", message: "ablate_stages must contain only stage-name strings" };
|
|
87
|
+
}
|
|
88
|
+
const known = new Set(knownStages);
|
|
89
|
+
const unknown = (rawStages as string[]).filter((n) => !known.has(n));
|
|
90
|
+
if (unknown.length) {
|
|
91
|
+
return { ok: false, status: 400, code: "invalid_ablation", message: `unknown stage(s): ${unknown.join(", ")}` };
|
|
92
|
+
}
|
|
93
|
+
stages = [...new Set(rawStages as string[])];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { ok: true, config: { stages, noVerdict, route, speculative } };
|
|
97
|
+
}
|
|
@@ -6,6 +6,7 @@ import { embed } from "./ai";
|
|
|
6
6
|
import { err, json, corsHeaders, readJson, authenticate } from "./lib/http";
|
|
7
7
|
import type { Env } from "./env";
|
|
8
8
|
import enrichmentPrompt from "../prompts/enrichment.md";
|
|
9
|
+
import docSummaryPrompt from "../prompts/doc-summary.md";
|
|
9
10
|
import sectionSummaryPrompt from "../prompts/section-summary.md";
|
|
10
11
|
import relevancyPrompt from "../prompts/relevancy.md";
|
|
11
12
|
import precisionPrompt from "../prompts/precision.md";
|
|
@@ -138,19 +139,26 @@ export async function handleSectionUnit(env: Env, ctx: ExecutionContext, req: Re
|
|
|
138
139
|
return { id: u?.id ?? null, ok: false, error: "invalid unit (id, metadata.doc_id, metadata.clause_anchor, children required)" };
|
|
139
140
|
}
|
|
140
141
|
try {
|
|
142
|
+
// RAPTOR depth (TODO.sota/06, the catalog's row 3): level 2
|
|
143
|
+
// summarizes the LEVEL-1 summary units of one document into a
|
|
144
|
+
// document-level navigation node — the children are unit ids,
|
|
145
|
+
// and the stamp says so the descent can walk another level.
|
|
146
|
+
const level = u?.level === 2 ? 2 : 1;
|
|
141
147
|
const cacheKey = `s:${u.id}`;
|
|
142
148
|
let summary = body?.force === true ? null : await env.CACHE.get(cacheKey);
|
|
143
149
|
const cached = !!summary;
|
|
144
150
|
if (!summary) {
|
|
145
|
-
const head =
|
|
151
|
+
const head = level === 2
|
|
152
|
+
? `${m.docidentifier ?? m.doc_id} — document summary`
|
|
153
|
+
: `${m.docidentifier ?? m.doc_id} §${m.clause_anchor}${m.clause_title ? " — " + m.clause_title : ""}`;
|
|
146
154
|
const listing = u.children
|
|
147
155
|
.slice(0, 12)
|
|
148
|
-
.map((c: any) => `§${c.anchor ?? ""}${c.title ? " " + c.title : ""} — ${String(c.excerpt ?? "").slice(0, 260)}`)
|
|
156
|
+
.map((c: any) => (level === 2 ? `- ${c.title ?? c.anchor ?? ""} — ${String(c.excerpt ?? "").slice(0, 300)}` : `§${c.anchor ?? ""}${c.title ? " " + c.title : ""} — ${String(c.excerpt ?? "").slice(0, 260)}`))
|
|
149
157
|
.join("\n");
|
|
150
158
|
const res: any = await env.AI.run(model, {
|
|
151
159
|
messages: [
|
|
152
|
-
{ role: "system", content: sectionSummaryPrompt.trimEnd() },
|
|
153
|
-
{ role: "user", content: `${head}\n\
|
|
160
|
+
{ role: "system", content: (level === 2 ? docSummaryPrompt : sectionSummaryPrompt).trimEnd() },
|
|
161
|
+
{ role: "user", content: `${head}\n\n${level === 2 ? "Section summaries:" : "Sub-clauses:"}\n${listing}` },
|
|
154
162
|
],
|
|
155
163
|
max_tokens: 1600, // parity with the chunk-enrichment call — 900 starved ~40% of section summaries (model-card budget rule)
|
|
156
164
|
reasoning_effort: "low",
|
|
@@ -172,9 +180,9 @@ export async function handleSectionUnit(env: Env, ctx: ExecutionContext, req: Re
|
|
|
172
180
|
const vectorText = `${m.docidentifier ?? m.doc_id} §${m.clause_anchor} ${text}`.slice(0, 2000);
|
|
173
181
|
const vector = await embed(portModelRunner(env), MODELS.embed, vectorText);
|
|
174
182
|
await env.VECTORIZE.upsert([
|
|
175
|
-
{ id: u.id, values: vector, metadata: { ...m, chunk_text: text, section_summary: "1", child_anchors: childAnchors, ctx: "1" } },
|
|
183
|
+
{ id: u.id, values: vector, metadata: { ...m, chunk_text: text, section_summary: "1", ...(level === 2 ? { summary_level: "2" } : {}), child_anchors: childAnchors, ctx: "1" } },
|
|
176
184
|
]);
|
|
177
|
-
return { id: u.id, ok: true, cached, children: u.children.length };
|
|
185
|
+
return { id: u.id, ok: true, cached, level, children: u.children.length };
|
|
178
186
|
} catch (e: any) {
|
|
179
187
|
return { id: u.id, ok: false, error: String(e?.message ?? e).slice(0, 200) };
|
|
180
188
|
}
|