harness-everything 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +42 -0
- package/PRIVACY.md +9 -0
- package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
- package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
- package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
- package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
- package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
- package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
- package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
- package/ci/fable-mode-test.js +16 -0
- package/ci/mechanism-34-observations.test.js +550 -0
- package/ci/mechanism-system-one-collect.test.js +16 -0
- package/ci/mechanism-system-one-intent.test.js +406 -0
- package/ci/mechanism-system-one-laya.test.js +45 -0
- package/ci/mechanism-system-one-relevance.test.js +112 -0
- package/ci/mechanism-system-one-resident.test.js +16 -0
- package/ci/system-one-laya-calibrate-test.py +65 -0
- package/ci/system-one-laya-export-test.py +155 -0
- package/ci/system-one-laya-label-test.py +201 -0
- package/ci/system-one-laya-perintent-test.py +60 -0
- package/ci/system-one-laya-provider-test.py +115 -0
- package/ci/system-one-laya-train-test.py +93 -0
- package/docs/routing.md +25 -0
- package/docs/system-one-intent-gates.md +102 -0
- package/docs/system-one-intent.md +173 -0
- package/docs/system-one-laya-training.md +38 -0
- package/docs/system-one-observations.md +206 -0
- package/docs/system-one-routing.md +25 -10
- package/docs/system-one-suggestion-gates.md +292 -0
- package/docs/system-one-training.md +56 -6
- package/docs/workflows/fable-mode.md +7 -0
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +4 -4
- package/fable-mode/agents/fable-worker-haiku.md +4 -4
- package/fable-mode/agents/fable-worker-sonnet.md +4 -4
- package/fable-mode/execution-guardrails/SKILL.md +5 -3
- package/fable-mode/execution-guardrails/references/guardrail-rules.md +12 -8
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/kernel-router.js +10 -0
- package/harness-everything/scripts/system-one/catalogs.js +35 -0
- package/harness-everything/scripts/system-one/contract.js +45 -1
- package/harness-everything/scripts/system-one/evaluate.js +115 -19
- package/harness-everything/scripts/system-one/intent.js +22 -0
- package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
- package/harness-everything/scripts/system-one/provider.js +10 -3
- package/harness-everything/scripts/system-one/resident.js +8 -3
- package/harness-everything/scripts/system-one/router.js +1 -1
- package/hooks/hooks.json +40 -0
- package/hooks/scripts/codex-user-prompt.js +39 -0
- package/hooks/scripts/lib/observations.js +581 -0
- package/hooks/scripts/observation-hook.js +25 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +35 -2
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
- package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
- package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +4 -4
- package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-haiku.md +4 -4
- package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-sonnet.md +4 -4
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +5 -3
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/references/guardrail-rules.md +12 -8
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +115 -19
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +10 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +8 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/evaluate-system-one.js +17 -5
- package/scripts/system-one-calibrate-laya.py +165 -0
- package/scripts/system-one-calibrate.js +42 -13
- package/scripts/system-one-collect.js +7 -3
- package/scripts/system-one-compare-laya.py +156 -0
- package/scripts/system-one-corpus.js +22 -15
- package/scripts/system-one-eval-laya.js +112 -0
- package/scripts/system-one-export-laya.py +197 -0
- package/scripts/system-one-label-laya.py +243 -0
- package/scripts/system-one-label.js +163 -37
- package/scripts/system-one-observations-export.js +106 -0
- package/scripts/system-one-observations-review.js +86 -0
- package/scripts/system-one-perintent-laya.py +134 -0
- package/scripts/system-one-train-laya.py +344 -0
- package/scripts/system-one-train-ngram.py +66 -14
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// Assembles
|
|
4
|
-
// See docs/system-one-corpus.md. A decision counts only for the exact prompt text it reviewed.
|
|
3
|
+
// Assembles a System One holdout (tier or intent stage) from a draft plus human review decisions.
|
|
4
|
+
// See docs/system-one-corpus.md and docs/system-one-intent.md. A decision counts only for the exact prompt text it reviewed.
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const { createHash } = require('node:crypto');
|
|
7
7
|
const { createRequest } = require('../harness-everything/scripts/system-one/contract');
|
|
8
|
-
const {
|
|
8
|
+
const { CATALOGS, MULTI, goldLabels, validSecondary } = require('../harness-everything/scripts/system-one/catalogs');
|
|
9
9
|
const { validateCorpus } = require('../harness-everything/scripts/system-one/evaluate');
|
|
10
10
|
|
|
11
11
|
const SOURCES = ['synthetic:authored', 'derived:local-history'];
|
|
12
12
|
const LANGUAGES = ['en', 'zh-TW'];
|
|
13
|
-
const GOLD = ['tier1', 'tier2', 'tier3', null];
|
|
14
13
|
const REVIEWER = 'human:repository-owner';
|
|
15
14
|
// Drafts are committed publicly: no email addresses or user home paths.
|
|
16
15
|
const PRIVATE = [/[\w.+-]+@[\w-]+\.[\w.-]+/, /[A-Za-z]:\\Users\\/i, /\/home\/[^/\s]+/, /\/Users\/[^/\s]+/];
|
|
@@ -20,7 +19,8 @@ const nonempty = v => typeof v === 'string' && v.trim().length > 0;
|
|
|
20
19
|
const fail = code => { throw new TypeError(code); };
|
|
21
20
|
const promptHash = text => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
22
21
|
|
|
23
|
-
function validateDraft(draft) {
|
|
22
|
+
function validateDraft(draft, task = 'tier') {
|
|
23
|
+
const GOLD = goldLabels(task);
|
|
24
24
|
if (!exact(draft, ['schemaVersion', 'cases']) || draft.schemaVersion !== 1 || !Array.isArray(draft.cases) || !draft.cases.length) fail('invalid-draft');
|
|
25
25
|
const ids = new Set(); const prompts = new Set();
|
|
26
26
|
for (const c of draft.cases) {
|
|
@@ -33,24 +33,27 @@ function validateDraft(draft) {
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function validateReviews(reviews) {
|
|
36
|
+
function validateReviews(reviews, task = 'tier') {
|
|
37
|
+
const GOLD = goldLabels(task);
|
|
37
38
|
if (!exact(reviews, ['schemaVersion', 'decisions']) || reviews.schemaVersion !== 1 || !Array.isArray(reviews.decisions)) fail('invalid-reviews');
|
|
38
39
|
const ids = new Set();
|
|
39
40
|
for (const d of reviews.decisions) {
|
|
40
|
-
|
|
41
|
+
const multi = MULTI.includes(task);
|
|
42
|
+
if (!exact(d, ['id', 'promptHash', 'decision', 'gold', ...(multi ? ['secondary'] : []), 'reviewer']) || !nonempty(d.id) || ids.has(d.id)
|
|
41
43
|
|| typeof d.promptHash !== 'string' || !/^[0-9a-f]{64}$/.test(d.promptHash)
|
|
42
44
|
|| !['accept', 'relabel', 'reject'].includes(d.decision) || !GOLD.includes(d.gold) || d.reviewer !== REVIEWER
|
|
45
|
+
|| !validSecondary(task, d.gold, d.secondary)
|
|
43
46
|
|| (d.decision === 'reject' && d.gold !== null)) fail('invalid-reviews');
|
|
44
47
|
ids.add(d.id);
|
|
45
48
|
}
|
|
46
49
|
return true;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function build(draft, reviews) {
|
|
50
|
-
validateDraft(draft);
|
|
52
|
+
function build(draft, reviews, task = 'tier') {
|
|
53
|
+
validateDraft(draft, task);
|
|
51
54
|
const byId = new Map();
|
|
52
55
|
if (reviews !== null) {
|
|
53
|
-
validateReviews(reviews);
|
|
56
|
+
validateReviews(reviews, task);
|
|
54
57
|
for (const d of reviews.decisions) {
|
|
55
58
|
const c = draft.cases.find(item => item.id === d.id);
|
|
56
59
|
if (!c) fail('unknown-review-case');
|
|
@@ -60,7 +63,8 @@ function build(draft, reviews) {
|
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
const summary = { cases: 0, reviewed: 0, rejected: 0, stale: 0, unreviewed: 0,
|
|
63
|
-
byLanguage: { en: 0, 'zh-TW': 0 }, byGold:
|
|
66
|
+
byLanguage: { en: 0, 'zh-TW': 0 }, byGold: Object.fromEntries(CATALOGS[task].map(o => [o.id, 0])),
|
|
67
|
+
...(MULTI.includes(task) ? { bySecondary: {} } : {}), reviewedHoldoutGate: false };
|
|
64
68
|
const cases = [];
|
|
65
69
|
for (const c of draft.cases) {
|
|
66
70
|
const d = byId.get(c.id);
|
|
@@ -70,10 +74,13 @@ function build(draft, reviews) {
|
|
|
70
74
|
const reviewed = Boolean(current);
|
|
71
75
|
if (reviewed) summary.reviewed += 1; else if (!d) summary.unreviewed += 1;
|
|
72
76
|
const gold = reviewed ? d.gold : c.proposedGold;
|
|
77
|
+
// Drafts propose only the primary intent; secondary intents come from the reviewer.
|
|
78
|
+
const secondary = MULTI.includes(task) ? { secondary: reviewed ? [...d.secondary] : [] } : {};
|
|
73
79
|
cases.push({ id: c.id, family: c.family, split: 'holdout', language: c.language, source: c.source, reviewed,
|
|
74
|
-
request: createRequest(
|
|
80
|
+
request: createRequest(task, c.prompt, CATALOGS[task]), gold, ...secondary });
|
|
75
81
|
summary.byLanguage[c.language] += 1;
|
|
76
82
|
summary.byGold[gold === null ? 'unclassified' : gold] += 1;
|
|
83
|
+
for (const s of secondary.secondary || []) summary.bySecondary[s] = (summary.bySecondary[s] || 0) + 1;
|
|
77
84
|
}
|
|
78
85
|
summary.cases = cases.length;
|
|
79
86
|
const corpus = { schemaVersion: 1, cases };
|
|
@@ -85,14 +92,14 @@ function build(draft, reviews) {
|
|
|
85
92
|
|
|
86
93
|
if (require.main === module) {
|
|
87
94
|
try {
|
|
88
|
-
const [op, draftPath, reviewsPath, outPath, extra] = process.argv.slice(2);
|
|
95
|
+
const [op, draftPath, reviewsPath, outPath, task = 'tier', extra] = process.argv.slice(2);
|
|
89
96
|
if (op !== 'build' || !draftPath || !reviewsPath || !outPath || extra) throw new Error('usage');
|
|
90
97
|
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
91
|
-
const { corpus, summary } = build(read(draftPath), reviewsPath === '-' ? null : read(reviewsPath));
|
|
98
|
+
const { corpus, summary } = build(read(draftPath), reviewsPath === '-' ? null : read(reviewsPath), task);
|
|
92
99
|
fs.writeFileSync(outPath, `${JSON.stringify(corpus, null, 2)}\n`);
|
|
93
100
|
console.log(JSON.stringify(summary));
|
|
94
101
|
} catch (_) {
|
|
95
|
-
console.error('Usage: system-one-corpus.js build <draft.json> <reviews.json|-> <corpus.json> (invalid draft, reviews, or path)');
|
|
102
|
+
console.error('Usage: system-one-corpus.js build <draft.json> <reviews.json|-> <corpus.json> [tier|intent] (invalid draft, reviews, or path)');
|
|
96
103
|
process.exitCode = 1;
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Research evaluation driver for the fine-tuned Laya bridge (#255 Phase 3).
|
|
4
|
+
// It does NOT touch provider.js/resident.js (still cua-only): it starts the
|
|
5
|
+
// laya resident server itself, scores cases through it, and feeds the
|
|
6
|
+
// production decide()/evaluate() machinery. Usage:
|
|
7
|
+
// score <cases.json> <manifest.json> <records.json> [workdir]
|
|
8
|
+
// report <corpus.json> <records.json> <report.json> [--secondary-threshold T]
|
|
9
|
+
// cases.json: { cases: [{ id, baseline, request }] } (request = holdout-style,
|
|
10
|
+
// context + 13 catalog options). records.json matches evaluate() input.
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const net = require('node:net');
|
|
14
|
+
const { spawn } = require('node:child_process');
|
|
15
|
+
const { performance } = require('node:perf_hooks');
|
|
16
|
+
const { evaluate } = require('../harness-everything/scripts/system-one/evaluate');
|
|
17
|
+
const { decide } = require('../harness-everything/scripts/system-one/contract');
|
|
18
|
+
const { decisionPolicy } = require('../harness-everything/scripts/system-one/provider');
|
|
19
|
+
|
|
20
|
+
const ADAPTER = path.join(__dirname, '..', 'harness-everything', 'scripts', 'system-one', 'laya_adapter.py');
|
|
21
|
+
const CALL_TIMEOUT_MS = 30000;
|
|
22
|
+
|
|
23
|
+
function send(port, token, message) {
|
|
24
|
+
return new Promise(resolve => {
|
|
25
|
+
const socket = net.connect({ port, host: '127.0.0.1' });
|
|
26
|
+
const chunks = [];
|
|
27
|
+
const done = value => { socket.destroy(); resolve(value); };
|
|
28
|
+
const timer = setTimeout(() => done({ ok: false, reason: 'timeout' }), CALL_TIMEOUT_MS);
|
|
29
|
+
socket.on('connect', () => socket.end(`${JSON.stringify({ token, ...message })}\n`));
|
|
30
|
+
socket.on('data', c => chunks.push(c));
|
|
31
|
+
socket.on('end', () => { clearTimeout(timer);
|
|
32
|
+
try { done(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
33
|
+
catch (_) { done({ ok: false, reason: 'bad-reply' }); } });
|
|
34
|
+
socket.on('error', () => { clearTimeout(timer); done({ ok: false, reason: 'io' }); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function withServer(python, manifest, workdir, fn) {
|
|
39
|
+
const manifestJson = JSON.stringify(manifest);
|
|
40
|
+
const state = path.join(workdir, 'laya-research-state.json');
|
|
41
|
+
const lock = path.join(workdir, 'laya-research-state.starting');
|
|
42
|
+
for (const f of [state, lock]) try { fs.unlinkSync(f); } catch (_) { /* fresh */ }
|
|
43
|
+
const child = spawn(python, [ADAPTER, '--serve', manifestJson, state, lock], { stdio: 'ignore' });
|
|
44
|
+
const deadline = Date.now() + 120000;
|
|
45
|
+
let info = null;
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
try { info = JSON.parse(fs.readFileSync(state, 'utf8')); if (info.port && info.token) break; } catch (_) { /* starting */ }
|
|
48
|
+
if (child.exitCode !== null) throw new Error(`server exited during startup (code ${child.exitCode})`);
|
|
49
|
+
await new Promise(r => setTimeout(r, 200));
|
|
50
|
+
info = null;
|
|
51
|
+
}
|
|
52
|
+
if (!info) { child.kill('SIGKILL'); throw new Error('server did not become ready'); }
|
|
53
|
+
try {
|
|
54
|
+
return await fn(info);
|
|
55
|
+
} finally {
|
|
56
|
+
try { await send(info.port, info.token, { op: 'shutdown' }); } catch (_) { /* exiting */ }
|
|
57
|
+
child.kill('SIGKILL');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function score(casesPath, manifestPath, recordsPath, workdir) {
|
|
62
|
+
const { cases } = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
|
|
63
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
const policy = decisionPolicy(manifest.acceptance);
|
|
65
|
+
const records = await withServer(manifest.python, manifest, workdir, async info => {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const c of cases) {
|
|
68
|
+
const runs = [];
|
|
69
|
+
for (let i = 0; i < 2; i++) {
|
|
70
|
+
const start = performance.now();
|
|
71
|
+
const reply = await send(info.port, info.token, { op: 'score', request: c.request });
|
|
72
|
+
const latencyMs = performance.now() - start;
|
|
73
|
+
let decision;
|
|
74
|
+
let scores = null;
|
|
75
|
+
if (reply.ok) {
|
|
76
|
+
decision = decide(c.request, reply.response, policy);
|
|
77
|
+
if (decision.selectedId === 'unclassified') {
|
|
78
|
+
decision = { ...decision, status: 'abstain', reason: 'unclassified', selectedId: null };
|
|
79
|
+
}
|
|
80
|
+
if (['accepted', 'abstain'].includes(decision.status)) scores = reply.response.scores.map(s => s.probability);
|
|
81
|
+
} else {
|
|
82
|
+
decision = { status: 'unavailable', reason: reply.reason };
|
|
83
|
+
}
|
|
84
|
+
runs.push({ decision, scores, latencyMs, coldStart: false });
|
|
85
|
+
}
|
|
86
|
+
out.push({ id: c.id, baseline: c.baseline, runs });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
});
|
|
90
|
+
fs.writeFileSync(recordsPath, `${JSON.stringify(records)}\n`);
|
|
91
|
+
console.log(JSON.stringify({ cases: records.length }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function report(corpusPath, recordsPath, reportPath, secondaryThreshold) {
|
|
95
|
+
const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
96
|
+
const records = JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
|
|
97
|
+
const result = evaluate(corpus, records, null, { secondaryThreshold });
|
|
98
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
99
|
+
console.log(JSON.stringify({ cases: result.cases, coverage: result.model.coverage,
|
|
100
|
+
acceptedPrecision: result.model.acceptedPrecision, rolloutReady: result.rolloutReady }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const [mode, a, b, c, d] = process.argv.slice(2);
|
|
105
|
+
if (mode === 'score' && a && b && c) return score(a, b, c, d || fs.mkdtempSync('/tmp/laya-eval-'));
|
|
106
|
+
if (mode === 'report' && a && b && c) {
|
|
107
|
+
const t = d === undefined ? undefined : Number((d.match(/^--secondary-threshold=(.+)$/) || [])[1]);
|
|
108
|
+
return report(a, b, c, t);
|
|
109
|
+
}
|
|
110
|
+
throw new Error('usage: system-one-eval-laya.js (score <cases> <manifest> <records> [workdir] | report <corpus> <records> <report> [--secondary-threshold=T])');
|
|
111
|
+
}
|
|
112
|
+
main().catch(err => { console.error(`laya research eval failed: ${err.message}`); process.exitCode = 1; });
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export soft-target RLCD training items for Laya fine-tuning (#255 Phase 3).
|
|
3
|
+
|
|
4
|
+
Each train prompt with teacher relevance scores becomes 12 `noul` items, one
|
|
5
|
+
per owner intent (catalog order, v1 wording shared with system-one-label-laya).
|
|
6
|
+
Teacher relevance p maps to gold probabilities {true: p, false: 1-p}, matching
|
|
7
|
+
the upstream fine-tune notebook (LocalLLaMA/typed-decisions, noul target
|
|
8
|
+
[false, true], qtype 2, label = argmax).
|
|
9
|
+
|
|
10
|
+
Output is reviewable jsonl (one item per line); the training host converts it
|
|
11
|
+
to train_items.pt with torch.save (never commit .pt). Sequence building goes
|
|
12
|
+
through laya.common.build_sequence so tokenization matches inference exactly.
|
|
13
|
+
Stdlib only at import time; `laya` is imported lazily inside the default
|
|
14
|
+
builder so contract tests run without model weights.
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import importlib.util
|
|
19
|
+
import json
|
|
20
|
+
import random
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
HERE = Path(__file__).resolve()
|
|
25
|
+
_label_spec = importlib.util.spec_from_file_location(
|
|
26
|
+
'laya_labeler', HERE.parent / 'system-one-label-laya.py')
|
|
27
|
+
_label = importlib.util.module_from_spec(_label_spec)
|
|
28
|
+
_label_spec.loader.exec_module(_label)
|
|
29
|
+
INTENTS = _label.INTENTS
|
|
30
|
+
QUESTIONS_VERSION = _label.QUESTIONS_VERSION
|
|
31
|
+
build_questions = _label.build_questions
|
|
32
|
+
|
|
33
|
+
# laya.common.QTYPES = {"choice": 0, "score": 1, "noul": 2}.
|
|
34
|
+
QTYPE_NOUL = 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_item(state_text, instructions, probs, builder):
|
|
38
|
+
"""One upstream-format training item from gold {true, false} probabilities."""
|
|
39
|
+
pf, pt = float(probs['false']), float(probs['true'])
|
|
40
|
+
if not (pf >= 0 and pt >= 0 and abs(pf + pt - 1.0) < 1e-6):
|
|
41
|
+
raise ValueError(f'probabilities must sum to 1, got {probs!r}')
|
|
42
|
+
built = builder(state_text, instructions, {'true': pt, 'false': pf})
|
|
43
|
+
if built is None:
|
|
44
|
+
return None
|
|
45
|
+
target = [built['target'][0], built['target'][1]]
|
|
46
|
+
if abs(sum(target) - 1.0) >= 1e-6:
|
|
47
|
+
raise ValueError(f'builder target must sum to 1, got {target!r}')
|
|
48
|
+
return {'ids': built['ids'], 'markers': built['markers'], 'qtype': QTYPE_NOUL,
|
|
49
|
+
'target': target, 'label': target.index(max(target))}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_oversample(spec):
|
|
53
|
+
"""Parse 'intent:factor,...' into {intent: factor}; factor 1 is rejected."""
|
|
54
|
+
table = {}
|
|
55
|
+
for part in (spec or '').split(','):
|
|
56
|
+
if not part.strip():
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
intent, factor = part.split(':')
|
|
60
|
+
factor = int(factor)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise ValueError(f'oversample: bad entry {part!r}, want intent:factor')
|
|
63
|
+
if intent not in INTENTS or factor < 2:
|
|
64
|
+
raise ValueError(f'oversample: bad entry {part!r}, want known intent and factor >= 2')
|
|
65
|
+
table[intent] = factor
|
|
66
|
+
return table
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def default_builder_factory(checkpoint='convaiinnovations/laya', subfolder='multilingual'):
|
|
70
|
+
"""Real sequence builder through laya.common (lazy import)."""
|
|
71
|
+
from huggingface_hub import snapshot_download # noqa: PLC0415
|
|
72
|
+
from transformers import AutoTokenizer # noqa: PLC0415
|
|
73
|
+
from laya.agent import _fix_tokenizer_config # noqa: PLC0415
|
|
74
|
+
from laya.common import build_sequence, render_options # noqa: PLC0415
|
|
75
|
+
|
|
76
|
+
model_dir = Path(snapshot_download(checkpoint))
|
|
77
|
+
revision = model_dir.name
|
|
78
|
+
base = model_dir / subfolder if subfolder else model_dir
|
|
79
|
+
_fix_tokenizer_config(str(base))
|
|
80
|
+
tok = AutoTokenizer.from_pretrained(str(base / 'tokenizer'))
|
|
81
|
+
cfg = json.loads((base / 'rl_agent_config.json').read_text(encoding='utf-8'))
|
|
82
|
+
questions = build_questions()
|
|
83
|
+
|
|
84
|
+
def builder(state_text, instructions, probs):
|
|
85
|
+
question = {'t': 'noul', 'ins': instructions, 'crit': {}}
|
|
86
|
+
seq, markers = build_sequence(tok, state_text, question, cfg['max_len'], cfg['head_max_len'])
|
|
87
|
+
if len(markers) != len(render_options(question)):
|
|
88
|
+
return None
|
|
89
|
+
return {'ids': seq, 'markers': markers,
|
|
90
|
+
'target': [probs['false'], probs['true']]}
|
|
91
|
+
|
|
92
|
+
provenance = {'checkpoint': checkpoint, 'revision': revision, 'subfolder': subfolder or 'root',
|
|
93
|
+
'questions': QUESTIONS_VERSION,
|
|
94
|
+
'tokenizerDir': str(base / 'tokenizer')}
|
|
95
|
+
return builder, provenance
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def export(data_dir, split, out_path, manifest_path, builder, seed=0, forbid_path=None,
|
|
99
|
+
teacher_file='labels-intent-scores.jsonl', provenance=None, oversample=None):
|
|
100
|
+
"""Export one prompts.jsonl split to upstream-format items."""
|
|
101
|
+
if split not in ('train', 'validation'):
|
|
102
|
+
raise ValueError(f'split must be train or validation, got {split!r}')
|
|
103
|
+
oversample = dict(oversample or {})
|
|
104
|
+
for intent, factor in oversample.items():
|
|
105
|
+
if intent not in INTENTS or not isinstance(factor, int) or factor < 2:
|
|
106
|
+
raise ValueError(f'oversample: bad entry {intent!r}:{factor!r}')
|
|
107
|
+
data_dir, out_path, manifest_path = Path(data_dir), Path(out_path), Path(manifest_path)
|
|
108
|
+
prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
|
|
109
|
+
teacher_raw = (data_dir / teacher_file).read_text(encoding='utf-8')
|
|
110
|
+
teacher = {r['id']: r for r in
|
|
111
|
+
(json.loads(line) for line in teacher_raw.splitlines() if line.strip())}
|
|
112
|
+
rows = sorted((r for r in (json.loads(line) for line in prompts_raw.splitlines() if line.strip())
|
|
113
|
+
if r.get('split') == split), key=lambda r: r['id'])
|
|
114
|
+
if forbid_path is not None:
|
|
115
|
+
forbidden = set(json.loads(Path(forbid_path).read_text(encoding='utf-8')))
|
|
116
|
+
clash = sorted({r['id'] for r in rows} & forbidden)
|
|
117
|
+
if clash:
|
|
118
|
+
raise ValueError(f'overlap with forbidden ids: {clash[:5]} (n={len(clash)})')
|
|
119
|
+
if seed:
|
|
120
|
+
random.Random(seed).shuffle(rows)
|
|
121
|
+
questions = build_questions()
|
|
122
|
+
items, skipped_no_teacher, skipped_markers = 0, 0, 0
|
|
123
|
+
with out_path.open('w', encoding='utf-8') as fh:
|
|
124
|
+
for prompt in rows:
|
|
125
|
+
trow = teacher.get(prompt['id'])
|
|
126
|
+
if trow is None:
|
|
127
|
+
skipped_no_teacher += 1
|
|
128
|
+
continue
|
|
129
|
+
for intent in INTENTS:
|
|
130
|
+
p = float(trow['scores'].get(intent, 0.0))
|
|
131
|
+
repeat = oversample.get(intent, 1) if p >= 0.4 else 1
|
|
132
|
+
item = build_item(prompt['text'], questions[intent]['instructions'],
|
|
133
|
+
{'true': p, 'false': 1.0 - p}, builder)
|
|
134
|
+
if item is None:
|
|
135
|
+
skipped_markers += repeat
|
|
136
|
+
continue
|
|
137
|
+
line = json.dumps({'promptId': prompt['id'], 'intent': intent, **item}) + '\n'
|
|
138
|
+
for _ in range(repeat):
|
|
139
|
+
fh.write(line)
|
|
140
|
+
items += repeat
|
|
141
|
+
manifest = {'split': split, 'seed': seed, 'questions': QUESTIONS_VERSION,
|
|
142
|
+
'builder': provenance or {'builder': 'injected'},
|
|
143
|
+
'oversample': oversample,
|
|
144
|
+
'items': items, 'prompts': len(rows),
|
|
145
|
+
'skippedNoTeacher': skipped_no_teacher, 'skippedMarkers': skipped_markers,
|
|
146
|
+
'promptsSha256': hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
|
|
147
|
+
'teacherSha256': hashlib.sha256(teacher_raw.encode('utf-8')).hexdigest(),
|
|
148
|
+
'teacherFile': teacher_file,
|
|
149
|
+
'outputSha256': hashlib.sha256(out_path.read_bytes()).hexdigest()}
|
|
150
|
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
|
|
151
|
+
return {'items': items, 'skippedNoTeacher': skipped_no_teacher,
|
|
152
|
+
'skippedMarkers': skipped_markers, 'manifest': manifest}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def main(argv=None):
|
|
156
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
157
|
+
parser.add_argument('--data-dir', required=True, help='private corpus dir')
|
|
158
|
+
parser.add_argument('--split', choices=['train', 'validation'], default='train')
|
|
159
|
+
parser.add_argument('--out', required=True, help='output items jsonl')
|
|
160
|
+
parser.add_argument('--manifest', required=True, help='provenance manifest json')
|
|
161
|
+
parser.add_argument('--checkpoint', default='convaiinnovations/laya')
|
|
162
|
+
parser.add_argument('--subfolder', default='multilingual')
|
|
163
|
+
parser.add_argument('--seed', type=int, default=0, help='0 keeps id order; nonzero shuffles')
|
|
164
|
+
parser.add_argument('--forbid', default=None, help='json list of forbidden ids (overlap guard)')
|
|
165
|
+
parser.add_argument('--teacher', default='labels-intent-scores.jsonl')
|
|
166
|
+
parser.add_argument('--oversample', default='', help='intent:factor,... to repeat positive intent items')
|
|
167
|
+
parser.add_argument('--limit', type=int, default=0, help='export only the first N prompts (0 = all)')
|
|
168
|
+
args = parser.parse_args(argv)
|
|
169
|
+
oversample = parse_oversample(args.oversample)
|
|
170
|
+
builder, provenance = default_builder_factory(args.checkpoint, args.subfolder)
|
|
171
|
+
print(json.dumps({'builder': provenance}))
|
|
172
|
+
# --limit applies before export by staging a filtered view; export() keeps split semantics.
|
|
173
|
+
if args.limit > 0:
|
|
174
|
+
import tempfile # noqa: PLC0415
|
|
175
|
+
data_dir = Path(args.data_dir)
|
|
176
|
+
prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
|
|
177
|
+
rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
|
|
178
|
+
todo = sorted((r for r in rows if r.get('split') == args.split), key=lambda r: r['id'])[:args.limit]
|
|
179
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
180
|
+
view = Path(tmp) / 'view'
|
|
181
|
+
view.mkdir()
|
|
182
|
+
(view / 'prompts.jsonl').write_text(
|
|
183
|
+
'\n'.join(json.dumps(r) for r in todo) + '\n', encoding='utf-8')
|
|
184
|
+
(view / args.teacher).write_text((data_dir / args.teacher).read_text(encoding='utf-8'), encoding='utf-8')
|
|
185
|
+
summary = export(view, args.split, args.out, args.manifest, builder,
|
|
186
|
+
seed=args.seed, forbid_path=args.forbid, teacher_file=args.teacher,
|
|
187
|
+
provenance=provenance, oversample=oversample)
|
|
188
|
+
else:
|
|
189
|
+
summary = export(args.data_dir, args.split, args.out, args.manifest, builder,
|
|
190
|
+
seed=args.seed, forbid_path=args.forbid, teacher_file=args.teacher,
|
|
191
|
+
provenance=provenance, oversample=oversample)
|
|
192
|
+
print(json.dumps({k: v for k, v in summary.items() if k != 'manifest'}))
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == '__main__':
|
|
197
|
+
sys.exit(main())
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Zero-shot dense intent labeling with Laya (#255 Phase 1+2).
|
|
3
|
+
|
|
4
|
+
Maps each of the 12 owner intents (catalogs.js order, frozen) to one Laya
|
|
5
|
+
`noul` question and records the yes-probability as the per-intent relevance
|
|
6
|
+
score. Output rows match the `labels-intent-scores.jsonl` trainer contract
|
|
7
|
+
{id, gold, secondary, scores}, but MUST be written to a separate file: the
|
|
8
|
+
sonnet-scored labels are never overwritten. Derived gold/secondary are
|
|
9
|
+
mechanical bootstrap only (labeler.derived=true), never owner truth.
|
|
10
|
+
|
|
11
|
+
Stdlib only at import time; `laya` / `laya_mlx` are imported lazily inside
|
|
12
|
+
the backend constructors so contract tests run without model weights.
|
|
13
|
+
"""
|
|
14
|
+
import argparse
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
import statistics
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# Owner catalog order (harness-everything/scripts/system-one/catalogs.js).
|
|
24
|
+
# This order is the scorer output order; changing it invalidates weights.
|
|
25
|
+
INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
|
|
26
|
+
'review', 'test', 'docs', 'plan', 'investigate']
|
|
27
|
+
|
|
28
|
+
QUESTIONS_VERSION = 'v1'
|
|
29
|
+
|
|
30
|
+
# The sonnet-scored labels file. The labeler refuses to write to it.
|
|
31
|
+
PROTECTED_OUTPUT = 'labels-intent-scores.jsonl'
|
|
32
|
+
|
|
33
|
+
_PROPOSITIONS = {
|
|
34
|
+
'explain': 'The request asks for an explanation of how something works or an answer to a question.',
|
|
35
|
+
'discuss': 'The request asks to weigh options, give an opinion, or settle a decision.',
|
|
36
|
+
'git': 'The request asks for Git or GitHub housekeeping: commit, push, branch, or pull request.',
|
|
37
|
+
'fix': 'The request asks to repair wrong behavior: a bug, crash, failing test, or build.',
|
|
38
|
+
'edit': 'The request asks to change an existing value, setting, text, or behavior.',
|
|
39
|
+
'feature': 'The request asks to add new behavior: a command, option, format, or script.',
|
|
40
|
+
'refactor': 'The request asks to restructure or unify existing code without adding new behavior.',
|
|
41
|
+
'review': 'The request asks to evaluate an existing artifact against a standard.',
|
|
42
|
+
'test': 'The request asks to run tests or builds, or to write tests as the main deliverable.',
|
|
43
|
+
'docs': 'The request asks to write or edit documentation as the main deliverable.',
|
|
44
|
+
'plan': 'The request asks to produce a plan, specification, or breakdown before implementation.',
|
|
45
|
+
'investigate': 'The request asks to find facts or a cause and report back without changing anything.',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# v2 adds contrastive exclusions aimed at the measured top-2 co-firing pairs
|
|
49
|
+
# (edit/refactor, fix/investigate, investigate/review/explain, docs/edit,
|
|
50
|
+
# feature/edit, plan/discuss). Same 12 intents, same semantics: exclusions
|
|
51
|
+
# only sharpen boundaries, they never add or remove a category.
|
|
52
|
+
_PROPOSITIONS_V2 = {
|
|
53
|
+
'explain': 'The request asks for an explanation of how something works or an answer to a question. Not investigating an unknown cause (investigate).',
|
|
54
|
+
'discuss': 'The request asks to weigh options, give an opinion, or settle a decision. Not producing an implementation plan or breakdown (plan).',
|
|
55
|
+
'git': 'The request asks for Git or GitHub housekeeping: commit, push, branch, or pull request.',
|
|
56
|
+
'fix': 'The request asks to repair wrong behavior: a bug, crash, failing test, or build. Not merely finding the cause (investigate); only when a repair is requested.',
|
|
57
|
+
'edit': 'The request asks to change an existing value, setting, text, or behavior. Not a bug repair (fix), not a restructuring (refactor), not new behavior (feature), not documentation as the deliverable (docs).',
|
|
58
|
+
'feature': 'The request asks to add new behavior: a command, option, format, or script. Not modifying existing behavior (edit).',
|
|
59
|
+
'refactor': 'The request asks to restructure or unify existing code without adding new behavior. Not a value change (edit), not a bug repair (fix).',
|
|
60
|
+
'review': 'The request asks to evaluate an existing artifact against a standard. Not fact-finding (investigate).',
|
|
61
|
+
'test': 'The request asks to run tests or builds, or to write tests as the main deliverable.',
|
|
62
|
+
'docs': 'The request asks to write or edit documentation as the main deliverable. Not incidental text edits alongside other work (edit).',
|
|
63
|
+
'plan': 'The request asks to produce a plan, specification, or breakdown before implementation. Not weighing options in discussion (discuss).',
|
|
64
|
+
'investigate': 'The request asks to find facts or a cause and report back without changing anything. Not repairing (fix), not judging quality (review), not explaining how things work (explain).',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_QUESTIONS = {'v1': _PROPOSITIONS, 'v2': _PROPOSITIONS_V2}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_questions(version='v1'):
|
|
71
|
+
"""One `noul` question per intent, in catalog order."""
|
|
72
|
+
if version not in _QUESTIONS:
|
|
73
|
+
raise ValueError(f'unknown questions version: {version!r}')
|
|
74
|
+
table = _QUESTIONS[version]
|
|
75
|
+
return {intent: {'type': 'noul', 'instructions': table[intent]} for intent in INTENTS}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def validate_scores(scores):
|
|
79
|
+
"""True when scores cover exactly the 12 intents with finite 0..1 floats."""
|
|
80
|
+
if not isinstance(scores, dict) or set(scores) != set(INTENTS):
|
|
81
|
+
raise ValueError(f'scores keys must be exactly the 12 intents, got {sorted(scores) if isinstance(scores, dict) else type(scores)}')
|
|
82
|
+
for intent in INTENTS:
|
|
83
|
+
v = scores[intent]
|
|
84
|
+
if not isinstance(v, float) or not math.isfinite(v) or not 0.0 <= v <= 1.0:
|
|
85
|
+
raise ValueError(f'scores[{intent!r}] out of range: {v!r}')
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def derive_label(scores):
|
|
90
|
+
"""Mechanical {gold, secondary} from bands (bootstrap only, derived=true).
|
|
91
|
+
|
|
92
|
+
Mirrors the scoredLabel bands: secondary is every other intent at 0.4+;
|
|
93
|
+
a max at or below 0.2 abstains to null. Ties break by catalog order.
|
|
94
|
+
"""
|
|
95
|
+
validate_scores(scores)
|
|
96
|
+
top = max(scores[intent] for intent in INTENTS)
|
|
97
|
+
if top <= 0.2:
|
|
98
|
+
return {'gold': None, 'secondary': [], 'derived': True}
|
|
99
|
+
gold = max(INTENTS, key=lambda i: (scores[i], -INTENTS.index(i)))
|
|
100
|
+
secondary = sorted((i for i in INTENTS if i != gold and scores[i] >= 0.4),
|
|
101
|
+
key=lambda i: (-scores[i], INTENTS.index(i)))
|
|
102
|
+
return {'gold': gold, 'secondary': secondary, 'derived': True}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def output_row(prompt, scores, backend, out_name, routing=None, questions_version='v1'):
|
|
106
|
+
"""One trainer-contract row for a labeled prompt."""
|
|
107
|
+
if Path(out_name).name == PROTECTED_OUTPUT:
|
|
108
|
+
raise ValueError(f'protected output: refusing to overwrite {PROTECTED_OUTPUT}')
|
|
109
|
+
derived = derive_label(scores)
|
|
110
|
+
labeler = {'task': 'intent', 'target': 'scores', 'backend': backend,
|
|
111
|
+
'questions': questions_version, 'derived': True}
|
|
112
|
+
if routing is not None:
|
|
113
|
+
labeler['routing'] = routing
|
|
114
|
+
return {'id': prompt['id'], 'gold': derived['gold'], 'secondary': derived['secondary'],
|
|
115
|
+
'scores': {intent: scores[intent] for intent in INTENTS},
|
|
116
|
+
'labeler': labeler}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def torch_backend(questions_version='v1'):
|
|
120
|
+
"""Official PyTorch Laya backend (lazy import). Returns (fn, provenance)."""
|
|
121
|
+
import laya # noqa: PLC0415
|
|
122
|
+
router = laya.Router()
|
|
123
|
+
questions = build_questions(questions_version)
|
|
124
|
+
|
|
125
|
+
def run(text):
|
|
126
|
+
started = time.monotonic()
|
|
127
|
+
result = router.predict(text, questions)
|
|
128
|
+
elapsed_ms = (time.monotonic() - started) * 1000.0
|
|
129
|
+
answers = result['answers']
|
|
130
|
+
scores = {intent: float(answers[intent]['noul']) for intent in INTENTS}
|
|
131
|
+
return scores, result['routing']['model'], elapsed_ms
|
|
132
|
+
|
|
133
|
+
provenance = {'package': 'laya', 'version': laya.__version__, 'questions': questions_version}
|
|
134
|
+
return run, provenance
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def mlx_backend(questions_version='v1'):
|
|
138
|
+
"""Native MLX Laya port backend (lazy import, inference only)."""
|
|
139
|
+
import laya_mlx # noqa: PLC0415
|
|
140
|
+
from laya_mlx import Router # noqa: PLC0415
|
|
141
|
+
router = Router()
|
|
142
|
+
questions = build_questions(questions_version)
|
|
143
|
+
|
|
144
|
+
def run(text):
|
|
145
|
+
started = time.monotonic()
|
|
146
|
+
result = router.predict(text, questions)
|
|
147
|
+
elapsed_ms = (time.monotonic() - started) * 1000.0
|
|
148
|
+
answers = result['answers']
|
|
149
|
+
scores = {intent: float(answers[intent]['noul']) for intent in INTENTS}
|
|
150
|
+
return scores, result['routing']['model'], elapsed_ms
|
|
151
|
+
|
|
152
|
+
provenance = {'package': 'laya-mlx', 'version': laya_mlx.__version__, 'questions': questions_version}
|
|
153
|
+
return run, provenance
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def label_rows(rows, split, input_sha, out_path, manifest_path, backend_fn, backend_name, resume=False,
|
|
157
|
+
questions_version='v1'):
|
|
158
|
+
"""Label explicit rows; returns a summary dict."""
|
|
159
|
+
out_path, manifest_path = Path(out_path), Path(manifest_path)
|
|
160
|
+
if out_path.name == PROTECTED_OUTPUT:
|
|
161
|
+
raise ValueError(f'protected output: refusing to overwrite {PROTECTED_OUTPUT}')
|
|
162
|
+
done = set()
|
|
163
|
+
if resume and out_path.exists():
|
|
164
|
+
for line in out_path.read_text(encoding='utf-8').splitlines():
|
|
165
|
+
if line.strip():
|
|
166
|
+
done.add(json.loads(line)['id'])
|
|
167
|
+
rows = [r for r in rows if r['id'] not in done]
|
|
168
|
+
routing, latencies, this_run = {}, [], 0
|
|
169
|
+
with out_path.open('a' if (resume and done) else 'w', encoding='utf-8') as fh:
|
|
170
|
+
for prompt in rows:
|
|
171
|
+
scores, routed, elapsed_ms = backend_fn(prompt['text'])
|
|
172
|
+
fh.write(json.dumps(output_row(prompt, scores, backend_name, out_path.name, routed,
|
|
173
|
+
questions_version)) + '\n')
|
|
174
|
+
routing[routed] = routing.get(routed, 0) + 1
|
|
175
|
+
latencies.append(elapsed_ms)
|
|
176
|
+
this_run += 1
|
|
177
|
+
total, full_routing = this_run, routing
|
|
178
|
+
if resume:
|
|
179
|
+
# The file is the source of truth: recount everything for cumulative totals.
|
|
180
|
+
full_routing = {}
|
|
181
|
+
total = 0
|
|
182
|
+
for line in out_path.read_text(encoding='utf-8').splitlines():
|
|
183
|
+
if not line.strip():
|
|
184
|
+
continue
|
|
185
|
+
row = json.loads(line)
|
|
186
|
+
routed = (row.get('labeler') or {}).get('routing', 'unknown')
|
|
187
|
+
full_routing[routed] = full_routing.get(routed, 0) + 1
|
|
188
|
+
total += 1
|
|
189
|
+
manifest = {'backend': backend_name, 'split': split, 'questions': questions_version,
|
|
190
|
+
'labeled': total, 'thisRun': this_run, 'resumed': bool(resume and done),
|
|
191
|
+
'inputSha256': input_sha, 'routing': full_routing,
|
|
192
|
+
'latencyMs': {'p50': statistics.median(latencies) if latencies else 0.0,
|
|
193
|
+
'p95': statistics.quantiles(latencies, n=100)[94] if len(latencies) >= 100 else (max(latencies) if latencies else 0.0)}}
|
|
194
|
+
manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
|
|
195
|
+
return manifest
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def label_split(data_dir, split, out_path, manifest_path, backend_fn, backend_name, questions_version='v1'):
|
|
199
|
+
"""Label one prompts.jsonl split; returns a summary dict."""
|
|
200
|
+
if split not in ('train', 'validation'):
|
|
201
|
+
raise ValueError(f'split must be train or validation, got {split!r}')
|
|
202
|
+
data_dir = Path(data_dir)
|
|
203
|
+
prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
|
|
204
|
+
rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
|
|
205
|
+
todo = [r for r in rows if r.get('split') == split]
|
|
206
|
+
return label_rows(todo, split, hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
|
|
207
|
+
out_path, manifest_path, backend_fn, backend_name,
|
|
208
|
+
questions_version=questions_version)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def main(argv=None):
|
|
212
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
213
|
+
parser.add_argument('--data-dir', required=True, help='private corpus dir (holds prompts.jsonl)')
|
|
214
|
+
parser.add_argument('--split', choices=['train', 'validation'], default='validation')
|
|
215
|
+
parser.add_argument('--out', required=True, help='output jsonl (never labels-intent-scores.jsonl)')
|
|
216
|
+
parser.add_argument('--manifest', required=True, help='provenance manifest json')
|
|
217
|
+
parser.add_argument('--backend', choices=['torch', 'mlx'], required=True)
|
|
218
|
+
parser.add_argument('--questions', choices=['v1', 'v2'], default='v1')
|
|
219
|
+
parser.add_argument('--limit', type=int, default=0, help='label only the first N rows (0 = all)')
|
|
220
|
+
parser.add_argument('--resume', action='store_true', help='skip ids already present in --out')
|
|
221
|
+
args = parser.parse_args(argv)
|
|
222
|
+
run, provenance = (torch_backend(args.questions) if args.backend == 'torch'
|
|
223
|
+
else mlx_backend(args.questions))
|
|
224
|
+
print(json.dumps({'backend': args.backend, 'provenance': provenance}))
|
|
225
|
+
|
|
226
|
+
data_dir = Path(args.data_dir)
|
|
227
|
+
prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
|
|
228
|
+
rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
|
|
229
|
+
todo = [r for r in rows if r.get('split') == args.split]
|
|
230
|
+
if args.limit > 0:
|
|
231
|
+
todo = todo[:args.limit]
|
|
232
|
+
summary = label_rows(todo, args.split,
|
|
233
|
+
hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
|
|
234
|
+
args.out, args.manifest, run, args.backend, resume=args.resume,
|
|
235
|
+
questions_version=args.questions)
|
|
236
|
+
summary['limit'] = args.limit
|
|
237
|
+
Path(args.manifest).write_text(json.dumps(summary, indent=2), encoding='utf-8')
|
|
238
|
+
print(json.dumps(summary))
|
|
239
|
+
return 0
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == '__main__':
|
|
243
|
+
sys.exit(main())
|