microfactory-lab / learn /finetune /prep_dataset.py
kylebrodeur's picture
deploy: update Space from deploy_preflight --push
ed8be4a verified
Raw
History Blame Contribute Delete
3.96 kB
"""Build a supervised fine-tune dataset by DISTILLING the node's own judgment.
The live node decides with retrieval + a learned policy + a deterministic Spine.
This script turns that behaviour into (prompt -> Advice JSON) pairs over a grid of
(material, geometry, room) conditions, so a small Gemma can be fine-tuned to carry
the same judgment in its weights. That is the "Well-Tuned" frontier the writeup
names: visible memory (retrieval) stays the live path, and this bakes a copy of the
judgment into the weights.
Honesty: targets here are the node's *structured* output (the same Advice JSON the
live call returns). Offline, that is the deterministic advisor over real retrieved
precedent, so it is a faithful distillation of the system, not invented data. For a
higher-fidelity teacher, run with a live model up (Ollama) so `advise()` returns the
real Gemma output instead of the fallback; the pair format is identical either way.
Run: uv run python -m learn.finetune.prep_dataset
Out: data/finetune/sft.train.jsonl + data/finetune/sft.eval.jsonl (chat format)
"""
from __future__ import annotations
import json
from pathlib import Path
from core import seed_lessons
from core.chief_engineer import advise
from core.ledger import LedgerManager
from core.models import Environment, GEOMETRY_TYPES, Job, MATERIALS
from core.prompts import build_system_prompt
from learn.policy import LearnedPolicy
try:
from ingest.distill import reference_block
except Exception: # pragma: no cover
def reference_block(_m): # type: ignore
return []
OUT = Path(__file__).resolve().parents[2] / "data" / "finetune"
USER_TURN = "Give your recommendation for THIS job now."
# Grid: hold out two room points for eval so we measure generalization, not recall.
TEMPS_TRAIN = [16, 20, 24, 30, 34]
HUMS_TRAIN = [30, 45, 60, 75]
TEMPS_EVAL = [22, 28]
HUMS_EVAL = [38, 68]
def _pair(ledger: LedgerManager, policy: LearnedPolicy, material: str, geo: str,
temp: float, hum: float) -> dict:
job = Job(geometry_type=geo, material=material)
env = Environment(temp=float(temp), humidity=float(hum))
retrieved = ledger.retrieve(material, geo, env.temp, env.humidity)
refs = reference_block(material)
note = policy.policy_note(material, geo, env)
system = build_system_prompt(job, env, retrieved, refs, note)
advice = advise(job, env, retrieved, refs, note).advice # offline -> deterministic distillation
advice.reasoning = advice.reasoning.replace("[fallback] ", "").strip() # drop the offline marker
# Gemma has no system role: fold the system prompt into the first user turn,
# exactly as the live inference path does (core/llm_zerogpu._build_prompt).
return {"messages": [
{"role": "user", "content": f"{system}\n\n{USER_TURN}"},
{"role": "assistant", "content": advice.model_dump_json()},
]}
def build(temps: list[float], hums: list[float]) -> list[dict]:
ledger = LedgerManager()
seed_lessons.ensure_seeded(ledger)
policy = LearnedPolicy()
rows = []
for m in MATERIALS:
for g in GEOMETRY_TYPES:
for t in temps:
for h in hums:
rows.append(_pair(ledger, policy, m, g, t, h))
return rows
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
train = build(TEMPS_TRAIN, HUMS_TRAIN)
ev = build(TEMPS_EVAL, HUMS_EVAL)
(OUT / "sft.train.jsonl").write_text("\n".join(json.dumps(r) for r in train) + "\n")
(OUT / "sft.eval.jsonl").write_text("\n".join(json.dumps(r) for r in ev) + "\n")
print(f"train={len(train)} eval={len(ev)}{OUT}/sft.train.jsonl + sft.eval.jsonl")
print(f" ({len(MATERIALS)} materials × {len(GEOMETRY_TYPES)} geometries × grid)")
print(" targets = the node's structured Advice (offline = deterministic distillation;")
print(" start `ollama serve` first to distill the live Gemma instead).")
if __name__ == "__main__":
main()