@konneal/engine 0.1.0
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/LICENSE +29 -0
- package/README.md +13 -0
- package/dist/admin.d.ts +26 -0
- package/dist/ai.d.ts +6 -0
- package/dist/anchors.d.ts +6 -0
- package/dist/answercache.d.ts +22 -0
- package/dist/ask.d.ts +5 -0
- package/dist/auth.d.ts +12 -0
- package/dist/bubble.d.ts +14 -0
- package/dist/chunk-LLWPT2XV.js +49 -0
- package/dist/chunk-MB74PTRM.js +114 -0
- package/dist/chunk-WOGQM7DJ.js +197 -0
- package/dist/chunk-WWNCWKKC.js +42 -0
- package/dist/completion.d.ts +5 -0
- package/dist/config.d.ts +154 -0
- package/dist/config.js +37 -0
- package/dist/context.d.ts +115 -0
- package/dist/conversations.d.ts +5 -0
- package/dist/drafts.d.ts +129 -0
- package/dist/env.d.ts +57 -0
- package/dist/faithfulness.d.ts +5 -0
- package/dist/grader.d.ts +3 -0
- package/dist/graph.d.ts +13 -0
- package/dist/hybrid.d.ts +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5373 -0
- package/dist/internal_gateway.d.ts +14 -0
- package/dist/lexical.d.ts +7 -0
- package/dist/livedata.d.ts +77 -0
- package/dist/memories.d.ts +10 -0
- package/dist/modelplane.d.ts +61 -0
- package/dist/oidc.d.ts +73 -0
- package/dist/pipeline.d.ts +57 -0
- package/dist/profile.d.ts +2 -0
- package/dist/profile.gen.d.ts +70 -0
- package/dist/profile.js +8 -0
- package/dist/projects.d.ts +8 -0
- package/dist/prompts/conversational.md +8 -0
- package/dist/prompts/enrichment.md +3 -0
- package/dist/prompts/faithfulness.md +1 -0
- package/dist/prompts/grader.md +5 -0
- package/dist/prompts/listwise.md +3 -0
- package/dist/prompts/precision.md +1 -0
- package/dist/prompts/reflect.md +1 -0
- package/dist/prompts/relevancy.md +1 -0
- package/dist/prompts/research.md +10 -0
- package/dist/prompts/section-summary.md +5 -0
- package/dist/prompts/summarize.md +1 -0
- package/dist/prompts/system.md +18 -0
- package/dist/prompts/understanding.md +17 -0
- package/dist/quota.d.ts +13 -0
- package/dist/reflect.d.ts +5 -0
- package/dist/refs.d.ts +40 -0
- package/dist/refusal.d.ts +9 -0
- package/dist/refusal.js +9 -0
- package/dist/requestScope.d.ts +26 -0
- package/dist/requestScope.js +10 -0
- package/dist/research.d.ts +8 -0
- package/dist/search.d.ts +4 -0
- package/dist/selfquery.d.ts +7 -0
- package/dist/session.d.ts +1 -0
- package/dist/share.d.ts +2 -0
- package/dist/structural.d.ts +27 -0
- package/dist/tablecontext.d.ts +11 -0
- package/dist/understand.d.ts +11 -0
- package/dist/understandContract.d.ts +29 -0
- package/dist/verdict.d.ts +24 -0
- package/docs/API.md +451 -0
- package/docs/ARCHITECTURE.md +302 -0
- package/docs/AUDIT-2026-08-24.md +71 -0
- package/docs/CONTRIBUTOR-AUDIT-2026-08-25.md +147 -0
- package/docs/INGEST-ARCHITECTURE.md +158 -0
- package/docs/MCP.md +92 -0
- package/docs/METANORMA-AI-SERIALIZATION.md +247 -0
- package/docs/MKO-EXPORT-PIPELINE.md +147 -0
- package/docs/REDESIGN-NORMATIVE-RAG-ETSI.md +485 -0
- package/docs/RESEARCH-SOTA-2026.md +243 -0
- package/docs/ROADMAP-SOTA.md +130 -0
- package/docs/SOTA-STAGE-SPECS.md +509 -0
- package/docs/annealment/F1-verdict.md +27 -0
- package/docs/annealment/F10-notes.md +19 -0
- package/docs/annealment/F11-composition.md +17 -0
- package/docs/annealment/F12-passport.md +17 -0
- package/docs/annealment/F2-counterfactual.md +20 -0
- package/docs/annealment/F3-absence.md +21 -0
- package/docs/annealment/F4-instance.md +18 -0
- package/docs/annealment/F5-workflow.md +21 -0
- package/docs/annealment/F6-impact.md +21 -0
- package/docs/annealment/F7-editions.md +18 -0
- package/docs/annealment/F8-selfverify.md +19 -0
- package/docs/annealment/F9-projection-qa.md +17 -0
- package/docs/annealment/L0-locate.md +19 -0
- package/docs/annealment/L1-extract.md +18 -0
- package/docs/annealment/L2-nomenclature.md +22 -0
- package/docs/annealment/L3-geometry.md +23 -0
- package/docs/annealment/L4-composition.md +21 -0
- package/docs/annealment/L5-cross-standard.md +20 -0
- package/docs/annealment/L6-diachrony.md +21 -0
- package/docs/annealment/L7-perception.md +20 -0
- package/docs/annealment/L8-computation.md +22 -0
- package/docs/annealment/L9-instance-process.md +23 -0
- package/docs/annealment/README.md +10 -0
- package/docs/guidelines-metanorma-ai-programme.md +279 -0
- package/docs/identity-onboarding-rag.md +65 -0
- package/docs/identity-service.md +219 -0
- package/docs/knowledge-annealment.md +273 -0
- package/docs/konneal-extraction-plan.md +481 -0
- package/docs/metanorma-for-ai.md +270 -0
- package/docs/mirror-plan.md +36 -0
- package/docs/multi-sdo-architecture.md +191 -0
- package/docs/paper-annealment-comparison.md +259 -0
- package/docs/paper-assets/architecture.svg +94 -0
- package/docs/paper-assets/contract-v2.svg +94 -0
- package/docs/paper-assets/mko-ingest.svg +91 -0
- package/docs/paper-oiml-bulletin.md +402 -0
- package/docs/paper-oiml-bulletin.mdx +419 -0
- package/docs/product-branding-options.md +172 -0
- package/docs/projects-design.md +88 -0
- package/docs/sota-mechanisms.md +184 -0
- package/docs/spec-api.md +77 -0
- package/docs/spec-pipeline.md +126 -0
- package/docs/vector-adapter.md +88 -0
- package/package.json +70 -0
- package/profile/corpora.yaml +5 -0
- package/profile/datasets.yaml +14 -0
- package/profile/prompts.yaml +5 -0
- package/profile/publisher.yaml +17 -0
- package/profile/retrieval.yaml +1 -0
- package/profile/sources.yaml +5 -0
- package/profile/ui.yaml +7 -0
- package/scripts/gen_profile.mjs +33 -0
- package/workers/shared/ai.ts +21 -0
- package/workers/shared/auth.ts +16 -0
- package/workers/shared/chunk.ts +108 -0
- package/workers/shared/oidc.ts +312 -0
- package/workers/shared/router.ts +45 -0
- package/workers/shared/session.ts +104 -0
- package/workers/worker_internal/src/index.ts +157 -0
- package/workers/worker_internal/tsconfig.json +15 -0
- package/workers/worker_internal/wrangler.toml +32 -0
- package/workers/worker_mcp/src/index.ts +175 -0
- package/workers/worker_mcp/tsconfig.json +13 -0
- package/workers/worker_mcp/wrangler.toml +18 -0
- package/workers/worker_public/migrations/0002_conversations.sql +22 -0
- package/workers/worker_public/migrations/0003_shared_conversations.sql +9 -0
- package/workers/worker_public/migrations/0004_graph.sql +16 -0
- package/workers/worker_public/migrations/0005_documents.sql +19 -0
- package/workers/worker_public/migrations/0006_conversation_entities.sql +11 -0
- package/workers/worker_public/migrations/0007_chunks_fts.sql +43 -0
- package/workers/worker_public/migrations/0008_unit_payloads.sql +15 -0
- package/workers/worker_public/migrations/0009_chunks_unit.sql +8 -0
- package/workers/worker_public/migrations/0009_message_context.sql +7 -0
- package/workers/worker_public/migrations/0010_model_nodes.sql +33 -0
- package/workers/worker_public/migrations/0011_schema_union.sql +38 -0
- package/workers/worker_public/migrations/0012_memories.sql +15 -0
- package/workers/worker_public/migrations/0013_projects.sql +21 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.d.ts +16373 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.ts +16328 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.d.ts +16382 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.ts +16337 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.d.ts +16383 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.ts +16338 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.d.ts +16403 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.ts +16358 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.d.ts +16408 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.ts +16363 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/README.md +135 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/entrypoints.svg +53 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.d.ts +17095 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.ts +17050 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.d.ts +16447 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.ts +16402 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/package.json +11 -0
- package/workers/worker_public/package.json +13 -0
- package/workers/worker_public/prompts/conversational.md +8 -0
- package/workers/worker_public/prompts/enrichment.md +3 -0
- package/workers/worker_public/prompts/faithfulness.md +1 -0
- package/workers/worker_public/prompts/grader.md +5 -0
- package/workers/worker_public/prompts/listwise.md +3 -0
- package/workers/worker_public/prompts/precision.md +1 -0
- package/workers/worker_public/prompts/reflect.md +1 -0
- package/workers/worker_public/prompts/relevancy.md +1 -0
- package/workers/worker_public/prompts/research.md +10 -0
- package/workers/worker_public/prompts/section-summary.md +5 -0
- package/workers/worker_public/prompts/summarize.md +1 -0
- package/workers/worker_public/prompts/system.md +18 -0
- package/workers/worker_public/prompts/understanding.md +17 -0
- package/workers/worker_public/public/app.js +166 -0
- package/workers/worker_public/public/index.html +48 -0
- package/workers/worker_public/public/style.css +147 -0
- package/workers/worker_public/schema.sql +248 -0
- package/workers/worker_public/src/admin.ts +358 -0
- package/workers/worker_public/src/ai.ts +71 -0
- package/workers/worker_public/src/anchors.ts +41 -0
- package/workers/worker_public/src/answercache.ts +72 -0
- package/workers/worker_public/src/ask.ts +1094 -0
- package/workers/worker_public/src/auth.ts +252 -0
- package/workers/worker_public/src/bubble.ts +111 -0
- package/workers/worker_public/src/completion.ts +75 -0
- package/workers/worker_public/src/config.ts +238 -0
- package/workers/worker_public/src/context.ts +238 -0
- package/workers/worker_public/src/conversations.ts +162 -0
- package/workers/worker_public/src/drafts.ts +497 -0
- package/workers/worker_public/src/env.ts +90 -0
- package/workers/worker_public/src/faithfulness.ts +63 -0
- package/workers/worker_public/src/grader.ts +89 -0
- package/workers/worker_public/src/graph.ts +63 -0
- package/workers/worker_public/src/hybrid.ts +77 -0
- package/workers/worker_public/src/index.ts +441 -0
- package/workers/worker_public/src/internal_gateway.ts +41 -0
- package/workers/worker_public/src/lexical.ts +86 -0
- package/workers/worker_public/src/lib/hit.ts +4 -0
- package/workers/worker_public/src/lib/http.ts +83 -0
- package/workers/worker_public/src/lib/router.ts +4 -0
- package/workers/worker_public/src/livedata.ts +334 -0
- package/workers/worker_public/src/memories.ts +81 -0
- package/workers/worker_public/src/modelplane.ts +213 -0
- package/workers/worker_public/src/oidc.ts +333 -0
- package/workers/worker_public/src/pipeline.ts +377 -0
- package/workers/worker_public/src/ports/blobs.ts +7 -0
- package/workers/worker_public/src/ports/cloudflare/adapters.ts +177 -0
- package/workers/worker_public/src/ports/kv.ts +8 -0
- package/workers/worker_public/src/ports/model.ts +28 -0
- package/workers/worker_public/src/ports/runtime.ts +13 -0
- package/workers/worker_public/src/ports/store.ts +20 -0
- package/workers/worker_public/src/ports/vector.ts +26 -0
- package/workers/worker_public/src/profile.gen.ts +101 -0
- package/workers/worker_public/src/profile.ts +16 -0
- package/workers/worker_public/src/projects.ts +108 -0
- package/workers/worker_public/src/prompts.d.ts +6 -0
- package/workers/worker_public/src/quota.ts +54 -0
- package/workers/worker_public/src/reflect.ts +67 -0
- package/workers/worker_public/src/refs.ts +107 -0
- package/workers/worker_public/src/refusal.ts +65 -0
- package/workers/worker_public/src/requestScope.ts +71 -0
- package/workers/worker_public/src/research.ts +126 -0
- package/workers/worker_public/src/search.ts +56 -0
- package/workers/worker_public/src/selfquery.ts +25 -0
- package/workers/worker_public/src/session.ts +4 -0
- package/workers/worker_public/src/share.ts +53 -0
- package/workers/worker_public/src/stages/conceptGraph.ts +68 -0
- package/workers/worker_public/src/stages/conceptSteer.ts +39 -0
- package/workers/worker_public/src/stages/corpusScope.ts +25 -0
- package/workers/worker_public/src/stages/dedup.ts +10 -0
- package/workers/worker_public/src/stages/dense.ts +73 -0
- package/workers/worker_public/src/stages/diversity.ts +33 -0
- package/workers/worker_public/src/stages/editionCover.ts +63 -0
- package/workers/worker_public/src/stages/editionSteer.ts +88 -0
- package/workers/worker_public/src/stages/familyBoost.ts +22 -0
- package/workers/worker_public/src/stages/federate.ts +22 -0
- package/workers/worker_public/src/stages/glossary.ts +65 -0
- package/workers/worker_public/src/stages/graphLane.ts +31 -0
- package/workers/worker_public/src/stages/hyde.ts +29 -0
- package/workers/worker_public/src/stages/index.ts +69 -0
- package/workers/worker_public/src/stages/lexicalUnion.ts +21 -0
- package/workers/worker_public/src/stages/multiQuery.ts +57 -0
- package/workers/worker_public/src/stages/overviewDemote.ts +14 -0
- package/workers/worker_public/src/stages/poolOpen.ts +10 -0
- package/workers/worker_public/src/stages/propagate.ts +15 -0
- package/workers/worker_public/src/stages/rerank.ts +47 -0
- package/workers/worker_public/src/stages/seal.ts +16 -0
- package/workers/worker_public/src/stages/sectionDescent.ts +61 -0
- package/workers/worker_public/src/stages/stdRefNudge.ts +35 -0
- package/workers/worker_public/src/stages/subQuery.ts +42 -0
- package/workers/worker_public/src/stages/termNudge.ts +24 -0
- package/workers/worker_public/src/stages/typedPin.ts +131 -0
- package/workers/worker_public/src/stages/types.ts +112 -0
- package/workers/worker_public/src/stages/windowFloor.ts +23 -0
- package/workers/worker_public/src/structural.ts +171 -0
- package/workers/worker_public/src/tablecontext.ts +41 -0
- package/workers/worker_public/src/understand.ts +72 -0
- package/workers/worker_public/src/understandContract.ts +67 -0
- package/workers/worker_public/src/verdict.ts +255 -0
- package/workers/worker_public/tsconfig.json +18 -0
- package/workers/worker_public/wrangler.toml +104 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { embed } from "./ai";
|
|
2
|
+
import { P } from "./profile.ts";
|
|
3
|
+
import { LIMITS, MODELS, DATASETS, THRESHOLDS, processExpansion } from "./config";
|
|
4
|
+
import systemPromptText from "../prompts/system.md";
|
|
5
|
+
import conversationalPromptText from "../prompts/conversational.md";
|
|
6
|
+
import listwisePromptText from "../prompts/listwise.md";
|
|
7
|
+
import { tableContext } from "./tablecontext";
|
|
8
|
+
|
|
9
|
+
// the pinned refusal sentence lives with the canonicalizer in ./refusal
|
|
10
|
+
// (refusals are never cached: a refusal says "retrieval found nothing",
|
|
11
|
+
// which is a property of the moment, not of the question); re-exported
|
|
12
|
+
// here so the existing import surface keeps working
|
|
13
|
+
export { refusalAnswer } from "./refusal";
|
|
14
|
+
|
|
15
|
+
/** Fill {{TOKEN}} placeholders in a prompt data file. Unknown/empty tokens
|
|
16
|
+
* resolve to "" so optional lines vanish cleanly. */
|
|
17
|
+
function fill(template: string, vars: Record<string, string>): string {
|
|
18
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_m, k: string) => (k in vars ? vars[k] : ""));
|
|
19
|
+
}
|
|
20
|
+
import { QueryFilters, toVectorizeFilter } from "./selfquery";
|
|
21
|
+
import { lexicalPrefilter } from "./lexical";
|
|
22
|
+
import { positionOrder } from "./structural";
|
|
23
|
+
import { STAGES, runStages } from "./stages";
|
|
24
|
+
import type { PipelineContext, RetrieveOptions, GlossaryEntry } from "./stages/types";
|
|
25
|
+
// the chunk wire contract lives with its pydantic twin (workers/shared/
|
|
26
|
+
// chunk.ts ↔ ingest/vector_adapter.py); re-exported here so the existing
|
|
27
|
+
// import surface keeps working
|
|
28
|
+
export type { ChunkMeta, Hit } from "../../shared/chunk";
|
|
29
|
+
import type { ChunkMeta, Hit } from "../../shared/chunk";
|
|
30
|
+
import { portModelRunner } from "./env.ts";
|
|
31
|
+
|
|
32
|
+
export interface Retrieved {
|
|
33
|
+
hits: Hit[];
|
|
34
|
+
filters: QueryFilters;
|
|
35
|
+
/** vocabulary link (the L2 nomenclature bridge): top defined-term
|
|
36
|
+
* candidates for the question's subject — the answer model adjudicates
|
|
37
|
+
* among them (dense retrieval alone binds everyday words to the wrong
|
|
38
|
+
* term: measured "keeps drifting" → creep 0.69 vs durability 0.54) */
|
|
39
|
+
glossary?: GlossaryEntry[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Short follow-ups are usually elliptical ("and the limits?") — fold the
|
|
43
|
+
// previous question into the RETRIEVAL query (generation still sees the
|
|
44
|
+
// original wording). Purely structural (word count): whether a question
|
|
45
|
+
// is elliptical is a semantic judgment, and semantics belong to the
|
|
46
|
+
// understanding model, whose standalone_query takes precedence anyway.
|
|
47
|
+
export function retrievalQuery(query: string, prev?: string): string {
|
|
48
|
+
if (!prev || !prev.trim()) return query;
|
|
49
|
+
const words = query.trim().split(/\s+/).length;
|
|
50
|
+
if (words <= 8) return `${prev.trim()} — ${query.trim()}`;
|
|
51
|
+
return query;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// colloquial process questions ("how do I get a device certified to R 60")
|
|
55
|
+
// share almost no vocabulary with the B-series prose that answers them —
|
|
56
|
+
// THRESHOLDS.processExpansion is appended to the retrieval query so the
|
|
57
|
+
// window contains the certification-system documents at all
|
|
58
|
+
|
|
59
|
+
// ── the retrieval pipeline (TODO.impl/02) ────────────────────────────────
|
|
60
|
+
// retrieve() is COMPOSITION ONLY: build the context (the parallel
|
|
61
|
+
// embed/lexical prelude — Option C's latency structure is load-bearing),
|
|
62
|
+
// run the stage registry, project the result. Every mechanism lives in
|
|
63
|
+
// its own stage module under src/stages/; the registry order and the
|
|
64
|
+
// per-stage contracts are specified in docs/spec-pipeline.md.
|
|
65
|
+
export async function retrieve(
|
|
66
|
+
env: any,
|
|
67
|
+
query: string,
|
|
68
|
+
opts: RetrieveOptions = {},
|
|
69
|
+
): Promise<Retrieved> {
|
|
70
|
+
const u = opts.understanding ?? null;
|
|
71
|
+
// Filters and process-intent come ONLY from query understanding — no
|
|
72
|
+
// regex floor, no union. When understanding is unavailable the query
|
|
73
|
+
// runs unfiltered and unexpanded (vanilla retrieval); meaning is never
|
|
74
|
+
// decided by string matching.
|
|
75
|
+
// The declared context's HARD scope outranks the understanding's routing
|
|
76
|
+
// opinion: a certificate chip that says R 60:2021 keeps its dense filter
|
|
77
|
+
// even when the question's vocabulary makes the classifier emit
|
|
78
|
+
// process_intent (certification wording is the chip's subject, not the
|
|
79
|
+
// question's). Without this, dense runs unfiltered, the certification
|
|
80
|
+
// corpus floods the pool, and the pool-level seal — which must stay hard —
|
|
81
|
+
// cuts it to zero (the ctx-entity refusal, diagnosed 2026-09-08).
|
|
82
|
+
const scope: QueryFilters | null =
|
|
83
|
+
u && !u.process_intent && u.doc_number
|
|
84
|
+
? { doc_number: u.doc_number, ...(u.edition ? { edition: u.edition } : {}) }
|
|
85
|
+
: opts.sealScope
|
|
86
|
+
? { doc_number: opts.sealScope.doc_number, ...(opts.sealScope.edition ? { edition: opts.sealScope.edition } : {}) }
|
|
87
|
+
: null;
|
|
88
|
+
const filters: QueryFilters | null = scope;
|
|
89
|
+
const filter = filters ? toVectorizeFilter(filters) : null;
|
|
90
|
+
const folded = retrievalQuery(query, opts.prev);
|
|
91
|
+
let rq = opts.queryOverride?.trim() || u?.standalone_query?.trim() || folded;
|
|
92
|
+
if (u?.process_intent) rq += processExpansion();
|
|
93
|
+
// Dense embed + full-corpus BM25 prefilter in parallel (G-ETSI-1 /
|
|
94
|
+
// arXiv:2604.09868 §II-B5). Lexical must scan the whole corpus — the
|
|
95
|
+
// old keywordRank only re-ordered dense hits and could not recover
|
|
96
|
+
// exact-jargon misses. Fail-open: empty lexical list leaves dense alone.
|
|
97
|
+
// Option C: when the query is unchanged (rq === folded) the optimistic
|
|
98
|
+
// vector is already resolved — never await a fresh embed for it.
|
|
99
|
+
const vectorP =
|
|
100
|
+
rq === folded && opts.optimisticVec
|
|
101
|
+
? Promise.resolve(opts.optimisticVec)
|
|
102
|
+
: rq === folded && opts.warmEmbed
|
|
103
|
+
? opts.warmEmbed.then((w) => w ?? embed(portModelRunner(env), MODELS.embed, rq))
|
|
104
|
+
: embed(portModelRunner(env), MODELS.embed, rq);
|
|
105
|
+
const lexicalP = lexicalPrefilter(env, rq).catch(() => [] as Hit[]);
|
|
106
|
+
const [vector, lexicalHits0] = await Promise.all([vectorP, lexicalP]);
|
|
107
|
+
// The declared context's seal binds the lexical lane at the SOURCE: the
|
|
108
|
+
// RRF fusion mixes the full-corpus lexical ranking straight into the
|
|
109
|
+
// final hits — past the pool-level seal — so under a seal the lexical
|
|
110
|
+
// lane is the FAMILY's lexical hits only.
|
|
111
|
+
const lexicalHits = opts.sealScope
|
|
112
|
+
? lexicalHits0.filter((h) => h.metadata.doc_number === opts.sealScope!.doc_number && (!opts.sealScope!.edition || h.metadata.edition === opts.sealScope!.edition))
|
|
113
|
+
: lexicalHits0;
|
|
114
|
+
if (lexicalHits.length) console.log("lexical prefilter:", lexicalHits.length, "hits");
|
|
115
|
+
|
|
116
|
+
const ctx: PipelineContext = {
|
|
117
|
+
env, query, rq, folded, u, filters, filter, vector, lexicalHits,
|
|
118
|
+
matches: [], hits: [], finalHits: [], glossary: [], opts, lane: {},
|
|
119
|
+
};
|
|
120
|
+
await runStages(STAGES, ctx);
|
|
121
|
+
return {
|
|
122
|
+
hits: ctx.finalHits,
|
|
123
|
+
filters: ctx.filters ?? {},
|
|
124
|
+
...(ctx.glossary?.length ? { glossary: ctx.glossary } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface HistoryTurn {
|
|
129
|
+
role: "user" | "assistant";
|
|
130
|
+
content: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── context budget ─────────────────────────────────────────────────────
|
|
134
|
+
// The model window is finite; without a cap, a long conversation plus 8
|
|
135
|
+
// clause chunks (tables can be huge) overflows and the request FAILS
|
|
136
|
+
// outright. Everything is estimated (never exact) and stays well under
|
|
137
|
+
// the smallest context our models accept. Priority when the budget is
|
|
138
|
+
// tight: system + query always fit → newest history gets a bounded slice
|
|
139
|
+
// → passages fill the rest, best-ranked first, worst-ranked dropped.
|
|
140
|
+
|
|
141
|
+
function estTokens(s: string): number {
|
|
142
|
+
// CJK/Arabic/Indic scripts ≈ 1 token per char; Latin ≈ 1 per 4 chars
|
|
143
|
+
const wide = (s.match(/[-ۿݐ-ݿऀ-ॿ-ヿ㐀-䶿一-鿿가-]/g) || []).length;
|
|
144
|
+
return wide + Math.ceil((s.length - wide) / 4);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clipToTokens(s: string, maxTok: number): string {
|
|
148
|
+
if (maxTok < 40 || estTokens(s) <= maxTok) return s;
|
|
149
|
+
const wide = (s.match(/[-ۿݐ-ݿऀ-ॿ-ヿ㐀-䶿一-鿿가-]/g) || []).length;
|
|
150
|
+
const latinChars = Math.max(0, maxTok - wide) * 4;
|
|
151
|
+
return s.slice(0, Math.min(s.length, wide + latinChars)).trimEnd() + " …";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface BuiltMessages {
|
|
155
|
+
messages: { role: string; content: string }[];
|
|
156
|
+
usedHits: Hit[]; // passages actually included (citations must match these)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** System instruction for a conversational (non-knowledge) turn: the
|
|
160
|
+
* service facts the model speaks from, composed from the DATASETS
|
|
161
|
+
* catalog — the same SSOT /api/datasets serves. Routing is decided by
|
|
162
|
+
* query UNDERSTANDING (understanding.ts), never by string matching. */
|
|
163
|
+
export function identityNote(member: boolean): string {
|
|
164
|
+
const corpora = DATASETS().filter((d) => !d.session || member)
|
|
165
|
+
.map((d) => `- ${d.label}: ${d.description}`)
|
|
166
|
+
.join("\n");
|
|
167
|
+
const locked = DATASETS().filter((d) => d.session && !member);
|
|
168
|
+
const upsell = locked.length
|
|
169
|
+
? `Signed-in members additionally search: ${locked.map((d) => `${d.label} (${d.description})`).join("; ")}.`
|
|
170
|
+
: "";
|
|
171
|
+
return fill(conversationalPromptText, { CORPORA: corpora, UPSELL: upsell })
|
|
172
|
+
.split("\n")
|
|
173
|
+
.filter((l) => l.trim())
|
|
174
|
+
.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Split history into the turns that fit the budget slice (kept, newest)
|
|
178
|
+
* and the older ones that must be compacted into a summary (overflow). */
|
|
179
|
+
export function splitHistory(
|
|
180
|
+
history: HistoryTurn[],
|
|
181
|
+
budgetTokens: number,
|
|
182
|
+
): { kept: HistoryTurn[]; overflow: HistoryTurn[] } {
|
|
183
|
+
const historyBudget = Math.floor(budgetTokens * THRESHOLDS.historyBudgetShare);
|
|
184
|
+
let used = 0;
|
|
185
|
+
let cut = 0; // everything before `cut` overflows
|
|
186
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
187
|
+
const t = Math.min(estTokens(history[i].content), 600);
|
|
188
|
+
if (used + t > historyBudget) {
|
|
189
|
+
cut = i + 1;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
used += t;
|
|
193
|
+
}
|
|
194
|
+
return { kept: history.slice(cut), overflow: history.slice(0, cut) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
/** Final-tier LLM listwise rerank: jointly reorders the top passages for
|
|
199
|
+
* hard queries (cascade stage after the cross-encoder). Null = keep the
|
|
200
|
+
* incoming order (timeout/parse failure never blocks serving). */
|
|
201
|
+
export async function listwiseRerank(
|
|
202
|
+
env: any,
|
|
203
|
+
model: string,
|
|
204
|
+
query: string,
|
|
205
|
+
hits: Hit[],
|
|
206
|
+
): Promise<Hit[] | null> {
|
|
207
|
+
if (hits.length < 4) return null;
|
|
208
|
+
try {
|
|
209
|
+
const listing = hits
|
|
210
|
+
.map((h, i) => {
|
|
211
|
+
const label = `${h.metadata.docidentifier || h.metadata.doc_id}:${h.metadata.edition || ""} §${h.metadata.clause_anchor || ""}`;
|
|
212
|
+
return `[${i + 1}] ${label.replace(/(:|§)+$/g, "")} — ${h.text.replace(/\s+/g, " ").slice(0, 220)}`;
|
|
213
|
+
})
|
|
214
|
+
.join("\n");
|
|
215
|
+
// bounded to 2.5s: this call sits serially before generation starts —
|
|
216
|
+
// a slow reorder must never hold the first token hostage; the
|
|
217
|
+
// cross-encoder order is the fallback and is already good
|
|
218
|
+
const timeout = new Promise<null>((r) => setTimeout(() => r(null), 2500));
|
|
219
|
+
const call = (async () => {
|
|
220
|
+
const res: any = await env.AI.run(model, {
|
|
221
|
+
messages: [
|
|
222
|
+
{ role: "system", content: listwisePromptText.trimEnd() },
|
|
223
|
+
{ role: "user", content: `Question: ${query}\n\nPassages:\n${listing}` },
|
|
224
|
+
],
|
|
225
|
+
max_tokens: 700,
|
|
226
|
+
reasoning_effort: "low",
|
|
227
|
+
});
|
|
228
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
229
|
+
const m = (text ?? "").match(/\[[\s\S]*?\]/);
|
|
230
|
+
if (!m) return null;
|
|
231
|
+
const order = JSON.parse(m[0]);
|
|
232
|
+
if (!Array.isArray(order) || order.length !== hits.length) return null;
|
|
233
|
+
const idx = order.map((n: unknown) => Number(n) - 1);
|
|
234
|
+
if (idx.some((n: number) => !Number.isInteger(n) || n < 0 || n >= hits.length) || new Set(idx).size !== hits.length) return null;
|
|
235
|
+
return idx.map((n: number) => hits[n]);
|
|
236
|
+
})();
|
|
237
|
+
return await Promise.race([call, timeout]);
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function buildMessages(
|
|
244
|
+
query: string,
|
|
245
|
+
hits: Hit[],
|
|
246
|
+
lang?: string,
|
|
247
|
+
history: HistoryTurn[] = [],
|
|
248
|
+
retrievalNote?: string,
|
|
249
|
+
conversationSummary?: string,
|
|
250
|
+
budgetTokens: number = LIMITS.inputTokenBudget,
|
|
251
|
+
): BuiltMessages {
|
|
252
|
+
// per-corpus guidance travels WITH the dataset (config.ts): every
|
|
253
|
+
// dataset whose corpus appears in the passages contributes its note —
|
|
254
|
+
// new corpora need a catalog entry, never pipeline changes
|
|
255
|
+
const corpusNotes = DATASETS().filter(
|
|
256
|
+
(d) => d.note && hits.some((h) => (h.metadata as any).corpus === d.id),
|
|
257
|
+
)
|
|
258
|
+
.map((d) => d.note!)
|
|
259
|
+
.join("\n");
|
|
260
|
+
|
|
261
|
+
// the prompt itself is data (prompts/system.md); one rule per line,
|
|
262
|
+
// joined with spaces exactly as the original array form
|
|
263
|
+
const system = fill(systemPromptText, {
|
|
264
|
+
...P().prompts.vars,
|
|
265
|
+
HISTORY_CONTEXT: history.length
|
|
266
|
+
? " Earlier turns of this conversation are provided for context — answer the LATEST question, treating the passages below as the source of truth for facts and citations."
|
|
267
|
+
: "",
|
|
268
|
+
CORPUS_NOTES: corpusNotes,
|
|
269
|
+
LANG_CLAUSE: lang ? ` (explicitly requested: ${lang})` : "",
|
|
270
|
+
})
|
|
271
|
+
.split("\n")
|
|
272
|
+
.map((l) => l.trim())
|
|
273
|
+
.filter(Boolean)
|
|
274
|
+
.join(" ");
|
|
275
|
+
|
|
276
|
+
// history: newest-first into a bounded slice (oldest dropped first);
|
|
277
|
+
// each turn is clipped so accounting and content agree
|
|
278
|
+
const historyBudget = Math.floor(budgetTokens * THRESHOLDS.historyBudgetShare);
|
|
279
|
+
const keptHistory: { role: "user" | "assistant"; content: string }[] = [];
|
|
280
|
+
let historyUsed = 0;
|
|
281
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
282
|
+
const content = clipToTokens(history[i].content, 600);
|
|
283
|
+
const t = estTokens(content);
|
|
284
|
+
if (historyUsed + t > historyBudget) break;
|
|
285
|
+
keptHistory.unshift({ role: history[i].role, content });
|
|
286
|
+
historyUsed += t;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// passages: best-ranked first into whatever remains
|
|
290
|
+
const summaryBlock = conversationSummary
|
|
291
|
+
? `Earlier in this conversation (summarized for continuity):\n${conversationSummary}`
|
|
292
|
+
: "";
|
|
293
|
+
let remain =
|
|
294
|
+
budgetTokens - estTokens(system) - estTokens(retrievalNote ?? "") - estTokens(summaryBlock) - estTokens(`Question: ${query}\n\nContext passages:\n`) - historyUsed - 120; // slack for estimator error + output framing
|
|
295
|
+
const passageParts: string[] = [];
|
|
296
|
+
const usedHits: Hit[] = [];
|
|
297
|
+
// Passage label as the MODEL should cite it (it copies these into
|
|
298
|
+
// answers): drop OIML language markers, append the edition only when
|
|
299
|
+
// the identifier doesn't already carry it ("B 18:2025 (E)" + "2025" →
|
|
300
|
+
// no ":2025"; "PD-06 Edition 4" + "4" → no ":4"), and never show a
|
|
301
|
+
// producer UUID as a clause anchor — cite the clause title instead.
|
|
302
|
+
const passageLabel = (m: ChunkMeta): string => {
|
|
303
|
+
const id = (m.docidentifier || m.doc_id || "source").replace(/\s*\(([A-Z])\)\s*$/, "").trim();
|
|
304
|
+
const edition = m.edition && !id.includes(m.edition) ? ":" + m.edition : "";
|
|
305
|
+
const raw = String(m.clause_anchor ?? "");
|
|
306
|
+
const garbage = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(raw) || (raw.startsWith("_") && raw.length > 12);
|
|
307
|
+
const anchor = garbage || !raw ? "" : ` §${raw}`;
|
|
308
|
+
return `${id}${edition}${anchor}`;
|
|
309
|
+
};
|
|
310
|
+
// passages in document reading order (FABLE NodeFusion): same-doc
|
|
311
|
+
// clauses read top-to-bottom, docs by best rank — synthesis quality
|
|
312
|
+
// depends on arrangement, not just the selected set
|
|
313
|
+
for (const h of positionOrder(hits)) {
|
|
314
|
+
const st = h.metadata.status === "withdrawn" || h.metadata.status === "superseded" ? ` [${h.metadata.status}]` : "";
|
|
315
|
+
const label = `${passageLabel(h.metadata)}${st}`;
|
|
316
|
+
// answer contract v2: typed passages declare their unit id so the
|
|
317
|
+
// model can reference [[u:<id>]] instead of retyping the object
|
|
318
|
+
const unitTag = (h.metadata as any).unit_id ? ` unit ${(h.metadata as any).unit_id}${(h.metadata as any).block ? ` (${(h.metadata as any).block})` : ""}` : "";
|
|
319
|
+
const head = `[${usedHits.length + 1}] ${label}${unitTag} ${h.metadata.clause_title ? "— " + h.metadata.clause_title : ""}\n`;
|
|
320
|
+
// tables: schema-aware pruning from the producer payload; the
|
|
321
|
+
// stored text is the fallback (pruning never goes below baseline)
|
|
322
|
+
const pruned = (h.metadata as any).block === "table" ? tableContext(h.metadata, query) : null;
|
|
323
|
+
const body = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
|
|
324
|
+
const t = estTokens(head) + estTokens(body);
|
|
325
|
+
if (t <= remain) {
|
|
326
|
+
passageParts.push(head + body);
|
|
327
|
+
usedHits.push(h);
|
|
328
|
+
remain -= t;
|
|
329
|
+
} else if (usedHits.length < 2) {
|
|
330
|
+
// always keep at least the two best passages, truncated to fit
|
|
331
|
+
passageParts.push(head + clipToTokens(h.text, Math.max(150, remain - estTokens(head))));
|
|
332
|
+
usedHits.push(h);
|
|
333
|
+
remain = 0;
|
|
334
|
+
break;
|
|
335
|
+
} else break;
|
|
336
|
+
}
|
|
337
|
+
const context = passageParts.join("\n\n") || "(no passages)";
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
messages: [
|
|
341
|
+
{ role: "system", content: system },
|
|
342
|
+
...(retrievalNote ? [{ role: "system", content: retrievalNote }] : []),
|
|
343
|
+
...(summaryBlock ? [{ role: "system", content: summaryBlock }] : []),
|
|
344
|
+
...keptHistory.map((h) => ({ role: h.role, content: h.content })),
|
|
345
|
+
{ role: "user", content: `Question: ${query}\n\nContext passages:\n${context}` },
|
|
346
|
+
],
|
|
347
|
+
usedHits,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function oimlPublicationUrl(meta: ChunkMeta): string | undefined {
|
|
352
|
+
if (!meta.doctype || !meta.doc_number) return undefined;
|
|
353
|
+
const typeMap: Record<string, string> = { R: "r", D: "d", B: "b", G: "g", E: "e" };
|
|
354
|
+
const t = typeMap[meta.doctype];
|
|
355
|
+
if (!t) return undefined;
|
|
356
|
+
return `https://www.oiml.org/en/publications/${t}${meta.doc_number}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function citations(hits: Hit[]) {
|
|
360
|
+
const rank = (s?: string) => (s === "in-force" || s === "joint" ? 0 : s === "unknown" || !s ? 1 : 2);
|
|
361
|
+
return [...hits]
|
|
362
|
+
.map((h) => ({
|
|
363
|
+
doc_id: h.metadata.doc_id,
|
|
364
|
+
docidentifier: h.metadata.docidentifier,
|
|
365
|
+
edition: h.metadata.edition,
|
|
366
|
+
language: h.metadata.language,
|
|
367
|
+
clause_anchor: h.metadata.clause_anchor,
|
|
368
|
+
clause_title: h.metadata.clause_title,
|
|
369
|
+
status: h.metadata.status ?? "unknown",
|
|
370
|
+
superseded_by: h.metadata.superseded_by || undefined,
|
|
371
|
+
corpus: h.metadata.corpus || "oiml",
|
|
372
|
+
url: oimlPublicationUrl(h.metadata),
|
|
373
|
+
snippet: h.text.slice(0, 400),
|
|
374
|
+
score: h.rerank_score ?? h.score,
|
|
375
|
+
}))
|
|
376
|
+
.sort((a, b) => rank(a.status) - rank(b.status)); // in-force first, withdrawn last
|
|
377
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Blobs — immutable object storage (unit assets, rendered documents).
|
|
2
|
+
* Every mainstream provider speaks S3; the reference adapter speaks
|
|
3
|
+
* R2's binding API. Objects are content-typed and read by key. */
|
|
4
|
+
export interface Blobs {
|
|
5
|
+
get(key: string): Promise<{ body: ReadableStream | null; contentType?: string } | null>;
|
|
6
|
+
put(key: string, value: string | ReadableStream | ArrayBuffer, contentType?: string): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** The Cloudflare reference adapters — the ONLY module where provider
|
|
2
|
+
* APIs appear. Domain modules import the port interfaces; the purity
|
|
3
|
+
* lint (npm run lint:ports) fails any other file that names a
|
|
4
|
+
* provider binding type. Zero runtime change: each adapter forwards
|
|
5
|
+
* to the binding the call sites used to touch directly. */
|
|
6
|
+
import type { ModelRunner, GenerateRequest, GenerateResult } from "../model.ts";
|
|
7
|
+
import type { VectorIndex, VectorMatch, VectorQuery, VectorUpsert } from "../vector.ts";
|
|
8
|
+
import type { Kv } from "../kv.ts";
|
|
9
|
+
import type { Blobs } from "../blobs.ts";
|
|
10
|
+
import type { Runtime } from "../runtime.ts";
|
|
11
|
+
|
|
12
|
+
// Embedding request/response shapes vary across model generations; the
|
|
13
|
+
// probes below try the known shapes in order and remember the winner
|
|
14
|
+
// for the isolate's lifetime. Every wrong-shape attempt burns AI rate
|
|
15
|
+
// budget, so the verified shape goes first. Total failure THROWS — an
|
|
16
|
+
// empty vector would surface downstream as an opaque Vectorize 40006
|
|
17
|
+
// (the 2026-09-14 regression: a probe-less adapter returned [] and the
|
|
18
|
+
// index rejected the 0-dimension query).
|
|
19
|
+
|
|
20
|
+
type EmbedRequestShape = (texts: string[]) => unknown;
|
|
21
|
+
|
|
22
|
+
const EMBED_REQUEST_SHAPES: Record<string, EmbedRequestShape> = {
|
|
23
|
+
// "text" first: the verified request shape for qwen3-embedding-0.6b
|
|
24
|
+
text: (texts) => ({ text: texts }),
|
|
25
|
+
"input.input": (texts) => ({ input: { input: texts } }),
|
|
26
|
+
array: (texts) => ({ input: texts }),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let embedRequestWinner: string | null = null;
|
|
30
|
+
|
|
31
|
+
function extractVecBatch(res: unknown, n: number): number[][] | null {
|
|
32
|
+
const r = res as any;
|
|
33
|
+
const d = r?.data ?? r?.result?.data;
|
|
34
|
+
const rows = Array.isArray(d) ? d : Array.isArray(r?.embedding) ? [r.embedding] : null;
|
|
35
|
+
if (!rows) return null;
|
|
36
|
+
const out: number[][] = [];
|
|
37
|
+
for (const row of rows.slice(0, n)) {
|
|
38
|
+
const vec = Array.isArray(row) ? row : Array.isArray(row?.embedding) ? row.embedding : null;
|
|
39
|
+
if (!vec || vec.length === 0) return null;
|
|
40
|
+
out.push(vec.map(Number));
|
|
41
|
+
}
|
|
42
|
+
return out.length === n ? out : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const by20 = <T>(xs: T[]): T[][] => {
|
|
46
|
+
const out: T[][] = [];
|
|
47
|
+
for (let i = 0; i < xs.length; i += 20) out.push(xs.slice(i, i + 20));
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const RERANK_SHAPES = (query: string, texts: string[]) => [
|
|
52
|
+
{ query, contexts: texts.map((t) => ({ text: t })) },
|
|
53
|
+
{ query, contexts: texts },
|
|
54
|
+
{ query, candidates: texts.map((t, i) => ({ id: String(i), text: t })) },
|
|
55
|
+
{ query, passages: texts },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export function cfModelRunner(ai: unknown): ModelRunner {
|
|
59
|
+
const A = ai as any;
|
|
60
|
+
return {
|
|
61
|
+
async embed(texts: string[]) {
|
|
62
|
+
const order = embedRequestWinner ? [embedRequestWinner] : Object.keys(EMBED_REQUEST_SHAPES);
|
|
63
|
+
for (const name of order) {
|
|
64
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await A.run("@cf/qwen/qwen3-embedding-0.6b", EMBED_REQUEST_SHAPES[name](texts));
|
|
67
|
+
const vecs = extractVecBatch(res, texts.length);
|
|
68
|
+
if (vecs) {
|
|
69
|
+
embedRequestWinner = name;
|
|
70
|
+
return vecs;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// retry the same shape, then fall through to the next
|
|
74
|
+
}
|
|
75
|
+
await new Promise((r) => setTimeout(r, 250 * (attempt + 1)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`embedding failed for all request shapes (${texts.length} text(s))`);
|
|
79
|
+
},
|
|
80
|
+
async rerank(model, query, texts) {
|
|
81
|
+
for (const body of RERANK_SHAPES(query, texts)) {
|
|
82
|
+
try {
|
|
83
|
+
const res: any = await A.run(model, body);
|
|
84
|
+
const raw = res?.data ?? res?.result?.data ?? res?.response;
|
|
85
|
+
if (!Array.isArray(raw)) continue;
|
|
86
|
+
const scores: number[] = new Array(texts.length).fill(NaN);
|
|
87
|
+
raw.forEach((x: any, i: number) => {
|
|
88
|
+
if (typeof x === "number") {
|
|
89
|
+
scores[i] = x;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const id = Number(x?.id ?? x?.index ?? i);
|
|
93
|
+
const s = Number(x?.score ?? x?.relevance_score);
|
|
94
|
+
if (Number.isInteger(id) && id >= 0 && id < texts.length && Number.isFinite(s)) scores[id] = s;
|
|
95
|
+
});
|
|
96
|
+
if (scores.some((s) => Number.isFinite(s))) return scores;
|
|
97
|
+
} catch {
|
|
98
|
+
// next shape
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
},
|
|
103
|
+
async run(req: GenerateRequest): Promise<GenerateResult> {
|
|
104
|
+
const res: any = await (ai as any).run(req.model, {
|
|
105
|
+
messages: req.messages,
|
|
106
|
+
max_tokens: req.maxTokens,
|
|
107
|
+
...(req.effort ? { reasoning_effort: req.effort } : {}),
|
|
108
|
+
...(req.temperature != null ? { temperature: req.temperature } : {}),
|
|
109
|
+
...(req.topP != null ? { top_p: req.topP } : {}),
|
|
110
|
+
...(req.topK != null ? { top_k: req.topK } : {}),
|
|
111
|
+
...(req.stream ? { stream: true } : {}),
|
|
112
|
+
});
|
|
113
|
+
if (req.stream && res && typeof res.getReader === "function") return { text: null, stream: res };
|
|
114
|
+
if (req.stream && res?.body && typeof res.body.getReader === "function") return { text: null, stream: res.body };
|
|
115
|
+
const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
|
|
116
|
+
return { text: typeof text === "string" ? text : null };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function cfVectorIndex(index: unknown): VectorIndex {
|
|
122
|
+
const ix = index as any;
|
|
123
|
+
return {
|
|
124
|
+
async query(q: VectorQuery) {
|
|
125
|
+
const r = await ix.query(q.vector, {
|
|
126
|
+
topK: q.topK,
|
|
127
|
+
returnMetadata: "all",
|
|
128
|
+
...(q.filter ? { filter: q.filter } : {}),
|
|
129
|
+
});
|
|
130
|
+
return (r.matches ?? r).map((m: any) => ({ id: m.id, score: m.score, metadata: m.metadata ?? null }));
|
|
131
|
+
},
|
|
132
|
+
async upsert(vectors: VectorUpsert[]) {
|
|
133
|
+
// the adapter law: batches above the provider cap go in chunks
|
|
134
|
+
for (const group of by20(vectors)) await ix.upsert(group);
|
|
135
|
+
},
|
|
136
|
+
async getByIds(ids: string[]) {
|
|
137
|
+
// getByIds above ~20 returns EMPTY silently — chunk, always
|
|
138
|
+
const out: VectorMatch[] = [];
|
|
139
|
+
for (const group of by20(ids)) {
|
|
140
|
+
const got = await ix.getByIds(group);
|
|
141
|
+
out.push(...(got ?? []).map((m: any) => ({ id: m.id, score: 0, metadata: m.metadata ?? null })));
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function cfKv(ns: unknown): Kv {
|
|
149
|
+
const kv = ns as any;
|
|
150
|
+
return {
|
|
151
|
+
get: (key) => kv.get(key),
|
|
152
|
+
put: (key, value, opts) => kv.put(key, value, opts?.ttlSec ? { expirationTtl: opts.ttlSec } : undefined),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function cfBlobs(bucket: unknown): Blobs {
|
|
157
|
+
const r2 = bucket as any;
|
|
158
|
+
return {
|
|
159
|
+
async get(key) {
|
|
160
|
+
const obj = await r2.get(key);
|
|
161
|
+
if (!obj) return null;
|
|
162
|
+
return { body: obj.body, contentType: obj.httpMetadata?.contentType };
|
|
163
|
+
},
|
|
164
|
+
async put(key, value, contentType) {
|
|
165
|
+
await r2.put(key, value, contentType ? { httpMetadata: { contentType } } : undefined);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function cfRuntime(ctx: { waitUntil(p: Promise<unknown>): void } | undefined): Runtime {
|
|
171
|
+
return {
|
|
172
|
+
defer(fn) {
|
|
173
|
+
if (ctx) ctx.waitUntil(fn());
|
|
174
|
+
else void fn();
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Kv — the hot read path's get/put with TTL. Ephemeral by contract:
|
|
2
|
+
* answer caches and quota counters treat this as disposable state
|
|
3
|
+
* (generation stamps + TTLs), so any adapter that honors the two
|
|
4
|
+
* operations is conformant. */
|
|
5
|
+
export interface Kv {
|
|
6
|
+
get(key: string): Promise<string | null>;
|
|
7
|
+
put(key: string, value: string, opts?: { ttlSec?: number }): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** ModelRunner — the port over every model call. Adapters translate
|
|
2
|
+
* into provider catalogs and own provider-specific call shaping:
|
|
3
|
+
* request-shape probing (rerank variants), sampling quirks, the
|
|
4
|
+
* effort-budget coupling. Domain modules never see a provider body.
|
|
5
|
+
*
|
|
6
|
+
* multi-SDO / multi-cloud: docs/konneal-extraction-plan.md §5. */
|
|
7
|
+
export interface GenerateRequest {
|
|
8
|
+
model: string;
|
|
9
|
+
messages: unknown[];
|
|
10
|
+
effort?: string;
|
|
11
|
+
maxTokens?: number;
|
|
12
|
+
temperature?: number;
|
|
13
|
+
topP?: number;
|
|
14
|
+
topK?: number;
|
|
15
|
+
stream?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface GenerateResult {
|
|
18
|
+
text: string | null;
|
|
19
|
+
stream?: ReadableStream<Uint8Array> | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ModelRunner {
|
|
23
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
24
|
+
/** rerank with adapter-side request-shape probing; returns one score
|
|
25
|
+
* per text, or null when the provider degraded (caller falls back) */
|
|
26
|
+
rerank(model: string, query: string, texts: string[]): Promise<number[] | null>;
|
|
27
|
+
run(req: GenerateRequest): Promise<GenerateResult>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Runtime — background work after the response. The semantic is
|
|
2
|
+
* fire-and-forget best effort: adapters may await or schedule; the
|
|
3
|
+
* domain must never depend on completion for correctness. */
|
|
4
|
+
export interface Runtime {
|
|
5
|
+
defer(fn: () => Promise<void>): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Background — the raw waitUntil surface. ExecutionContext satisfies
|
|
9
|
+
* this structurally; telemetry and other edge utilities depend on the
|
|
10
|
+
* shape, never on the provider type. */
|
|
11
|
+
export interface Background {
|
|
12
|
+
waitUntil(p: Promise<unknown>): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** StoreQuery — the transitional relational port. The engine's
|
|
2
|
+
* repositories speak prepared statements (prepare/bind/first/all/
|
|
3
|
+
* batch); the Cloudflare adapter exposes D1 directly, and a Postgres
|
|
4
|
+
* adapter implements the same five methods over its driver. When the
|
|
5
|
+
* repository extraction completes, this narrows to semantic
|
|
6
|
+
* repositories (documents, sessions, memories…) and the SQL dialect
|
|
7
|
+
* disappears from the port entirely. */
|
|
8
|
+
export interface StoreQuery {
|
|
9
|
+
prepare(sql: string): {
|
|
10
|
+
bind(...values: unknown[]): { first<T = unknown>(): Promise<T | null>; all<T = unknown>(): Promise<{ results: T[] }>; run(): Promise<unknown> };
|
|
11
|
+
first<T = unknown>(): Promise<T | null>;
|
|
12
|
+
all<T = unknown>(): Promise<{ results: T[] }>;
|
|
13
|
+
run(): Promise<unknown>;
|
|
14
|
+
};
|
|
15
|
+
batch(statements: unknown[]): Promise<unknown[]>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// the glossary lane's vector probe: the port view of a lane index
|
|
19
|
+
import type { VectorIndex } from "./vector.ts";
|
|
20
|
+
export type { VectorIndex };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** VectorIndex — dense search with metadata filters. The port contract
|
|
2
|
+
* IS the wire law: metadata values are scalars or string arrays
|
|
3
|
+
* (API error 40017 otherwise); getByIds returns empty above its
|
|
4
|
+
* batch cap, so adapters chunk. Lexical search stays ENGINE-side —
|
|
5
|
+
* adapters need only dense+filters, the weakest common denominator. */
|
|
6
|
+
export interface VectorMatch {
|
|
7
|
+
id: string;
|
|
8
|
+
score: number;
|
|
9
|
+
metadata: Record<string, unknown> | null;
|
|
10
|
+
}
|
|
11
|
+
export interface VectorQuery {
|
|
12
|
+
vector: number[];
|
|
13
|
+
topK: number;
|
|
14
|
+
filter?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface VectorUpsert {
|
|
17
|
+
id: string;
|
|
18
|
+
values: number[];
|
|
19
|
+
metadata: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VectorIndex {
|
|
23
|
+
query(q: VectorQuery): Promise<VectorMatch[]>;
|
|
24
|
+
upsert(vectors: VectorUpsert[]): Promise<void>;
|
|
25
|
+
getByIds(ids: string[]): Promise<VectorMatch[]>;
|
|
26
|
+
}
|