Train model from JSONL data — keyword-free.
(data_path: str, out_path: str | None = None)
| 166 | |
| 167 | |
| 168 | def train_and_save_model(data_path: str, out_path: str | None = None) -> None: |
| 169 | """Train model from JSONL data — keyword-free.""" |
| 170 | import json |
| 171 | |
| 172 | cases = [] |
| 173 | with open(data_path, "r", encoding="utf-8") as f: |
| 174 | for line in f: |
| 175 | line = line.strip() |
| 176 | if line: |
| 177 | cases.append(json.loads(line)) |
| 178 | |
| 179 | feature_sets: list[tuple[dict[str, float], str]] = [] |
| 180 | model = ScriptAgnosticClassifier(use_ngrams=True) |
| 181 | |
| 182 | for case in cases: |
| 183 | prompt = case["prompt"] |
| 184 | feature_text = _merge_feature_text(prompt, case.get("system_prompt")) |
| 185 | struct_dims = extract_structural_features(feature_text) |
| 186 | structural_scores = {d.name: d.score for d in struct_dims} |
| 187 | unicode_blocks = extract_unicode_block_features(feature_text) |
| 188 | |
| 189 | features = model._build_features( |
| 190 | structural_scores, |
| 191 | unicode_blocks, |
| 192 | keyword_scores=None, |
| 193 | prompt=feature_text, |
| 194 | ) |
| 195 | normalized_tier = model._normalize_tier_label(case["expected_tier"]) |
| 196 | if normalized_tier is not None: |
| 197 | feature_sets.append((features, normalized_tier)) |
| 198 | |
| 199 | model.train(feature_sets, epochs=20) |
| 200 | |
| 201 | save_to = Path(out_path) if out_path else Path(__file__).parent / "model.json" |
| 202 | model.save(save_to) |
| 203 | print(f"Trained on {len(cases)} cases, saved to {save_to}") |
| 204 | |
| 205 | correct = sum(1 for feats, tier in feature_sets if model.predict(feats)[0] == tier) |
| 206 | if feature_sets: |
| 207 | print(f"Training accuracy: {correct}/{len(feature_sets)} ({correct / len(feature_sets) * 100:.1f}%)") |
| 208 | |
| 209 | |
| 210 | # ─── Trivial Detection (structural only, no keyword lists) ─── |
nothing calls this directly
no test coverage detected