harness-everything 0.29.0 → 0.30.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +35 -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/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/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/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
|
@@ -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())
|