jev-web 0.3.3 → 0.4.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/README.md +413 -201
- package/docs/demo-negation.png +0 -0
- package/docs/demo-typed.png +0 -0
- package/docs/demo.gif +0 -0
- package/package.json +60 -55
- package/src/answers.mjs +36 -36
- package/src/bekko.mjs +391 -0
- package/src/config.mjs +65 -65
- package/src/encode.mjs +83 -83
- package/src/index.mjs +83 -42
- package/src/laya.mjs +630 -625
- package/src/questions.mjs +39 -39
- package/src/registry.mjs +129 -102
- package/src/session.mjs +151 -151
- package/src/strands.mjs +535 -0
package/src/questions.mjs
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
// jev-web — question normalization for typed-decision models.
|
|
2
|
-
//
|
|
3
|
-
// The model answers three question kinds:
|
|
4
|
-
// choice — pick one of 2..255 options
|
|
5
|
-
// score — ordered levels (2..10); the answer is the expected level index
|
|
6
|
-
// noul — yes/no; the answer is p(yes)
|
|
7
|
-
|
|
8
|
-
export const QUESTION_TYPES = new Set(["choice", "score", "noul"]);
|
|
9
|
-
export const NOUL_OPTIONS = ["no", "yes"];
|
|
10
|
-
|
|
11
|
-
export function normalizeQuestions(questions) {
|
|
12
|
-
if (!Array.isArray(questions) || questions.length === 0) {
|
|
13
|
-
throw new TypeError("questions must be a non-empty array");
|
|
14
|
-
}
|
|
15
|
-
return questions.map((q, i) => {
|
|
16
|
-
const type = q?.type;
|
|
17
|
-
if (!QUESTION_TYPES.has(type)) {
|
|
18
|
-
throw new TypeError(`questions[${i}].type must be one of ${[...QUESTION_TYPES].join("|")}`);
|
|
19
|
-
}
|
|
20
|
-
const instructions = String(q?.instructions ?? "").trim();
|
|
21
|
-
if (!instructions) throw new TypeError(`questions[${i}].instructions is required`);
|
|
22
|
-
|
|
23
|
-
let options;
|
|
24
|
-
if (type === "noul") {
|
|
25
|
-
options = Array.isArray(q.options) && q.options.length ? q.options.map(String) : [...NOUL_OPTIONS];
|
|
26
|
-
if (options.length !== 2) throw new TypeError(`questions[${i}]: noul takes exactly 2 options`);
|
|
27
|
-
} else if (type === "choice") {
|
|
28
|
-
options = (q.options ?? []).map(String);
|
|
29
|
-
if (options.length < 2) throw new TypeError(`questions[${i}]: choice needs at least 2 options`);
|
|
30
|
-
if (options.length > 255) throw new TypeError(`questions[${i}]: choice supports at most 255 options`);
|
|
31
|
-
} else {
|
|
32
|
-
options = (q.options ?? []).map(String);
|
|
33
|
-
if (options.length < 2 || options.length > 10) {
|
|
34
|
-
throw new TypeError(`questions[${i}]: score needs 2-10 ordered levels`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { type, instructions, options };
|
|
38
|
-
});
|
|
39
|
-
}
|
|
1
|
+
// jev-web — question normalization for typed-decision models.
|
|
2
|
+
//
|
|
3
|
+
// The model answers three question kinds:
|
|
4
|
+
// choice — pick one of 2..255 options
|
|
5
|
+
// score — ordered levels (2..10); the answer is the expected level index
|
|
6
|
+
// noul — yes/no; the answer is p(yes)
|
|
7
|
+
|
|
8
|
+
export const QUESTION_TYPES = new Set(["choice", "score", "noul"]);
|
|
9
|
+
export const NOUL_OPTIONS = ["no", "yes"];
|
|
10
|
+
|
|
11
|
+
export function normalizeQuestions(questions) {
|
|
12
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
13
|
+
throw new TypeError("questions must be a non-empty array");
|
|
14
|
+
}
|
|
15
|
+
return questions.map((q, i) => {
|
|
16
|
+
const type = q?.type;
|
|
17
|
+
if (!QUESTION_TYPES.has(type)) {
|
|
18
|
+
throw new TypeError(`questions[${i}].type must be one of ${[...QUESTION_TYPES].join("|")}`);
|
|
19
|
+
}
|
|
20
|
+
const instructions = String(q?.instructions ?? "").trim();
|
|
21
|
+
if (!instructions) throw new TypeError(`questions[${i}].instructions is required`);
|
|
22
|
+
|
|
23
|
+
let options;
|
|
24
|
+
if (type === "noul") {
|
|
25
|
+
options = Array.isArray(q.options) && q.options.length ? q.options.map(String) : [...NOUL_OPTIONS];
|
|
26
|
+
if (options.length !== 2) throw new TypeError(`questions[${i}]: noul takes exactly 2 options`);
|
|
27
|
+
} else if (type === "choice") {
|
|
28
|
+
options = (q.options ?? []).map(String);
|
|
29
|
+
if (options.length < 2) throw new TypeError(`questions[${i}]: choice needs at least 2 options`);
|
|
30
|
+
if (options.length > 255) throw new TypeError(`questions[${i}]: choice supports at most 255 options`);
|
|
31
|
+
} else {
|
|
32
|
+
options = (q.options ?? []).map(String);
|
|
33
|
+
if (options.length < 2 || options.length > 10) {
|
|
34
|
+
throw new TypeError(`questions[${i}]: score needs 2-10 ordered levels`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { type, instructions, options, ...(q?.criteria !== undefined ? { criteria: q.criteria } : {}) };
|
|
38
|
+
});
|
|
39
|
+
}
|
package/src/registry.mjs
CHANGED
|
@@ -1,102 +1,129 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* registry.mjs — multi-family runtime for JEV-style typed-decision models.
|
|
3
|
-
*
|
|
4
|
-
* jev-web ships
|
|
5
|
-
* ModernBERT encoder + typed head
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
/**
|
|
2
|
+
* registry.mjs — multi-family runtime for JEV-style typed-decision models.
|
|
3
|
+
*
|
|
4
|
+
* jev-web ships three families today (open-jev's fused DeBERTa graph, Laya's
|
|
5
|
+
* ModernBERT encoder + typed head, Strands Decider 2B's Qwen torso + pointer
|
|
6
|
+
* head, and Bekko System One v0's cross-encoder) and any third party can add
|
|
7
|
+
* its own without touching core:
|
|
8
|
+
*
|
|
9
|
+
* registerDecisionFamily("my-model", {
|
|
10
|
+
* defaults: { model: "org/my-typed-decision-ONNX", revision: "main" },
|
|
11
|
+
* create: async (opts) => ({ info, decide(state, questions), dispose? }),
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* const runtime = await createDecisionRuntime({ family: "my-model" });
|
|
15
|
+
*
|
|
16
|
+
* `createDecisionRuntime` tries the requested family, then any `fallback`
|
|
17
|
+
* families in order, reporting each failure through `onFallback`. The returned
|
|
18
|
+
* object always exposes the shared contract:
|
|
19
|
+
* { info: { family, ... }, decide(state, questions) -> DecideResult, dispose? }
|
|
20
|
+
* where DecideResult is `{ answers, truncated, length, prompts?, timings? }`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createDecider } from "./session.mjs";
|
|
24
|
+
import { DEFAULT_MODEL, DEFAULT_REVISION } from "./config.mjs";
|
|
25
|
+
import {
|
|
26
|
+
createLayaDecider,
|
|
27
|
+
LAYA_DEFAULT_MODEL,
|
|
28
|
+
LAYA_DEFAULT_REVISION,
|
|
29
|
+
LAYA_DEFAULT_SUBFOLDER,
|
|
30
|
+
} from "./laya.mjs";
|
|
31
|
+
import {
|
|
32
|
+
createStrandsDecider,
|
|
33
|
+
STRANDS_DEFAULT_MODEL,
|
|
34
|
+
STRANDS_DEFAULT_REVISION,
|
|
35
|
+
} from "./strands.mjs";
|
|
36
|
+
import {
|
|
37
|
+
createBekkoDecider,
|
|
38
|
+
BEKKO_DEFAULT_MODEL,
|
|
39
|
+
BEKKO_DEFAULT_REVISION,
|
|
40
|
+
} from "./bekko.mjs";
|
|
41
|
+
|
|
42
|
+
const families = new Map();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register (or replace) a decision family.
|
|
46
|
+
* @param {string} id short stable identifier, e.g. "open-jev"
|
|
47
|
+
* @param {{create:(opts:object)=>Promise<object>, defaults?:object}} descriptor
|
|
48
|
+
*/
|
|
49
|
+
export function registerDecisionFamily(id, descriptor) {
|
|
50
|
+
if (!id || typeof id !== "string") throw new TypeError("registerDecisionFamily: id is required");
|
|
51
|
+
if (!descriptor || typeof descriptor.create !== "function") {
|
|
52
|
+
throw new TypeError(`registerDecisionFamily(${id}): descriptor.create is required`);
|
|
53
|
+
}
|
|
54
|
+
families.set(id, { defaults: {}, ...descriptor });
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @returns {string[]} registered family ids in registration order. */
|
|
59
|
+
export function listDecisionFamilies() {
|
|
60
|
+
return [...families.keys()];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getDecisionFamily(id) {
|
|
64
|
+
return families.get(id) ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
registerDecisionFamily("open-jev", {
|
|
68
|
+
defaults: { model: DEFAULT_MODEL, revision: DEFAULT_REVISION },
|
|
69
|
+
create: createDecider,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
registerDecisionFamily("laya", {
|
|
73
|
+
defaults: {
|
|
74
|
+
model: LAYA_DEFAULT_MODEL,
|
|
75
|
+
revision: LAYA_DEFAULT_REVISION,
|
|
76
|
+
subfolder: LAYA_DEFAULT_SUBFOLDER,
|
|
77
|
+
},
|
|
78
|
+
create: createLayaDecider,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
registerDecisionFamily("strands-decider", {
|
|
82
|
+
defaults: {
|
|
83
|
+
model: STRANDS_DEFAULT_MODEL,
|
|
84
|
+
revision: STRANDS_DEFAULT_REVISION,
|
|
85
|
+
},
|
|
86
|
+
create: createStrandsDecider,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
registerDecisionFamily("bekko", {
|
|
90
|
+
defaults: {
|
|
91
|
+
model: BEKKO_DEFAULT_MODEL,
|
|
92
|
+
revision: BEKKO_DEFAULT_REVISION,
|
|
93
|
+
},
|
|
94
|
+
create: createBekkoDecider,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a decision runtime, preferring `family` and falling back in order.
|
|
99
|
+
* @param {{family?:string, fallback?:string[], onFallback?:Function}} [opts]
|
|
100
|
+
* @returns {Promise<{info:object, decide:Function, dispose?:Function}>}
|
|
101
|
+
*/
|
|
102
|
+
export async function createDecisionRuntime({
|
|
103
|
+
family = "laya",
|
|
104
|
+
fallback = [],
|
|
105
|
+
onFallback = null,
|
|
106
|
+
...opts
|
|
107
|
+
} = {}) {
|
|
108
|
+
const order = [family, ...(Array.isArray(fallback) ? fallback : [])].filter(Boolean);
|
|
109
|
+
let lastError = null;
|
|
110
|
+
for (const id of order) {
|
|
111
|
+
const descriptor = families.get(id);
|
|
112
|
+
if (!descriptor) {
|
|
113
|
+
lastError = new Error(`unknown decision family "${id}"`);
|
|
114
|
+
onFallback?.({ family: id, error: lastError });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const runtime = await descriptor.create({ ...descriptor.defaults, ...opts });
|
|
119
|
+
return {
|
|
120
|
+
...runtime,
|
|
121
|
+
info: { family: id, ...(runtime?.info ?? {}) },
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
lastError = error;
|
|
125
|
+
onFallback?.({ family: id, error });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw lastError ?? new Error("createDecisionRuntime: no family could be created");
|
|
129
|
+
}
|
package/src/session.mjs
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
// jev-web — decider session: tokenizer + model + decide().
|
|
2
|
-
//
|
|
3
|
-
// transformers.js is an optional peer dependency, imported lazily so the
|
|
4
|
-
// package works anywhere and hosts can inject their own copy (tests, custom
|
|
5
|
-
// builds). Everything heavy stays out of module scope.
|
|
6
|
-
|
|
7
|
-
import { normalizeQuestions } from "./questions.mjs";
|
|
8
|
-
|
|
9
|
-
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
10
|
-
import { buildDecisionInput, typedMarkerIds } from "./encode.mjs";
|
|
11
|
-
import { answersFromScores } from "./answers.mjs";
|
|
12
|
-
import {
|
|
13
|
-
DEFAULT_MODEL,
|
|
14
|
-
DEFAULT_REVISION,
|
|
15
|
-
loadModelConfig,
|
|
16
|
-
resolveDevice,
|
|
17
|
-
resolveDtype,
|
|
18
|
-
detectWebGPU,
|
|
19
|
-
} from "./config.mjs";
|
|
20
|
-
|
|
21
|
-
export async function createDecider({
|
|
22
|
-
model = DEFAULT_MODEL,
|
|
23
|
-
revision = DEFAULT_REVISION,
|
|
24
|
-
dtype = "auto",
|
|
25
|
-
device = "auto",
|
|
26
|
-
temperature = null,
|
|
27
|
-
maxStateTokens = null,
|
|
28
|
-
onProgress = null,
|
|
29
|
-
transformers = null,
|
|
30
|
-
fetchImpl = fetch,
|
|
31
|
-
scope = globalThis,
|
|
32
|
-
// Directory (or CDN base) holding the onnxruntime-web wasm/loader files.
|
|
33
|
-
// Bundlers usually cannot ship the 26 MB asyncify wasm under static-host
|
|
34
|
-
// file-size limits; hosts that self-host a compressed copy set this to
|
|
35
|
-
// e.g. "/assets/". Defaults to the runtime's own resolution.
|
|
36
|
-
wasmPaths = null,
|
|
37
|
-
} = {}) {
|
|
38
|
-
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
39
|
-
if (wasmPaths) {
|
|
40
|
-
try {
|
|
41
|
-
tf.env.backends.onnx.wasm.wasmPaths = wasmPaths;
|
|
42
|
-
} catch { /* older runtime: ignore */ }
|
|
43
|
-
}
|
|
44
|
-
const remote = await loadModelConfig({ model, revision, fetchImpl });
|
|
45
|
-
const resolvedDevice = resolveDevice({ device, webgpu: await detectWebGPU(scope) });
|
|
46
|
-
const resolvedDtype = resolveDtype(resolvedDevice, dtype);
|
|
47
|
-
|
|
48
|
-
const report = (phase) => (p) =>
|
|
49
|
-
onProgress?.({
|
|
50
|
-
phase,
|
|
51
|
-
status: p?.status ?? null,
|
|
52
|
-
file: p?.file ?? null,
|
|
53
|
-
loaded: p?.loaded ?? null,
|
|
54
|
-
total: p?.total ?? null,
|
|
55
|
-
progress: p?.progress ?? null,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const tokenizer = await tf.AutoTokenizer.from_pretrained(model, {
|
|
59
|
-
revision,
|
|
60
|
-
progress_callback: report("tokenizer"),
|
|
61
|
-
});
|
|
62
|
-
const net = await tf.AutoModel.from_pretrained(model, {
|
|
63
|
-
revision,
|
|
64
|
-
dtype: resolvedDtype,
|
|
65
|
-
device: resolvedDevice,
|
|
66
|
-
progress_callback: report("model"),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const encode = (text) => Array.from(tokenizer(text, { add_special_tokens: false }).input_ids.data, Number);
|
|
70
|
-
const markers = typedMarkerIds(encode, { markers: remote.markers });
|
|
71
|
-
|
|
72
|
-
const effective = {
|
|
73
|
-
temperature: Number.isFinite(temperature) ? temperature : remote.temperature,
|
|
74
|
-
maxStateTokens: Number.isFinite(maxStateTokens) ? maxStateTokens : remote.maxStateTokens,
|
|
75
|
-
maxLen: remote.maxLen,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const i64 = (values, dims) => new tf.Tensor("int64", BigInt64Array.from(values, BigInt), dims);
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
info: {
|
|
82
|
-
family: "open-jev",
|
|
83
|
-
model,
|
|
84
|
-
revision,
|
|
85
|
-
device: resolvedDevice,
|
|
86
|
-
dtype: resolvedDtype,
|
|
87
|
-
temperature: effective.temperature,
|
|
88
|
-
maxStateTokens: effective.maxStateTokens,
|
|
89
|
-
maxLen: effective.maxLen,
|
|
90
|
-
pool: remote.pool,
|
|
91
|
-
configSource: remote.source,
|
|
92
|
-
},
|
|
93
|
-
async decide(state, questions) {
|
|
94
|
-
const t0 = now();
|
|
95
|
-
const normalized = normalizeQuestions(questions);
|
|
96
|
-
const tokenized = normalized.map((q) => ({
|
|
97
|
-
type: q.type,
|
|
98
|
-
instructionIds: encode(q.instructions),
|
|
99
|
-
optionIds: q.options.map((option) => encode(option)),
|
|
100
|
-
}));
|
|
101
|
-
const stateIds = encode(String(state ?? ""));
|
|
102
|
-
const input = buildDecisionInput({
|
|
103
|
-
stateIds,
|
|
104
|
-
markers,
|
|
105
|
-
questions: tokenized,
|
|
106
|
-
maxStateTokens: effective.maxStateTokens,
|
|
107
|
-
maxLen: effective.maxLen,
|
|
108
|
-
});
|
|
109
|
-
const feeds = {
|
|
110
|
-
input_ids: i64(input.inputIds, [1, input.length]),
|
|
111
|
-
attention_mask: i64(input.inputIds.map(() => 1), [1, input.length]),
|
|
112
|
-
seg: i64(input.seg, [1, input.length]),
|
|
113
|
-
pair_q: i64(input.pairQ, [1, input.pairQ.length]),
|
|
114
|
-
pair_opt: i64(input.pairOpt, [1, input.pairOpt.length]),
|
|
115
|
-
};
|
|
116
|
-
const tInfer = now();
|
|
117
|
-
const out = await net(feeds);
|
|
118
|
-
const inferMs = now() - tInfer;
|
|
119
|
-
const logits = out?.logits;
|
|
120
|
-
if (!logits) throw new Error("model output has no `logits` — not a typed-decision export?");
|
|
121
|
-
const scores = Array.from(logits.to("float32").data, Number);
|
|
122
|
-
const answers = answersFromScores(scores, {
|
|
123
|
-
questions: normalized,
|
|
124
|
-
groups: input.groups,
|
|
125
|
-
temperature: effective.temperature,
|
|
126
|
-
});
|
|
127
|
-
// Exact view of the single fused sequence the graph consumed.
|
|
128
|
-
let prompt;
|
|
129
|
-
try {
|
|
130
|
-
prompt = tokenizer.decode(input.inputIds, { skip_special_tokens: false });
|
|
131
|
-
} catch {
|
|
132
|
-
prompt = input.inputIds.join(" ");
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
answers,
|
|
136
|
-
truncated: input.truncated,
|
|
137
|
-
length: input.length,
|
|
138
|
-
prompts: [prompt],
|
|
139
|
-
timings: { totalMs: Math.round(now() - t0), inferMs: Math.round(inferMs) },
|
|
140
|
-
};
|
|
141
|
-
},
|
|
142
|
-
async decideMany(cases) {
|
|
143
|
-
const out = [];
|
|
144
|
-
for (const c of cases ?? []) out.push(await this.decide(c.state, c.questions));
|
|
145
|
-
return out;
|
|
146
|
-
},
|
|
147
|
-
dispose() {
|
|
148
|
-
net?.dispose?.();
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
1
|
+
// jev-web — decider session: tokenizer + model + decide().
|
|
2
|
+
//
|
|
3
|
+
// transformers.js is an optional peer dependency, imported lazily so the
|
|
4
|
+
// package works anywhere and hosts can inject their own copy (tests, custom
|
|
5
|
+
// builds). Everything heavy stays out of module scope.
|
|
6
|
+
|
|
7
|
+
import { normalizeQuestions } from "./questions.mjs";
|
|
8
|
+
|
|
9
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
10
|
+
import { buildDecisionInput, typedMarkerIds } from "./encode.mjs";
|
|
11
|
+
import { answersFromScores } from "./answers.mjs";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_MODEL,
|
|
14
|
+
DEFAULT_REVISION,
|
|
15
|
+
loadModelConfig,
|
|
16
|
+
resolveDevice,
|
|
17
|
+
resolveDtype,
|
|
18
|
+
detectWebGPU,
|
|
19
|
+
} from "./config.mjs";
|
|
20
|
+
|
|
21
|
+
export async function createDecider({
|
|
22
|
+
model = DEFAULT_MODEL,
|
|
23
|
+
revision = DEFAULT_REVISION,
|
|
24
|
+
dtype = "auto",
|
|
25
|
+
device = "auto",
|
|
26
|
+
temperature = null,
|
|
27
|
+
maxStateTokens = null,
|
|
28
|
+
onProgress = null,
|
|
29
|
+
transformers = null,
|
|
30
|
+
fetchImpl = fetch,
|
|
31
|
+
scope = globalThis,
|
|
32
|
+
// Directory (or CDN base) holding the onnxruntime-web wasm/loader files.
|
|
33
|
+
// Bundlers usually cannot ship the 26 MB asyncify wasm under static-host
|
|
34
|
+
// file-size limits; hosts that self-host a compressed copy set this to
|
|
35
|
+
// e.g. "/assets/". Defaults to the runtime's own resolution.
|
|
36
|
+
wasmPaths = null,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
39
|
+
if (wasmPaths) {
|
|
40
|
+
try {
|
|
41
|
+
tf.env.backends.onnx.wasm.wasmPaths = wasmPaths;
|
|
42
|
+
} catch { /* older runtime: ignore */ }
|
|
43
|
+
}
|
|
44
|
+
const remote = await loadModelConfig({ model, revision, fetchImpl });
|
|
45
|
+
const resolvedDevice = resolveDevice({ device, webgpu: await detectWebGPU(scope) });
|
|
46
|
+
const resolvedDtype = resolveDtype(resolvedDevice, dtype);
|
|
47
|
+
|
|
48
|
+
const report = (phase) => (p) =>
|
|
49
|
+
onProgress?.({
|
|
50
|
+
phase,
|
|
51
|
+
status: p?.status ?? null,
|
|
52
|
+
file: p?.file ?? null,
|
|
53
|
+
loaded: p?.loaded ?? null,
|
|
54
|
+
total: p?.total ?? null,
|
|
55
|
+
progress: p?.progress ?? null,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const tokenizer = await tf.AutoTokenizer.from_pretrained(model, {
|
|
59
|
+
revision,
|
|
60
|
+
progress_callback: report("tokenizer"),
|
|
61
|
+
});
|
|
62
|
+
const net = await tf.AutoModel.from_pretrained(model, {
|
|
63
|
+
revision,
|
|
64
|
+
dtype: resolvedDtype,
|
|
65
|
+
device: resolvedDevice,
|
|
66
|
+
progress_callback: report("model"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const encode = (text) => Array.from(tokenizer(text, { add_special_tokens: false }).input_ids.data, Number);
|
|
70
|
+
const markers = typedMarkerIds(encode, { markers: remote.markers });
|
|
71
|
+
|
|
72
|
+
const effective = {
|
|
73
|
+
temperature: Number.isFinite(temperature) ? temperature : remote.temperature,
|
|
74
|
+
maxStateTokens: Number.isFinite(maxStateTokens) ? maxStateTokens : remote.maxStateTokens,
|
|
75
|
+
maxLen: remote.maxLen,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const i64 = (values, dims) => new tf.Tensor("int64", BigInt64Array.from(values, BigInt), dims);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
info: {
|
|
82
|
+
family: "open-jev",
|
|
83
|
+
model,
|
|
84
|
+
revision,
|
|
85
|
+
device: resolvedDevice,
|
|
86
|
+
dtype: resolvedDtype,
|
|
87
|
+
temperature: effective.temperature,
|
|
88
|
+
maxStateTokens: effective.maxStateTokens,
|
|
89
|
+
maxLen: effective.maxLen,
|
|
90
|
+
pool: remote.pool,
|
|
91
|
+
configSource: remote.source,
|
|
92
|
+
},
|
|
93
|
+
async decide(state, questions) {
|
|
94
|
+
const t0 = now();
|
|
95
|
+
const normalized = normalizeQuestions(questions);
|
|
96
|
+
const tokenized = normalized.map((q) => ({
|
|
97
|
+
type: q.type,
|
|
98
|
+
instructionIds: encode(q.instructions),
|
|
99
|
+
optionIds: q.options.map((option) => encode(option)),
|
|
100
|
+
}));
|
|
101
|
+
const stateIds = encode(String(state ?? ""));
|
|
102
|
+
const input = buildDecisionInput({
|
|
103
|
+
stateIds,
|
|
104
|
+
markers,
|
|
105
|
+
questions: tokenized,
|
|
106
|
+
maxStateTokens: effective.maxStateTokens,
|
|
107
|
+
maxLen: effective.maxLen,
|
|
108
|
+
});
|
|
109
|
+
const feeds = {
|
|
110
|
+
input_ids: i64(input.inputIds, [1, input.length]),
|
|
111
|
+
attention_mask: i64(input.inputIds.map(() => 1), [1, input.length]),
|
|
112
|
+
seg: i64(input.seg, [1, input.length]),
|
|
113
|
+
pair_q: i64(input.pairQ, [1, input.pairQ.length]),
|
|
114
|
+
pair_opt: i64(input.pairOpt, [1, input.pairOpt.length]),
|
|
115
|
+
};
|
|
116
|
+
const tInfer = now();
|
|
117
|
+
const out = await net(feeds);
|
|
118
|
+
const inferMs = now() - tInfer;
|
|
119
|
+
const logits = out?.logits;
|
|
120
|
+
if (!logits) throw new Error("model output has no `logits` — not a typed-decision export?");
|
|
121
|
+
const scores = Array.from(logits.to("float32").data, Number);
|
|
122
|
+
const answers = answersFromScores(scores, {
|
|
123
|
+
questions: normalized,
|
|
124
|
+
groups: input.groups,
|
|
125
|
+
temperature: effective.temperature,
|
|
126
|
+
});
|
|
127
|
+
// Exact view of the single fused sequence the graph consumed.
|
|
128
|
+
let prompt;
|
|
129
|
+
try {
|
|
130
|
+
prompt = tokenizer.decode(input.inputIds, { skip_special_tokens: false });
|
|
131
|
+
} catch {
|
|
132
|
+
prompt = input.inputIds.join(" ");
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
answers,
|
|
136
|
+
truncated: input.truncated,
|
|
137
|
+
length: input.length,
|
|
138
|
+
prompts: [prompt],
|
|
139
|
+
timings: { totalMs: Math.round(now() - t0), inferMs: Math.round(inferMs) },
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
async decideMany(cases) {
|
|
143
|
+
const out = [];
|
|
144
|
+
for (const c of cases ?? []) out.push(await this.decide(c.state, c.questions));
|
|
145
|
+
return out;
|
|
146
|
+
},
|
|
147
|
+
dispose() {
|
|
148
|
+
net?.dispose?.();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|