Spaces:
Running on Zero
Running on Zero
deploy: update Space from deploy_preflight --push
Browse files- .codeboarding/logs/wrapper-server.log +6 -0
- Makefile +4 -1
- README.md +2 -2
- app.py +46 -3
- core/deliberation_log.py +125 -0
- core/theme.py +120 -0
- dist/README.md +50 -8
- dist/chief_engineer_ledger.jsonl +0 -13
- dist/deliberation/README.md +37 -0
- dist/deliberation/deliberations.jsonl +44 -0
- learn/finetune/MODEL_CARD.md +23 -17
- learn/finetune/MODEL_CARD_QAT.md +25 -20
- learn/finetune/OLLAMA_PUBLISHING.md +28 -85
- learn/finetune/README.md +12 -24
- learn/finetune/SERVING.md +44 -65
- scripts/export_deliberation.py +204 -0
.codeboarding/logs/wrapper-server.log
CHANGED
|
@@ -342,3 +342,9 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
|
|
| 342 |
[stderr] 2026-06-14 07:40:33 INFO [watchfiles.main:308] 1 change detected
|
| 343 |
[stderr] 2026-06-14 07:40:33 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 344 |
[stderr] 2026-06-14 07:40:33 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
[stderr] 2026-06-14 07:40:33 INFO [watchfiles.main:308] 1 change detected
|
| 343 |
[stderr] 2026-06-14 07:40:33 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 344 |
[stderr] 2026-06-14 07:40:33 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 345 |
+
[stderr] 2026-06-14 08:17:29 INFO [watchfiles.main:308] 3 changes detected
|
| 346 |
+
[stderr] 2026-06-14 08:17:29 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 347 |
+
[stderr] 2026-06-14 08:17:29 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 348 |
+
[stderr] 2026-06-14 08:27:24 INFO [watchfiles.main:308] 1 change detected
|
| 349 |
+
[stderr] 2026-06-14 08:27:24 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 350 |
+
[stderr] 2026-06-14 08:27:24 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
Makefile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
.PHONY: setup setup-zerogpu assets run test demo bench trace preflight deploy-check deploy record record-check record-auto
|
| 2 |
|
| 3 |
# Local dev uses uv (fast, locked). The HF Space still installs via pip+requirements.txt.
|
| 4 |
# Entrypoints: app.py + test_core.py at root; helper scripts live in scripts/ and run
|
|
@@ -38,6 +38,9 @@ bench: ## measure model latency on this hardware (needs Ollama)
|
|
| 38 |
trace: ## export the lesson ledger as a HF Datasets-ready trace
|
| 39 |
uv run python -m scripts.export_trace
|
| 40 |
|
|
|
|
|
|
|
|
|
|
| 41 |
record-check: ## recording preflight (cap-cli + Space + playwright gates)
|
| 42 |
uv run python -m scripts.record --preflight-only
|
| 43 |
|
|
|
|
| 1 |
+
.PHONY: setup setup-zerogpu assets run test demo bench trace deliberation preflight deploy-check deploy record record-check record-auto
|
| 2 |
|
| 3 |
# Local dev uses uv (fast, locked). The HF Space still installs via pip+requirements.txt.
|
| 4 |
# Entrypoints: app.py + test_core.py at root; helper scripts live in scripts/ and run
|
|
|
|
| 38 |
trace: ## export the lesson ledger as a HF Datasets-ready trace
|
| 39 |
uv run python -m scripts.export_trace
|
| 40 |
|
| 41 |
+
deliberation: ## export the multi-persona deliberation as a HF Datasets-ready trace
|
| 42 |
+
uv run python -m scripts.export_deliberation
|
| 43 |
+
|
| 44 |
record-check: ## recording preflight (cap-cli + Space + playwright gates)
|
| 45 |
uv run python -m scripts.record --preflight-only
|
| 46 |
|
README.md
CHANGED
|
@@ -64,7 +64,7 @@ The model **never grades its own outcome**: you click *Printed clean / Sagged /
|
|
| 64 |
run the **simulated world** (a deterministic stand-in for the printer + sensors). Either way the
|
| 65 |
outcome comes from outside the model. The honest signal is what makes the compounding real.
|
| 66 |
|
| 67 |
-
**This is
|
| 68 |
prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
|
| 69 |
setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
|
| 70 |
cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
|
|
@@ -167,7 +167,7 @@ own homework.
|
|
| 167 |
MEDIA TODO (capture from the live Space at ~1600px wide; drop PNGs in assets/screenshots/):
|
| 168 |
β’ hero-build.png: Build page mid-reasoning: O'Brien's precedent read + a 3D risk flag + LAYER scrubber (the hero, top).
|
| 169 |
β’ print-loop.png: Print, the failβclean quality curve + La Forge's per-iteration grades (above).
|
| 170 |
-
β’ second-opinion.png: Build, La Forge's
|
| 171 |
β’ studio.png: Studio, Benchy loaded, material + simulated environment (optional, for the writeup).
|
| 172 |
β’ review-ledger.png: Review, the ledger growing seed β earned β sim (optional).
|
| 173 |
Demo video + social post go in the Links section below once recorded/published.
|
|
|
|
| 64 |
run the **simulated world** (a deterministic stand-in for the printer + sensors). Either way the
|
| 65 |
outcome comes from outside the model. The honest signal is what makes the compounding real.
|
| 66 |
|
| 67 |
+
**This is not just a lookup.** Two knowledge sources feed every recommendation, *RAG* (retrieved
|
| 68 |
prior jobs the LLM reasons over) and a **learned parametric policy** (`learn/policy.py`) that stores
|
| 69 |
setting offsets per (material, geometry, *environment-bucket*) and updates from each outcome. Because
|
| 70 |
cells are bucketed, a lesson from one humid PETG bridge **generalizes** to the next, not-identical
|
|
|
|
| 167 |
MEDIA TODO (capture from the live Space at ~1600px wide; drop PNGs in assets/screenshots/):
|
| 168 |
β’ hero-build.png: Build page mid-reasoning: O'Brien's precedent read + a 3D risk flag + LAYER scrubber (the hero, top).
|
| 169 |
β’ print-loop.png: Print, the failβclean quality curve + La Forge's per-iteration grades (above).
|
| 170 |
+
β’ second-opinion.png: Build, La Forge's second-opinion card disputing a plan (the two-agent moment).
|
| 171 |
β’ studio.png: Studio, Benchy loaded, material + simulated environment (optional, for the writeup).
|
| 172 |
β’ review-ledger.png: Review, the ledger growing seed β earned β sim (optional).
|
| 173 |
Demo video + social post go in the Links section below once recorded/published.
|
app.py
CHANGED
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
| 18 |
|
| 19 |
import os
|
| 20 |
import random
|
|
|
|
| 21 |
|
| 22 |
import gradio as gr
|
| 23 |
|
|
@@ -31,6 +32,7 @@ except ImportError: # not installed locally (base env / offline); decorator no-
|
|
| 31 |
|
| 32 |
spaces = _SpacesShim() # type: ignore
|
| 33 |
|
|
|
|
| 34 |
from core import field_log
|
| 35 |
from core import inspector
|
| 36 |
from core import llm
|
|
@@ -124,6 +126,12 @@ _apply_model_choice("LoRA v3 (QAT E4B)")
|
|
| 124 |
# the Off-Brand skin stays a single removable module. See ../DESIGN.md.
|
| 125 |
|
| 126 |
PRINTER = "Creality Ender 3 V2"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
_SCROLL_TOP = "() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }"
|
| 128 |
|
| 129 |
|
|
@@ -366,14 +374,26 @@ def build_job(part, material, description, temp, humidity, bed_position, model_c
|
|
| 366 |
confirm_vis = gr.update(visible=spine.requires_approval)
|
| 367 |
approval_md = ("**HITL gate:** the Spine clamped a boundary setting β review, then **Confirm & Print**."
|
| 368 |
if spine.requires_approval else "Within safe envelope β ready when you are.")
|
|
|
|
| 369 |
state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
|
| 370 |
-
"advice": rec.advice.model_dump(), "label": part.get("label")}
|
| 371 |
|
| 372 |
# ββ field log (Space only β gated on HF_TOKEN; local/offline no-ops) ββ
|
| 373 |
field_log.log_build(
|
| 374 |
job=state["job"], env=state["env"], settings=state["settings"],
|
| 375 |
advice=state["advice"], backend=rec.backend, used_fallback=rec.used_fallback,
|
| 376 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
return (
|
| 379 |
f"{rec.backend}{fb}", # backend status
|
|
@@ -408,6 +428,10 @@ def second_opinion(state):
|
|
| 408 |
field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
|
| 409 |
"inspector_stance": verdict.stance,
|
| 410 |
"inspector_headline": verdict.headline})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
|
| 412 |
if verdict.stance.lower() == "dispute":
|
| 413 |
panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
|
|
@@ -428,8 +452,13 @@ def toggle_read(choice, state):
|
|
| 428 |
return (gr.update(visible=False), gr.update(visible=True), panel, to_print, override)
|
| 429 |
|
| 430 |
|
| 431 |
-
def ack_override():
|
| 432 |
"""Human overrides the Inspector's dispute β re-open β PRINT (on the operator's call)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
return gr.update(interactive=True), gr.update(visible=False)
|
| 434 |
|
| 435 |
|
|
@@ -500,6 +529,20 @@ def run_print(state, iterations):
|
|
| 500 |
"iterations": len(sess.records), "q_start": round(traj[0], 3),
|
| 501 |
"q_end": round(traj[-1], 3), "first_clean": first,
|
| 502 |
"inspector_stance": run_summary.stance})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
headline = (
|
| 504 |
f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β "
|
| 505 |
f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
|
|
@@ -806,7 +849,7 @@ def build() -> gr.Blocks:
|
|
| 806 |
read_toggle.change(toggle_read, [read_toggle, state],
|
| 807 |
[eng_read_group, second_op_group, second_opinion_panel,
|
| 808 |
to_print_btn, override_btn])
|
| 809 |
-
override_btn.click(ack_override,
|
| 810 |
to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
|
| 811 |
None, None, None, js=_SCROLL_TOP)
|
| 812 |
tabs.select(job_readout, [state], [p_job])
|
|
|
|
| 18 |
|
| 19 |
import os
|
| 20 |
import random
|
| 21 |
+
import uuid
|
| 22 |
|
| 23 |
import gradio as gr
|
| 24 |
|
|
|
|
| 32 |
|
| 33 |
spaces = _SpacesShim() # type: ignore
|
| 34 |
|
| 35 |
+
from core import deliberation_log
|
| 36 |
from core import field_log
|
| 37 |
from core import inspector
|
| 38 |
from core import llm
|
|
|
|
| 126 |
# the Off-Brand skin stays a single removable module. See ../DESIGN.md.
|
| 127 |
|
| 128 |
PRINTER = "Creality Ender 3 V2"
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _delib_ctx(job, env) -> dict:
|
| 132 |
+
"""Context columns shared by every deliberation-log turn for a job."""
|
| 133 |
+
return {"material": job.material, "geometry": job.geometry_type,
|
| 134 |
+
"bed_position": job.bed_position, "env_temp": env.temp, "env_humidity": env.humidity}
|
| 135 |
_SCROLL_TOP = "() => { window.scrollTo({ top: 0, behavior: 'smooth' }); }"
|
| 136 |
|
| 137 |
|
|
|
|
| 374 |
confirm_vis = gr.update(visible=spine.requires_approval)
|
| 375 |
approval_md = ("**HITL gate:** the Spine clamped a boundary setting β review, then **Confirm & Print**."
|
| 376 |
if spine.requires_approval else "Within safe envelope β ready when you are.")
|
| 377 |
+
session_id = uuid.uuid4().hex
|
| 378 |
state = {"job": job.model_dump(), "env": env.model_dump(), "settings": spine.settings.model_dump(),
|
| 379 |
+
"advice": rec.advice.model_dump(), "label": part.get("label"), "session_id": session_id}
|
| 380 |
|
| 381 |
# ββ field log (Space only β gated on HF_TOKEN; local/offline no-ops) ββ
|
| 382 |
field_log.log_build(
|
| 383 |
job=state["job"], env=state["env"], settings=state["settings"],
|
| 384 |
advice=state["advice"], backend=rec.backend, used_fallback=rec.used_fallback,
|
| 385 |
)
|
| 386 |
+
# ββ deliberation log: O'Brien proposes -> Spine vetoes (same gate) ββ
|
| 387 |
+
s = spine.settings
|
| 388 |
+
deliberation_log.log_turns(session_id, "preflight", [
|
| 389 |
+
{"agent": "O'Brien", "act": "propose",
|
| 390 |
+
"content": f"{rec.advice.reasoning} Proposed: nozzle {s.nozzle_temp:.0f}Β°C, bed "
|
| 391 |
+
f"{s.bed_temp:.0f}Β°C, fan {s.fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm."},
|
| 392 |
+
{"agent": "Spine", "act": "veto",
|
| 393 |
+
"stance": "clamped" if spine.requires_approval else "clear",
|
| 394 |
+
"content": ("Clamped: " + " Β· ".join(spine.vetoes)) if spine.vetoes
|
| 395 |
+
else "Within the safe envelope for this material β no clamp."},
|
| 396 |
+
], _delib_ctx(job, env))
|
| 397 |
|
| 398 |
return (
|
| 399 |
f"{rec.backend}{fb}", # backend status
|
|
|
|
| 428 |
field_log.log_event("second_opinion", {"material": job.material, "geometry": job.geometry_type,
|
| 429 |
"inspector_stance": verdict.stance,
|
| 430 |
"inspector_headline": verdict.headline})
|
| 431 |
+
deliberation_log.log_turns(state.get("session_id"), "preflight", [
|
| 432 |
+
{"agent": "La Forge", "act": "second_opinion", "stance": verdict.stance,
|
| 433 |
+
"content": f"{verdict.headline} β {verdict.detail}"},
|
| 434 |
+
], _delib_ctx(job, env))
|
| 435 |
panel = inspector_panel(verdict, label="LA FORGE Β· SECOND OPINION (PRE-PRINT)")
|
| 436 |
if verdict.stance.lower() == "dispute":
|
| 437 |
panel += ("<div style='margin-top:6px;padding:6px 10px;border-left:3px solid var(--ao-red,#d9534f);"
|
|
|
|
| 452 |
return (gr.update(visible=False), gr.update(visible=True), panel, to_print, override)
|
| 453 |
|
| 454 |
|
| 455 |
+
def ack_override(state):
|
| 456 |
"""Human overrides the Inspector's dispute β re-open β PRINT (on the operator's call)."""
|
| 457 |
+
if state and state.get("session_id"):
|
| 458 |
+
deliberation_log.log_turns(state["session_id"], "preflight", [
|
| 459 |
+
{"agent": "Operator", "act": "override", "stance": "override",
|
| 460 |
+
"content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call."},
|
| 461 |
+
], _delib_ctx(Job(**state["job"]), Environment(**state["env"])))
|
| 462 |
return gr.update(interactive=True), gr.update(visible=False)
|
| 463 |
|
| 464 |
|
|
|
|
| 529 |
"iterations": len(sess.records), "q_start": round(traj[0], 3),
|
| 530 |
"q_end": round(traj[-1], 3), "first_clean": first,
|
| 531 |
"inspector_stance": run_summary.stance})
|
| 532 |
+
# ββ deliberation log: World simulates -> La Forge grades, per iteration; then verdict ββ
|
| 533 |
+
ctx = _delib_ctx(job, env)
|
| 534 |
+
loop_turns = []
|
| 535 |
+
for r, g in zip(sess.records, verdicts):
|
| 536 |
+
clamp = " (Spine clamped a setting)" if r.clamped else ""
|
| 537 |
+
loop_turns.append({"agent": "World", "act": "simulate", "stance": r.result.outcome,
|
| 538 |
+
"content": f"Iteration {r.n}: {r.result.detail}.{clamp} Policy: {r.learned}."})
|
| 539 |
+
loop_turns.append({"agent": "La Forge", "act": "grade", "stance": g.stance,
|
| 540 |
+
"content": f"{g.headline} β {g.detail}"})
|
| 541 |
+
deliberation_log.log_turns(state.get("session_id"), "print-loop", loop_turns, ctx)
|
| 542 |
+
deliberation_log.log_turns(state.get("session_id"), "review", [
|
| 543 |
+
{"agent": "La Forge", "act": "verdict", "stance": run_summary.stance,
|
| 544 |
+
"content": f"{run_summary.headline} β {run_summary.detail}"},
|
| 545 |
+
], ctx)
|
| 546 |
headline = (
|
| 547 |
f"**{state.get('label') or geometry_type} Β· {material} @ {env.temp:.0f}Β°C / {env.humidity:.0f}% RH** β "
|
| 548 |
f"started at quality **{traj[0]:.2f}** ({sess.records[0].result.outcome}); "
|
|
|
|
| 849 |
read_toggle.change(toggle_read, [read_toggle, state],
|
| 850 |
[eng_read_group, second_op_group, second_opinion_panel,
|
| 851 |
to_print_btn, override_btn])
|
| 852 |
+
override_btn.click(ack_override, state, [to_print_btn, override_btn])
|
| 853 |
to_print_btn.click(lambda: gr.Tabs(selected="print"), None, [tabs]).then(
|
| 854 |
None, None, None, js=_SCROLL_TOP)
|
| 855 |
tabs.select(job_readout, [state], [p_job])
|
core/deliberation_log.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deliberation log β live, turn-by-turn capture of the personas' argument.
|
| 2 |
+
|
| 3 |
+
Companion to field_log.py. Where the field log records the *interaction config*,
|
| 4 |
+
this records *how the agent reasoned*: each run appends the turns of the
|
| 5 |
+
multi-persona deliberation (O'Brien proposes -> Spine vetoes -> La Forge second
|
| 6 |
+
opinion / dispute -> operator override -> world simulates -> La Forge grades ->
|
| 7 |
+
run verdict) and CommitScheduler pushes them to an open HF Dataset.
|
| 8 |
+
|
| 9 |
+
Same guarantees as field_log:
|
| 10 |
+
- Gated on HF_TOKEN β nothing is written or pushed if the secret is absent.
|
| 11 |
+
- Best-effort + exception-safe β logging never breaks a run.
|
| 12 |
+
- Config + agent reasoning only; no PII, no uploaded mesh files.
|
| 13 |
+
|
| 14 |
+
Schema mirrors scripts/export_deliberation.py (one row per turn) so the live
|
| 15 |
+
dataset and the static export share the same shape:
|
| 16 |
+
session_id, track, turn, agent, role, act, stance, content,
|
| 17 |
+
material, geometry, bed_position, env_temp, env_humidity, ts
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import json
|
| 23 |
+
import os
|
| 24 |
+
import threading
|
| 25 |
+
from datetime import datetime, timezone
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
from typing import Any
|
| 28 |
+
|
| 29 |
+
DELIB_LOG_DIR = Path(__file__).resolve().parent.parent / "deliberation_logs"
|
| 30 |
+
DELIB_LOG_FILE = DELIB_LOG_DIR / "deliberations.jsonl"
|
| 31 |
+
DELIB_LOG_REPO = "kylebrodeur/chief-engineer-deliberation"
|
| 32 |
+
FLUSH_MINUTES = 5
|
| 33 |
+
|
| 34 |
+
ROLE = {
|
| 35 |
+
"O'Brien": "Chief Engineer",
|
| 36 |
+
"La Forge": "QA Inspector",
|
| 37 |
+
"Spine": "Safety Spine",
|
| 38 |
+
"World": "Outcome Simulator",
|
| 39 |
+
"Operator": "Operator",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
_CANON = (
|
| 43 |
+
"session_id", "track", "turn", "agent", "role", "act", "stance", "content",
|
| 44 |
+
"material", "geometry", "bed_position", "env_temp", "env_humidity", "ts",
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
_scheduler: Any = None
|
| 48 |
+
_lock = threading.Lock()
|
| 49 |
+
_turns: dict[str, int] = {} # session_id -> last turn number (single process on the Space)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _get_scheduler():
|
| 53 |
+
"""Lazy-init the CommitScheduler. Returns None if HF_TOKEN is missing."""
|
| 54 |
+
global _scheduler
|
| 55 |
+
token = os.environ.get("HF_TOKEN", "").strip()
|
| 56 |
+
if not token:
|
| 57 |
+
return None
|
| 58 |
+
if _scheduler is None:
|
| 59 |
+
with _lock:
|
| 60 |
+
if _scheduler is None:
|
| 61 |
+
try:
|
| 62 |
+
from huggingface_hub import CommitScheduler
|
| 63 |
+
except ImportError:
|
| 64 |
+
return None
|
| 65 |
+
DELIB_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
| 66 |
+
if not DELIB_LOG_FILE.exists():
|
| 67 |
+
DELIB_LOG_FILE.write_text("", encoding="utf-8")
|
| 68 |
+
_scheduler = CommitScheduler(
|
| 69 |
+
repo_id=DELIB_LOG_REPO,
|
| 70 |
+
repo_type="dataset",
|
| 71 |
+
folder_path=str(DELIB_LOG_DIR),
|
| 72 |
+
every=FLUSH_MINUTES,
|
| 73 |
+
token=token,
|
| 74 |
+
allow_patterns=["*.jsonl"],
|
| 75 |
+
)
|
| 76 |
+
return _scheduler
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def is_active() -> bool:
|
| 80 |
+
"""True if deliberation logging is live (HF_TOKEN present + scheduler ready)."""
|
| 81 |
+
return _get_scheduler() is not None
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _next_turn(session_id: str) -> int:
|
| 85 |
+
with _lock:
|
| 86 |
+
n = _turns.get(session_id, 0) + 1
|
| 87 |
+
_turns[session_id] = n
|
| 88 |
+
return n
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def log_turns(session_id: str, track: str, turns: list[dict], ctx: dict) -> bool:
|
| 92 |
+
"""Append a batch of deliberation turns for one phase of one run.
|
| 93 |
+
|
| 94 |
+
`turns` is a list of {agent, act, content, stance?} dicts; `ctx` carries
|
| 95 |
+
material/geometry/bed_position/env_temp/env_humidity. Gated + exception-safe:
|
| 96 |
+
if HF_TOKEN is unset or anything fails, this is a silent no-op."""
|
| 97 |
+
try:
|
| 98 |
+
sched = _get_scheduler()
|
| 99 |
+
if sched is None or not session_id or not turns:
|
| 100 |
+
return False
|
| 101 |
+
lines: list[str] = []
|
| 102 |
+
for tn in turns:
|
| 103 |
+
agent = tn.get("agent", "")
|
| 104 |
+
row = {k: None for k in _CANON}
|
| 105 |
+
row.update({
|
| 106 |
+
"session_id": session_id, "track": track, "turn": _next_turn(session_id),
|
| 107 |
+
"agent": agent, "role": ROLE.get(agent, agent),
|
| 108 |
+
"act": tn.get("act"), "stance": tn.get("stance", ""),
|
| 109 |
+
"content": (tn.get("content") or "").strip(),
|
| 110 |
+
"material": ctx.get("material"), "geometry": ctx.get("geometry"),
|
| 111 |
+
"bed_position": ctx.get("bed_position"),
|
| 112 |
+
"env_temp": ctx.get("env_temp"), "env_humidity": ctx.get("env_humidity"),
|
| 113 |
+
"ts": datetime.now(timezone.utc).isoformat(),
|
| 114 |
+
})
|
| 115 |
+
lines.append(json.dumps(row, ensure_ascii=False))
|
| 116 |
+
with _lock:
|
| 117 |
+
with DELIB_LOG_FILE.open("a", encoding="utf-8") as f:
|
| 118 |
+
f.write("\n".join(lines) + "\n")
|
| 119 |
+
try:
|
| 120 |
+
sched.trigger()
|
| 121 |
+
except Exception:
|
| 122 |
+
pass
|
| 123 |
+
return True
|
| 124 |
+
except Exception:
|
| 125 |
+
return False # logging is best-effort β never break a run
|
core/theme.py
CHANGED
|
@@ -199,6 +199,61 @@ textarea:focus, input:focus {{ border-color:var(--ao-orange) !important; box-sha
|
|
| 199 |
.ce-icon-printer::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); }}
|
| 200 |
.ce-icon-info::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></svg>"); }}
|
| 201 |
.ce-icon-sliders::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></svg>"); }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
/* ββ IMPLICIT GROUPING (borderless, proximity-based) ββ */
|
| 204 |
/* Model switcher row: sits BELOW the LCARS command bar (right-aligned).
|
|
@@ -455,6 +510,71 @@ CSS += """
|
|
| 455 |
width:8px !important; height:300px !important; }
|
| 456 |
.ce-vslider { display:flex; justify-content:center; }
|
| 457 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
/* segmented toggle (Engineer's Read | Second Opinion) β reuses pill radios, joined */
|
| 459 |
.ce-seg fieldset, .ce-seg .wrap { display:flex !important; gap:0 !important; }
|
| 460 |
.ce-seg label { border-radius:0 !important; border:1px solid var(--ao-outline) !important;
|
|
|
|
| 199 |
.ce-icon-printer::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); }}
|
| 200 |
.ce-icon-info::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><path d='M12 11v5 M12 8h.01'/></svg>"); }}
|
| 201 |
.ce-icon-sliders::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M4 6h10 M18 6h2 M4 12h2 M10 12h10 M4 18h8 M16 18h4 M16 4v4 M6 10v4 M12 16v4'/></svg>"); }}
|
| 202 |
+
.ce-icon-arrow::before {{ -webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>"); }}
|
| 203 |
+
|
| 204 |
+
/* Arrow AFTER text (right-side icon β indicates "next step"). For buttons whose
|
| 205 |
+
label means "proceed to next stage" (SLICE β, PRINT β, REFRESH β). */
|
| 206 |
+
.ce-icon-arrow-after::after {{
|
| 207 |
+
content:""; display:inline-block; width:14px; height:14px;
|
| 208 |
+
margin-left:8px; vertical-align:-2px;
|
| 209 |
+
background:currentColor; -webkit-mask-size:contain; mask-size:contain;
|
| 210 |
+
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
|
| 211 |
+
-webkit-mask-position:center; mask-position:center;
|
| 212 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 213 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 214 |
+
}}
|
| 215 |
+
/* Inside a Gradio button the label is nested in a <span> β attach to button so
|
| 216 |
+
the ::after lands at the end of the visible label, not after the wrapper. */
|
| 217 |
+
.ce-icon-arrow-after button::after, button.ce-icon-arrow-after::after {{
|
| 218 |
+
content:""; display:inline-block; width:14px; height:14px;
|
| 219 |
+
margin-left:8px; vertical-align:-2px;
|
| 220 |
+
background:currentColor; -webkit-mask-size:contain; mask-size:contain;
|
| 221 |
+
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
|
| 222 |
+
-webkit-mask-position:center; mask-position:center;
|
| 223 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 224 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12h14 M13 5l7 7-7 7'/></svg>");
|
| 225 |
+
}}
|
| 226 |
+
/* But ALSO put one inside the inner span so it appears flush with the label text */
|
| 227 |
+
.ce-icon-arrow-after button span::after {{
|
| 228 |
+
content:"\2192"; margin-left:6px; font-weight:900; opacity:.95;
|
| 229 |
+
}}
|
| 230 |
+
|
| 231 |
+
/* ββ TAB ICONS (CSS-only via :nth-child on the four workflow tabs) ββ */
|
| 232 |
+
/* Tab bar order is fixed: LOAD Β· SLICE Β· PRINT Β· REVIEW. Each gets its own icon
|
| 233 |
+
prepended via ::before. The icons are inline-svg via mask-image so they inherit
|
| 234 |
+
the tab's text color (orange-on-dark or dark-on-orange when selected). */
|
| 235 |
+
.tab-nav button::before, button[role="tab"]::before {{
|
| 236 |
+
content:""; display:inline-block; width:13px; height:13px; margin-right:8px;
|
| 237 |
+
vertical-align:-2px; background:currentColor;
|
| 238 |
+
-webkit-mask-size:contain; mask-size:contain;
|
| 239 |
+
-webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
|
| 240 |
+
-webkit-mask-position:center; mask-position:center; }}
|
| 241 |
+
/* LOAD β inbox/upload tray icon */
|
| 242 |
+
.tab-nav button:nth-of-type(1)::before, button[role="tab"]:nth-of-type(1)::before {{
|
| 243 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M22 12h-6l-2 3h-4l-2-3H2 M5.5 6h13l3 6v6a2 2 0 01-2 2h-15a2 2 0 01-2-2v-6l3-6z'/></svg>");
|
| 244 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M22 12h-6l-2 3h-4l-2-3H2 M5.5 6h13l3 6v6a2 2 0 01-2 2h-15a2 2 0 01-2-2v-6l3-6z'/></svg>"); }}
|
| 245 |
+
/* SLICE β layered stack icon */
|
| 246 |
+
.tab-nav button:nth-of-type(2)::before, button[role="tab"]:nth-of-type(2)::before {{
|
| 247 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5'/></svg>");
|
| 248 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5'/></svg>"); }}
|
| 249 |
+
/* PRINT β printer icon */
|
| 250 |
+
.tab-nav button:nth-of-type(3)::before, button[role="tab"]:nth-of-type(3)::before {{
|
| 251 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>");
|
| 252 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9V2h12v7 M6 18H4a2 2 0 01-2-2v-4a2 2 0 012-2h16a2 2 0 012 2v4a2 2 0 01-2 2h-2 M6 14h12v8H6z'/></svg>"); }}
|
| 253 |
+
/* REVIEW β clipboard-check icon */
|
| 254 |
+
.tab-nav button:nth-of-type(4)::before, button[role="tab"]:nth-of-type(4)::before {{
|
| 255 |
+
-webkit-mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 3h6v3H9z M9 5H6a2 2 0 00-2 2v13a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-3 M8 14l3 3 5-6'/></svg>");
|
| 256 |
+
mask-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 3h6v3H9z M9 5H6a2 2 0 00-2 2v13a2 2 0 002 2h12a2 2 0 002-2V7a2 2 0 00-2-2h-3 M8 14l3 3 5-6'/></svg>"); }}
|
| 257 |
|
| 258 |
/* ββ IMPLICIT GROUPING (borderless, proximity-based) ββ */
|
| 259 |
/* Model switcher row: sits BELOW the LCARS command bar (right-aligned).
|
|
|
|
| 510 |
width:8px !important; height:300px !important; }
|
| 511 |
.ce-vslider { display:flex; justify-content:center; }
|
| 512 |
|
| 513 |
+
/* ββ HORIZONTAL LAYER SCRUBBER (replaces the broken vertical slider) ββ */
|
| 514 |
+
/* Spans full width below the slicer image. Filled track left of the thumb,
|
| 515 |
+
tick marks every 5 layers, big thumb, current/max readout above. */
|
| 516 |
+
.ce-hslider { width:100% !important; margin:14px 0 8px !important;
|
| 517 |
+
padding:14px 16px !important;
|
| 518 |
+
background:linear-gradient(180deg, rgba(255,156,0,0.04), transparent);
|
| 519 |
+
border-top:1px solid var(--ao-outline-dim);
|
| 520 |
+
border-bottom:1px solid var(--ao-outline-dim); }
|
| 521 |
+
.ce-hslider > * { width:100% !important; }
|
| 522 |
+
.ce-hslider label, .ce-hslider .head { color:var(--ao-orange) !important;
|
| 523 |
+
letter-spacing:3px; text-transform:uppercase; font-weight:800 !important;
|
| 524 |
+
font-size:11px !important; margin-bottom:6px !important;
|
| 525 |
+
display:flex !important; align-items:center !important; gap:10px !important; }
|
| 526 |
+
.ce-hslider label::before { content:"LAYER"; }
|
| 527 |
+
.ce-hslider label > * { font-size:11px !important; }
|
| 528 |
+
.ce-hslider input[type=range] { width:100% !important; height:14px !important;
|
| 529 |
+
-webkit-appearance:none !important; appearance:none !important;
|
| 530 |
+
background:transparent !important; cursor:pointer; }
|
| 531 |
+
.ce-hslider input[type=range]::-webkit-slider-runnable-track {
|
| 532 |
+
height:6px; background:var(--ao-surface-hi);
|
| 533 |
+
border:1px solid var(--ao-outline-dim);
|
| 534 |
+
background-image:repeating-linear-gradient(90deg,
|
| 535 |
+
var(--ao-outline-dim) 0, var(--ao-outline-dim) 1px,
|
| 536 |
+
transparent 1px, transparent calc(100% / 8)); }
|
| 537 |
+
.ce-hslider input[type=range]::-moz-range-track {
|
| 538 |
+
height:6px; background:var(--ao-surface-hi);
|
| 539 |
+
border:1px solid var(--ao-outline-dim); }
|
| 540 |
+
.ce-hslider input[type=range]::-webkit-slider-thumb {
|
| 541 |
+
-webkit-appearance:none; appearance:none;
|
| 542 |
+
width:20px; height:20px; border-radius:0;
|
| 543 |
+
background:var(--ao-orange); border:2px solid var(--ao-orange);
|
| 544 |
+
margin-top:-7px; cursor:grab;
|
| 545 |
+
box-shadow:0 0 8px rgba(255,156,0,0.6); }
|
| 546 |
+
.ce-hslider input[type=range]::-webkit-slider-thumb:active { cursor:grabbing;
|
| 547 |
+
background:#fff; }
|
| 548 |
+
.ce-hslider input[type=range]::-moz-range-thumb {
|
| 549 |
+
width:20px; height:20px; border-radius:0;
|
| 550 |
+
background:var(--ao-orange); border:2px solid var(--ao-orange);
|
| 551 |
+
cursor:grab; box-shadow:0 0 8px rgba(255,156,0,0.6); }
|
| 552 |
+
/* hide gradio's number-field side (we only want the slider here) */
|
| 553 |
+
.ce-hslider .min, .ce-hslider .max,
|
| 554 |
+
.ce-hslider input[type=number],
|
| 555 |
+
.ce-hslider button[aria-label="Reset"],
|
| 556 |
+
.ce-hslider .source-selection { display:none !important; }
|
| 557 |
+
.ce-hslider .head .v { color:var(--ao-blue); font-weight:800; }
|
| 558 |
+
.ce-hslider .head .max { color:var(--ao-outline); font-weight:600; }
|
| 559 |
+
|
| 560 |
+
/* Virtual print preview β bigger, framed, motion-aware */
|
| 561 |
+
.ce-vp { padding:8px !important; border:1px solid var(--ao-outline-dim) !important;
|
| 562 |
+
background:var(--ao-void) !important; min-height:340px !important;
|
| 563 |
+
display:flex !important; flex-direction:column !important; gap:6px !important; }
|
| 564 |
+
.ce-vp svg { width:100% !important; height:auto !important; max-height:300px !important; }
|
| 565 |
+
.ce-vp .ce-vp-caption { color:var(--ao-outline); font-size:11px;
|
| 566 |
+
letter-spacing:1px; text-transform:uppercase; padding:4px 2px; }
|
| 567 |
+
|
| 568 |
+
/* Mini loader (Second Opinion in-flight β smaller than the full preflight loader).
|
| 569 |
+
Same scanning-bar look but condensed. */
|
| 570 |
+
.ce-mini-loader { display:flex; flex-direction:column; gap:8px;
|
| 571 |
+
padding:14px 16px; min-height:90px;
|
| 572 |
+
background:linear-gradient(180deg, rgba(255,156,0,0.04), transparent 80px);
|
| 573 |
+
border-left:3px solid var(--ao-orange); margin:6px 0; }
|
| 574 |
+
.ce-mini-loader .ce-loader-bar { height:4px; }
|
| 575 |
+
.ce-mini-loader .ce-loader-text { font-size:11px; letter-spacing:2px; }
|
| 576 |
+
.ce-mini-loader .ce-loader-stages > div { font-size:10px; }
|
| 577 |
+
|
| 578 |
/* segmented toggle (Engineer's Read | Second Opinion) β reuses pill radios, joined */
|
| 579 |
.ce-seg fieldset, .ce-seg .wrap { display:flex !important; gap:0 !important; }
|
| 580 |
.ce-seg label { border-radius:0 !important; border:1px solid var(--ao-outline) !important;
|
dist/README.md
CHANGED
|
@@ -1,14 +1,56 @@
|
|
| 1 |
---
|
| 2 |
license: mit
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
---
|
| 6 |
|
| 7 |
-
#
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
tags:
|
| 6 |
+
- 3d-printing
|
| 7 |
+
- additive-manufacturing
|
| 8 |
+
- llm
|
| 9 |
+
- retrieval
|
| 10 |
+
- build-small-hackathon
|
| 11 |
+
pretty_name: "Microfactory Node: Lesson Ledger"
|
| 12 |
+
size_categories:
|
| 13 |
+
- n<1K
|
| 14 |
---
|
| 15 |
|
| 16 |
+
# Microfactory Node: 3D Printer (Lesson Ledger)
|
| 17 |
|
| 18 |
+
The compounding memory of **Microfactory Node: 3D Printer**, a small local Gemma that learns 3D
|
| 19 |
+
printing job by job. Each row is one durable lesson keyed to the conditions it was learned in.
|
| 20 |
+
The node retrieves from this ledger before every print, so job N+1 starts smarter than job N.
|
| 21 |
+
This is the knowledge that usually lives in one maker's head and dies with the shop. Here it
|
| 22 |
+
persists, and anyone can read it.
|
| 23 |
|
| 24 |
+
Project: [Live](https://node.microfactory.space) Β·
|
| 25 |
+
[Code](https://github.com/kylebrodeur/microfactory-lab).
|
| 26 |
+
|
| 27 |
+
## Schema (one JSON object per line)
|
| 28 |
+
|
| 29 |
+
```json
|
| 30 |
+
{"job_id": "seed-004", "material": "PLA", "geometry_type": "overhang",
|
| 31 |
+
"env_temp": 28.0, "env_humidity": 50.0, "outcome": "failed_sag",
|
| 32 |
+
"lesson": "PLA overhang sagged at 28C with the fan low; raise cooling, drop nozzle ~10C.",
|
| 33 |
+
"source": "seed", "timestamp": "2026-05-20T09:00:00Z"}
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
| Field | Meaning |
|
| 37 |
+
|---|---|
|
| 38 |
+
| `material` | PLA, PETG, ABS, TPU |
|
| 39 |
+
| `geometry_type` | overhang, bridge, stringing, adhesion, vase (the failure-mode class) |
|
| 40 |
+
| `env_temp` / `env_humidity` | room conditions the lesson was learned in |
|
| 41 |
+
| `outcome` | success, failed_sag, failed_stringing |
|
| 42 |
+
| `lesson` | one durable, directional sentence (the transferable craft) |
|
| 43 |
+
| `source` | `seed` (curated start), `ingested` (from real configs/prints), `earned` (a real reported outcome), `sim` (the deterministic world) |
|
| 44 |
+
|
| 45 |
+
## How the knowledge is sourced
|
| 46 |
+
|
| 47 |
+
Lessons are grounded, never invented. Seed lessons are hand-curated starters. Ingested lessons
|
| 48 |
+
come from real slicer/firmware profiles and real print history. Earned and simulated lessons come
|
| 49 |
+
from outcomes reported outside the model: the model proposes, a deterministic world or a human
|
| 50 |
+
reports what happened, and only then is a lesson written. The model never grades its own work.
|
| 51 |
+
|
| 52 |
+
## Honest limits
|
| 53 |
+
|
| 54 |
+
Small and deliberately so. The simulated outcomes are a physics-lite stand-in for a printer, used
|
| 55 |
+
to run the closed loop on camera; they are labeled as such. Treat the lessons as a maker's
|
| 56 |
+
notebook: directionally true, condition-keyed, and meant to be checked against your own machine.
|
dist/chief_engineer_ledger.jsonl
CHANGED
|
@@ -24,16 +24,3 @@
|
|
| 24 |
{"job_id":"ingest-009","material":"TPU","geometry_type":"stringing","env_temp":22.0,"env_humidity":35.0,"outcome":"success","lesson":"TPU printed clean with 3mm retraction at 25mm/s in a dry room. TPU's flexibility means retraction must be slow and short β fast retraction stretches the filament instead of pulling it back.","source":"ingested","timestamp":"2026-06-12T12:25:00Z"}
|
| 25 |
{"job_id":"ingest-010","material":"PETG","geometry_type":"adhesion","env_temp":22.0,"env_humidity":50.0,"outcome":"success","lesson":"PETG adhesion is strong on PEI at 80C bed β too strong on smooth PEI without release agent. Use textured PEI or glue stick as a release layer, not an adhesion aid, for PETG.","source":"ingested","timestamp":"2026-06-12T12:30:00Z"}
|
| 26 |
{"job_id":"octo-petg-stringy-240","material":"PETG","geometry_type":"stringing","env_temp":22.0,"env_humidity":50.0,"outcome":"failed_stringing","lesson":"My own PETG print at 240C nozzle came out very stringy on a retraction-heavy part. PETG strings when the nozzle runs hot or the spool is damp - dry the spool, drop ~10C, and increase retraction.","source":"ingested","timestamp":"2024-05-20T20:57:00+00:00"}
|
| 27 |
-
{"job_id":"sim-035004454637","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.39) with nozzle 235Β°C, fan 40%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.454661+00:00"}
|
| 28 |
-
{"job_id":"sim-035004458175","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.47) with nozzle 232Β°C, fan 52%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.458194+00:00"}
|
| 29 |
-
{"job_id":"sim-035004458709","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.53) with nozzle 229Β°C, fan 64%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.458722+00:00"}
|
| 30 |
-
{"job_id":"sim-035004459140","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.54) with nozzle 226Β°C, fan 76%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.459152+00:00"}
|
| 31 |
-
{"job_id":"sim-035004459597","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.55) with nozzle 223Β°C, fan 88%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.459609+00:00"}
|
| 32 |
-
{"job_id":"sim-035004459987","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.56) with nozzle 220Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.459999+00:00"}
|
| 33 |
-
{"job_id":"sim-035004461203","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.68) with nozzle 225Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.461215+00:00"}
|
| 34 |
-
{"job_id":"sim-035004461453","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.61) with nozzle 222Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.461461+00:00"}
|
| 35 |
-
{"job_id":"sim-035004462543","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.462561+00:00"}
|
| 36 |
-
{"job_id":"sim-035004462854","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.462864+00:00"}
|
| 37 |
-
{"job_id":"sim-035004463172","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.463184+00:00"}
|
| 38 |
-
{"job_id":"sim-035004463498","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"success","lesson":"[sim] PETG bridge at 32Β°C/52% RH printed clean (q=0.73) with nozzle 227Β°C, fan 100%, retraction 4.0mm.","source":"sim","timestamp":"2026-06-13T03:50:04.463507+00:00"}
|
| 39 |
-
{"job_id":"sim-035039090556","material":"PETG","geometry_type":"bridge","env_temp":32.0,"env_humidity":52.0,"outcome":"failed_sag","lesson":"[sim] PETG bridge at 32Β°C/52% RH sagged (q=0.39) with nozzle 245Β°C, fan 40%, retraction 5.0mm.","source":"sim","timestamp":"2026-06-13T03:50:39.090586+00:00"}
|
|
|
|
| 24 |
{"job_id":"ingest-009","material":"TPU","geometry_type":"stringing","env_temp":22.0,"env_humidity":35.0,"outcome":"success","lesson":"TPU printed clean with 3mm retraction at 25mm/s in a dry room. TPU's flexibility means retraction must be slow and short β fast retraction stretches the filament instead of pulling it back.","source":"ingested","timestamp":"2026-06-12T12:25:00Z"}
|
| 25 |
{"job_id":"ingest-010","material":"PETG","geometry_type":"adhesion","env_temp":22.0,"env_humidity":50.0,"outcome":"success","lesson":"PETG adhesion is strong on PEI at 80C bed β too strong on smooth PEI without release agent. Use textured PEI or glue stick as a release layer, not an adhesion aid, for PETG.","source":"ingested","timestamp":"2026-06-12T12:30:00Z"}
|
| 26 |
{"job_id":"octo-petg-stringy-240","material":"PETG","geometry_type":"stringing","env_temp":22.0,"env_humidity":50.0,"outcome":"failed_stringing","lesson":"My own PETG print at 240C nozzle came out very stringy on a retraction-heavy part. PETG strings when the nozzle runs hot or the spool is damp - dry the spool, drop ~10C, and increase retraction.","source":"ingested","timestamp":"2024-05-20T20:57:00+00:00"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dist/deliberation/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
task_categories: [text-generation]
|
| 4 |
+
language: [en]
|
| 5 |
+
tags: [3d-printing, additive-manufacturing, agent-trace, multi-agent, deliberation, build-small-hackathon]
|
| 6 |
+
pretty_name: Chief Engineer β Deliberation Traces
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# Chief Engineer β Deliberation Traces
|
| 10 |
+
|
| 11 |
+
Turn-by-turn **multi-persona deliberation** from **The Chief Engineer**, a small local
|
| 12 |
+
Gemma agent built for the HF Build Small hackathon (Backyard AI). Where the
|
| 13 |
+
[lesson ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
|
| 14 |
+
records *what the agent learned*, this records *how it reasons*: the argument between
|
| 15 |
+
the personas on each job. It grows two ways: a reproducible static export
|
| 16 |
+
(`make deliberation`) and **live turns logged on every run of the Space** (gated on
|
| 17 |
+
`HF_TOKEN`; config + agent reasoning only, never PII or uploaded files).
|
| 18 |
+
|
| 19 |
+
Each row is one **turn**:
|
| 20 |
+
|
| 21 |
+
- **O'Brien** (Chief Engineer) β proposes settings + reasoning over precedent.
|
| 22 |
+
- **Spine** (Safety Spine) β deterministically vetoes/clamps unsafe values.
|
| 23 |
+
- **La Forge** (QA Inspector) β a separate, skeptical voice: second opinion before the
|
| 24 |
+
print (`concur` / `caution` / `dispute`), a grade on each run, and a run verdict.
|
| 25 |
+
- **Operator** β the human, who can override a `dispute` and proceed.
|
| 26 |
+
- **World** (Outcome Simulator) β the deterministic physics-lite world that reports the
|
| 27 |
+
actual print outcome (the agent never grades its own work).
|
| 28 |
+
|
| 29 |
+
The integrity rule made literal: the proposer never marks its own homework.
|
| 30 |
+
|
| 31 |
+
## Schema
|
| 32 |
+
|
| 33 |
+
`session_id, track, turn, agent, role, act, stance, content, material, geometry,
|
| 34 |
+
bed_position, env_temp, env_humidity, ts`
|
| 35 |
+
|
| 36 |
+
`track` is the phase β `preflight` (propose β veto β second opinion β override),
|
| 37 |
+
`print-loop` (simulate β grade, per iteration), `review` (run verdict).
|
dist/deliberation/deliberations.jsonl
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists, so I reason from ABS properties and the current environment. ABS is prone to warping and requires high thermal management; 26Β°C ambient is acceptable but not ideal. The overhang geometry demands careful temperature control to prevent sag or delamination. We must set temps aggressively to keep the plastic fluid enough for the overhang while minimizing residual stress.\nProposed: nozzle 255Β°C, bed 100Β°C, fan 30%, first-layer fan 0%, retraction 4.0mm.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:07+00:00"}
|
| 2 |
+
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:14+00:00"}
|
| 3 |
+
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "dispute", "content": "Temperatures are too aggressive and unvalidated. β The proposed high temps increase warping risk significantly, especially with unknown thermal chamber performance.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:21+00:00"}
|
| 4 |
+
{"session_id": "abs-overhang-edge", "track": "preflight", "turn": 4, "agent": "Operator", "role": "Operator", "act": "override", "stance": "override", "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:28+00:00"}
|
| 5 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 5, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 1: sag (quality 0.19, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.19) β learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:35+00:00"}
|
| 6 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 6, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β exactly as called. β Quality 0.19. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:42+00:00"}
|
| 7 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 7, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 2: sag (quality 0.27, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.27) β learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:49+00:00"}
|
| 8 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 8, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β exactly as called. β Quality 0.27. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:00:56+00:00"}
|
| 9 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 9, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 3: sag (quality 0.33, dominant cost: sag). Policy: ABS/overhang/mid/humid: sag (q=0.33) β learned: fan_pct +12, nozzle_temp -3.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:03+00:00"}
|
| 10 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "confirmed", "content": "Failed on sagging β exactly as called. β Quality 0.33. The Engineer's risk flag was right; the loop now has the lesson.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:10+00:00"}
|
| 11 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 11, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "failed_sag", "content": "Iteration 4: warp (quality 0.34, dominant cost: warp). Policy: ABS/overhang/mid/humid: warp (q=0.34) β learned: fan_pct -10.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:17+00:00"}
|
| 12 |
+
{"session_id": "abs-overhang-edge", "track": "print-loop", "turn": 12, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "missed", "content": "Failed on warping β and it wasn't flagged. β Quality 0.34. The Engineer didn't predict this mode. That gap is what the next iteration has to close.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:24+00:00"}
|
| 13 |
+
{"session_id": "abs-overhang-edge", "track": "review", "turn": 13, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "caution", "content": "Improving but not yet clean (best 0.34). β Quality rose 0.19 β 0.34 over 4 runs but never crossed the bar. More iterations or a different lever needed β the loop is learning, slowly.", "material": "ABS", "geometry": "overhang", "bed_position": "edge", "env_temp": 26.0, "env_humidity": 60.0, "ts": "2026-06-14T12:01:31+00:00"}
|
| 14 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The prior job [1] is the closest precedent, involving PETG and an overhang. The lesson learnedβthat PETG needs a cooling compromiseβis critical here. I will maintain the fan settings from that job's failure analysis (60-70% over overhang) because it directly addresses the material/geometry interaction. Since the environment (24Β°C, 55% RH) is very close to the precedent's conditions, the core temperature and cooling strategy remains valid.\nProposed: nozzle 245Β°C, bed 85Β°C, fan 65%, first-layer fan 0%, retraction 5.0mm.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:38+00:00"}
|
| 15 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:45+00:00"}
|
| 16 |
+
{"session_id": "petg-overhang-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Cooling compromise ignores thermal gradient risk. β The 65% fan setting risks rapid cooling and warping on PETG overhangs, especially if local drafts exceed ambient control.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:52+00:00"}
|
| 17 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:01:59+00:00"}
|
| 18 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β Engineer flagged sag; the settings covered it. Credit the call β or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:06+00:00"}
|
| 19 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:13+00:00"}
|
| 20 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β Engineer flagged sag; the settings covered it. Credit the call β or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:20+00:00"}
|
| 21 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.73). Policy: PETG/overhang/mid/mid: success (q=0.73) β holding policy.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:27+00:00"}
|
| 22 |
+
{"session_id": "petg-overhang-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.73). β Engineer flagged sag; the settings covered it. Credit the call β or it was conservative.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:34+00:00"}
|
| 23 |
+
{"session_id": "petg-overhang-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β PETG/overhang climbed 0.73 β 0.73. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PETG", "geometry": "overhang", "bed_position": "center", "env_temp": 24.0, "env_humidity": 55.0, "ts": "2026-06-14T12:02:41+00:00"}
|
| 24 |
+
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "The closest precedent [1] shows that cold ambient temperature (19Β°C) severely compromised first-layer adhesion, requiring a significant bed bump and fan shutdown. Since the current room is 21Β°C, which is only slightly warmer than the failed job's environment, we must apply the core lesson: prioritize thermal energy transfer to the build plate. We will maintain the elevated bed temperature and disable cooling for initial layers to maximize bonding strength.\nProposed: nozzle 215Β°C, bed 60Β°C, fan 0%, first-layer fan 0%, retraction 5.0mm.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:48+00:00"}
|
| 25 |
+
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:02:55+00:00"}
|
| 26 |
+
{"session_id": "pla-adhesion-center", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "caution", "content": "Thermal gradient assumption is weak. β A slight ambient bump does not guarantee adhesion success. The print environment must be stable, not just marginally improved.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:02+00:00"}
|
| 27 |
+
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 4, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:09+00:00"}
|
| 28 |
+
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 5, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:16+00:00"}
|
| 29 |
+
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 6, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:23+00:00"}
|
| 30 |
+
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 7, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:30+00:00"}
|
| 31 |
+
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 8, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 1.00). Policy: PLA/adhesion/mid/mid: success (q=1.00) β holding policy.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:37+00:00"}
|
| 32 |
+
{"session_id": "pla-adhesion-center", "track": "print-loop", "turn": 9, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=1.00). β Engineer flagged adhesion; the settings covered it. Credit the call β or it was conservative.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:44+00:00"}
|
| 33 |
+
{"session_id": "pla-adhesion-center", "track": "review", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β PLA/adhesion climbed 1.00 β 1.00. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "PLA", "geometry": "adhesion", "bed_position": "center", "env_temp": 21.0, "env_humidity": 45.0, "ts": "2026-06-14T12:03:51+00:00"}
|
| 34 |
+
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 1, "agent": "O'Brien", "role": "Chief Engineer", "act": "propose", "stance": "", "content": "No close precedent exists. TPU is flexible and prone to stringing because of its high elasticity and low melt strength, especially when ambient conditions are humid (65% RH). The goal is minimizing oozing without compromising print speed or layer adhesion. We need a balance: enough heat to flow the material but not so much that it overheats and degrades, and aggressive cooling where possible.\nProposed: nozzle 230Β°C, bed 45Β°C, fan 60%, first-layer fan 0%, retraction 1.5mm.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:03:58+00:00"}
|
| 35 |
+
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 2, "agent": "Spine", "role": "Safety Spine", "act": "veto", "stance": "clear", "content": "Within the safe envelope for this material β no clamp.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:05+00:00"}
|
| 36 |
+
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 3, "agent": "La Forge", "role": "QA Inspector", "act": "second_opinion", "stance": "dispute", "content": "High heat settings for TPU are risky overkill. β 230Β°C nozzle risks degradation and excessive ooze. Lower it and manage cooling/retraction better.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:12+00:00"}
|
| 37 |
+
{"session_id": "tpu-stringing-edge", "track": "preflight", "turn": 4, "agent": "Operator", "role": "Operator", "act": "override", "stance": "override", "content": "Acknowledged La Forge's objection. Proceeding to print on the operator's call.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:19+00:00"}
|
| 38 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 5, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 1: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:26+00:00"}
|
| 39 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 6, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β Engineer flagged stringing; the settings covered it. Credit the call β or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:33+00:00"}
|
| 40 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 7, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 2: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:40+00:00"}
|
| 41 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 8, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β Engineer flagged stringing; the settings covered it. Credit the call β or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:47+00:00"}
|
| 42 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 9, "agent": "World", "role": "Outcome Simulator", "act": "simulate", "stance": "success", "content": "Iteration 3: clean print (quality 0.86). Policy: TPU/stringing/mid/humid: success (q=0.86) β holding policy.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:04:54+00:00"}
|
| 43 |
+
{"session_id": "tpu-stringing-edge", "track": "print-loop", "turn": 10, "agent": "La Forge", "role": "QA Inspector", "act": "grade", "stance": "overcautious", "content": "Print held (q=0.86). β Engineer flagged stringing; the settings covered it. Credit the call β or it was conservative.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:01+00:00"}
|
| 44 |
+
{"session_id": "tpu-stringing-edge", "track": "review", "turn": 11, "agent": "La Forge", "role": "QA Inspector", "act": "verdict", "stance": "concur", "content": "Converged to clean by iteration 1. β TPU/stringing climbed 0.86 β 0.86. The compounding is real: each simulated outcome tightened the policy and the next run was better-informed.", "material": "TPU", "geometry": "stringing", "bed_position": "edge", "env_temp": 23.0, "env_humidity": 65.0, "ts": "2026-06-14T12:05:08+00:00"}
|
learn/finetune/MODEL_CARD.md
CHANGED
|
@@ -13,17 +13,14 @@ tags:
|
|
| 13 |
|
| 14 |
# Microfactory Node: 3D Printer (LoRA v2)
|
| 15 |
|
| 16 |
-
|
| 17 |
-
the weights of Gemma 4 E4B (8B). The live node uses retrieval over a lesson
|
| 18 |
-
ledger; this adapter is the "bake it into the weights" frontier.
|
| 19 |
|
| 20 |
## What it does
|
| 21 |
|
| 22 |
-
|
| 23 |
-
outputs structured **Advice JSON** with:
|
| 24 |
- **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
|
| 25 |
- **Risk regions**: where on the part, what risk, why, anchor hint
|
| 26 |
-
- **Reasoning**:
|
| 27 |
|
| 28 |
## Training
|
| 29 |
|
|
@@ -40,16 +37,13 @@ outputs structured **Advice JSON** with:
|
|
| 40 |
| GPU | NVIDIA A10G (24GB) |
|
| 41 |
| Framework | TRL SFTTrainer + transformers 5.x |
|
| 42 |
|
|
|
|
|
|
|
| 43 |
## Dataset
|
| 44 |
|
| 45 |
-
|
| 46 |
-
4 materials Γ 5 geometries Γ 3 temperatures Γ 3 humidities (train) and
|
| 47 |
-
2 temperatures Γ 2 humidities (eval). Each example is a chat-format pair:
|
| 48 |
-
system prompt describing the job β structured Advice JSON response.
|
| 49 |
|
| 50 |
-
|
| 51 |
-
template memorization β the v1 adapter (Gemma 3, deterministic targets, r=16,
|
| 52 |
-
3 epochs) parroted identical settings for every input.
|
| 53 |
|
| 54 |
## Usage
|
| 55 |
|
|
@@ -74,9 +68,7 @@ print(tok.decode(out[0], skip_special_tokens=True))
|
|
| 74 |
|
| 75 |
## Safety
|
| 76 |
|
| 77 |
-
This adapter
|
| 78 |
-
all settings against material bounds before any printer sees them. The LoRA
|
| 79 |
-
proposes; the Spine vetoes.
|
| 80 |
|
| 81 |
## Iteration history
|
| 82 |
|
|
@@ -85,9 +77,23 @@ proposes; the Spine vetoes.
|
|
| 85 |
| v1 | gemma-3-1b-it | r=16 | 3 | deterministic | β Parroted template |
|
| 86 |
| **v2** | **gemma-4-E4B-it** | **r=4** | **1** | **live-generated** | **β
Well-Tuned (100% JSON-valid, 100% Spine-safe, real judgment)** |
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
## Try it via GGUF (Ollama / llama.cpp)
|
| 89 |
|
| 90 |
-
A quantized GGUF of this adapter
|
| 91 |
[`kylebrodeur/microfactory-node-gguf` Β· `microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf)
|
| 92 |
(5.1 GB, q4_k_m) and on the public Ollama registry:
|
| 93 |
|
|
|
|
| 13 |
|
| 14 |
# Microfactory Node: 3D Printer (LoRA v2)
|
| 15 |
|
| 16 |
+
I trained this LoRA to bake Chief Engineer O'Brien's judgment into Gemma 4 E4B. The live node still reads from the lesson ledger; this adapter is what happens when I try to put that ledger into the weights instead.
|
|
|
|
|
|
|
| 17 |
|
| 18 |
## What it does
|
| 19 |
|
| 20 |
+
Give it a print job β material, geometry, room temperature and humidity β and it returns structured **Advice JSON**:
|
|
|
|
| 21 |
- **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
|
| 22 |
- **Risk regions**: where on the part, what risk, why, anchor hint
|
| 23 |
+
- **Reasoning**: what transfers from prior knowledge and why
|
| 24 |
|
| 25 |
## Training
|
| 26 |
|
|
|
|
| 37 |
| GPU | NVIDIA A10G (24GB) |
|
| 38 |
| Framework | TRL SFTTrainer + transformers 5.x |
|
| 39 |
|
| 40 |
+
I kept rank low and epochs at one on purpose. v1 used r=16 for three epochs on deterministic targets and parroted the same settings for every input. This run sacrifices raw capacity for actual attention to the job.
|
| 41 |
+
|
| 42 |
## Dataset
|
| 43 |
|
| 44 |
+
I generated the training set by driving the base model across a grid of 4 materials Γ 5 geometries Γ 3 temperatures Γ 3 humidities (train), with 2 temperatures Γ 2 humidities held out for eval. Each example is a chat-format pair: system prompt describing the job β structured Advice JSON response.
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
I kept targets noisy β temperature=0.7, top_p=0.95 β so the model cannot memorize a single template. v1 proved that deterministic targets and a high rank just produce a parrot. Noise forces judgment.
|
|
|
|
|
|
|
| 47 |
|
| 48 |
## Usage
|
| 49 |
|
|
|
|
| 68 |
|
| 69 |
## Safety
|
| 70 |
|
| 71 |
+
This adapter proposes settings. It does not validate them. A deterministic Spine clamps every proposed value against hard material bounds before any printer sees them. The LoRA gives the opinion; the Spine has the veto.
|
|
|
|
|
|
|
| 72 |
|
| 73 |
## Iteration history
|
| 74 |
|
|
|
|
| 77 |
| v1 | gemma-3-1b-it | r=16 | 3 | deterministic | β Parroted template |
|
| 78 |
| **v2** | **gemma-4-E4B-it** | **r=4** | **1** | **live-generated** | **β
Well-Tuned (100% JSON-valid, 100% Spine-safe, real judgment)** |
|
| 79 |
|
| 80 |
+
v1 taught me what not to do.
|
| 81 |
+
|
| 82 |
+
## Limitations
|
| 83 |
+
|
| 84 |
+
This adapter is narrow by design, and it will fail loudly outside that narrow band.
|
| 85 |
+
|
| 86 |
+
- **Materials and geometries outside the training grid** β The grid covered four materials and five geometries. Hand it an exotic filament or an unusual geometry and it will guess confidently. That guess is extrapolation, not recall.
|
| 87 |
+
- **Humid PETG stringing** β Small Gemmas can return perfectly valid JSON with bad physics. During early driving I saw a lesson recommend slightly higher nozzle temperature to fight humid-PETG stringing, when the correct move is lower. Schema validation does not catch that. The human reads the plan before it runs.
|
| 88 |
+
- **Multi-tool or multi-material prints** β These were not in the training grid. Expect invented tool-change behavior.
|
| 89 |
+
- **ABS without an enclosure** β The model may propose settings that ignore chamber drafts. The Spine clamps individual values, but it does not model enclosure physics.
|
| 90 |
+
- **Mechanically risky combinations** β Very small layer heights paired with aggressive retraction can pass JSON schema and still fail on the bed. That is why La Forge inspects and the human decides.
|
| 91 |
+
- **No live sensor feedback** β It predicts from precedent and stops. It does not see actual bed adhesion, layer curling, or nozzle state. The printer and the human close the loop.
|
| 92 |
+
- **Single-epoch, low-rank LoRA** β It has not deeply rewritten the base model. Ask it something far from 3D printing and it answers like base Gemma. That is intentional.
|
| 93 |
+
|
| 94 |
## Try it via GGUF (Ollama / llama.cpp)
|
| 95 |
|
| 96 |
+
A quantized GGUF of this adapter, merged into the base model, is published as
|
| 97 |
[`kylebrodeur/microfactory-node-gguf` Β· `microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf)
|
| 98 |
(5.1 GB, q4_k_m) and on the public Ollama registry:
|
| 99 |
|
learn/finetune/MODEL_CARD_QAT.md
CHANGED
|
@@ -14,20 +14,14 @@ tags:
|
|
| 14 |
|
| 15 |
# Microfactory Node: 3D Printer (LoRA v3 QAT)
|
| 16 |
|
| 17 |
-
|
| 18 |
-
the weights of the QAT-trained `gemma-4-E4B-it-qat-q4_0-unquantized` model.
|
| 19 |
-
|
| 20 |
-
This v3 iteration runs parallel to the standard v2 iteration, exploring whether
|
| 21 |
-
fine-tuning directly on a Quantization-Aware-Trained (QAT) base yields higher
|
| 22 |
-
quality after GGUF conversion and merging.
|
| 23 |
|
| 24 |
## What it does
|
| 25 |
|
| 26 |
-
|
| 27 |
-
outputs structured **Advice JSON** with:
|
| 28 |
- **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
|
| 29 |
- **Risk regions**: where on the part, what risk, why, anchor hint
|
| 30 |
-
- **Reasoning**:
|
| 31 |
|
| 32 |
## Training
|
| 33 |
|
|
@@ -44,15 +38,13 @@ outputs structured **Advice JSON** with:
|
|
| 44 |
| GPU | NVIDIA A10G (24GB) |
|
| 45 |
| Framework | TRL SFTTrainer + transformers 5.x |
|
| 46 |
|
|
|
|
|
|
|
| 47 |
## Dataset
|
| 48 |
|
| 49 |
-
|
| 50 |
-
4 materials Γ 5 geometries Γ 3 temperatures Γ 3 humidities (train) and
|
| 51 |
-
2 temperatures Γ 2 humidities (eval). Each example is a chat-format pair:
|
| 52 |
-
system prompt describing the job β structured Advice JSON response.
|
| 53 |
|
| 54 |
-
|
| 55 |
-
template memorization.
|
| 56 |
|
| 57 |
## Usage
|
| 58 |
|
|
@@ -77,9 +69,7 @@ print(tok.decode(out[0], skip_special_tokens=True))
|
|
| 77 |
|
| 78 |
## Safety
|
| 79 |
|
| 80 |
-
This adapter
|
| 81 |
-
all settings against material bounds before any printer sees them. The LoRA
|
| 82 |
-
proposes; the Spine vetoes.
|
| 83 |
|
| 84 |
## Iteration history
|
| 85 |
|
|
@@ -89,9 +79,24 @@ proposes; the Spine vetoes.
|
|
| 89 |
| v2 | gemma-4-E4B-it | r=4 | 1 | live-generated | β
Well-Tuned |
|
| 90 |
| **v3** | **gemma-4-E4B-it-qat-q4_0-unquantized** | **r=4** | **1** | **live-generated** | **β
Well-Tuned (QAT-trained β better fidelity after q4_0 quant)** |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
## Try it via GGUF (Ollama / llama.cpp)
|
| 93 |
|
| 94 |
-
Two quantized GGUFs of this adapter
|
| 95 |
Both live in [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf)
|
| 96 |
and on the public Ollama registry:
|
| 97 |
|
|
@@ -117,4 +122,4 @@ for the merge β quantize β upload pipeline. The non-QAT sibling lives at
|
|
| 117 |
|
| 118 |
## License
|
| 119 |
|
| 120 |
-
This adapter inherits the [Gemma license](https://ai.google.dev/gemma/terms) from its base model.
|
|
|
|
| 14 |
|
| 15 |
# Microfactory Node: 3D Printer (LoRA v3 QAT)
|
| 16 |
|
| 17 |
+
I trained this LoRA on top of the QAT-trained `gemma-4-E4B-it-qat-q4_0-unquantized` base. It runs parallel to v2: the same O'Brien judgment, but I wanted to see if fine-tuning on a Quantization-Aware-Trained base keeps more quality after q4_0 GGUF conversion.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
## What it does
|
| 20 |
|
| 21 |
+
Give it a print job β material, geometry, room temperature and humidity β and it returns structured **Advice JSON**:
|
|
|
|
| 22 |
- **Settings**: nozzle_temp, bed_temp, retraction_mm, fan_pct, first_layer_fan_pct
|
| 23 |
- **Risk regions**: where on the part, what risk, why, anchor hint
|
| 24 |
+
- **Reasoning**: what transfers from prior knowledge and why
|
| 25 |
|
| 26 |
## Training
|
| 27 |
|
|
|
|
| 38 |
| GPU | NVIDIA A10G (24GB) |
|
| 39 |
| Framework | TRL SFTTrainer + transformers 5.x |
|
| 40 |
|
| 41 |
+
Same low-rank, single-epoch setup as v2. The variable is the QAT base.
|
| 42 |
+
|
| 43 |
## Dataset
|
| 44 |
|
| 45 |
+
I generated the training set by driving the base model across a grid of 4 materials Γ 5 geometries Γ 3 temperatures Γ 3 humidities (train), with 2 temperatures Γ 2 humidities held out for eval. Each example is a chat-format pair: system prompt describing the job β structured Advice JSON response.
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
I kept targets noisy β temperature=0.7, top_p=0.95 β to prevent template memorization.
|
|
|
|
| 48 |
|
| 49 |
## Usage
|
| 50 |
|
|
|
|
| 69 |
|
| 70 |
## Safety
|
| 71 |
|
| 72 |
+
This adapter proposes settings. It does not validate them. A deterministic Spine clamps every proposed value against hard material bounds before any printer sees them. The LoRA gives the opinion; the Spine has the veto.
|
|
|
|
|
|
|
| 73 |
|
| 74 |
## Iteration history
|
| 75 |
|
|
|
|
| 79 |
| v2 | gemma-4-E4B-it | r=4 | 1 | live-generated | β
Well-Tuned |
|
| 80 |
| **v3** | **gemma-4-E4B-it-qat-q4_0-unquantized** | **r=4** | **1** | **live-generated** | **β
Well-Tuned (QAT-trained β better fidelity after q4_0 quant)** |
|
| 81 |
|
| 82 |
+
v1 taught me what not to do. v3 tests whether QAT pre-training helps the quantized artifact.
|
| 83 |
+
|
| 84 |
+
## Limitations
|
| 85 |
+
|
| 86 |
+
This adapter is narrow by design, and it will fail loudly outside that narrow band.
|
| 87 |
+
|
| 88 |
+
- **Materials and geometries outside the training grid** β The grid covered four materials and five geometries. Hand it an exotic filament or an unusual geometry and it will guess confidently. That guess is extrapolation, not recall.
|
| 89 |
+
- **Humid PETG stringing** β Small Gemmas can return perfectly valid JSON with bad physics. During early driving I saw a lesson recommend slightly higher nozzle temperature to fight humid-PETG stringing, when the correct move is lower. Schema validation does not catch that. The human reads the plan before it runs.
|
| 90 |
+
- **Multi-tool or multi-material prints** β These were not in the training grid. Expect invented tool-change behavior.
|
| 91 |
+
- **ABS without an enclosure** β The model may propose settings that ignore chamber drafts. The Spine clamps individual values, but it does not model enclosure physics.
|
| 92 |
+
- **Mechanically risky combinations** β Very small layer heights paired with aggressive retraction can pass JSON schema and still fail on the bed. That is why La Forge inspects and the human decides.
|
| 93 |
+
- **No live sensor feedback** β It predicts from precedent and stops. It does not see actual bed adhesion, layer curling, or nozzle state. The printer and the human close the loop.
|
| 94 |
+
- **QAT-specific quant mismatch** β The QAT base was trained for q4_0. If you pick q4_k_m you get a balanced default, but it is slightly off the quant the base prepared for. Use q4_0 for highest fidelity.
|
| 95 |
+
- **Single-epoch, low-rank LoRA on a specialized base** β It has not deeply rewritten base knowledge, and the QAT base is already a specialized artifact. Ask it something far from 3D printing and it may behave less like general Gemma than v2 does. That is the trade-off.
|
| 96 |
+
|
| 97 |
## Try it via GGUF (Ollama / llama.cpp)
|
| 98 |
|
| 99 |
+
Two quantized GGUFs of this adapter, merged into the QAT base, are published.
|
| 100 |
Both live in [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf)
|
| 101 |
and on the public Ollama registry:
|
| 102 |
|
|
|
|
| 122 |
|
| 123 |
## License
|
| 124 |
|
| 125 |
+
This adapter inherits the [Gemma license](https://ai.google.dev/gemma/terms) from its base model.
|
learn/finetune/OLLAMA_PUBLISHING.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
# Publishing GGUFs to ollama.com β runbook & decision log
|
| 2 |
|
| 3 |
-
Date: 2026-06-14. This documents how the Chief Engineer GGUFs got listed on the
|
| 4 |
-
public Ollama registry at `ollama.com/kylebrodeur/microfactory-node-*`, so the
|
| 5 |
-
process is reproducible (next adapter, next quant, next mistake to recover from).
|
| 6 |
|
| 7 |
## TL;DR β what got pushed
|
| 8 |
|
|
@@ -13,8 +11,7 @@ process is reproducible (next adapter, next quant, next mistake to recover from)
|
|
| 13 |
| `kylebrodeur/microfactory-node-v2` | `microfactory-node-v2.gguf` | q4_k_m | 5.3 GB | Standard E4B fine-tune |
|
| 14 |
| `kylebrodeur/microfactory-node` | `microfactory-node.gguf` | q4_k_m | 5.3 GB | First fine-tune (v1, historical) |
|
| 15 |
|
| 16 |
-
All four are public on `https://ollama.com/kylebrodeur` and pullable with one
|
| 17 |
-
command:
|
| 18 |
|
| 19 |
```bash
|
| 20 |
ollama run kylebrodeur/microfactory-node-v3-qat # recommended
|
|
@@ -23,8 +20,7 @@ ollama run kylebrodeur/microfactory-node-v2
|
|
| 23 |
ollama run kylebrodeur/microfactory-node
|
| 24 |
```
|
| 25 |
|
| 26 |
-
|
| 27 |
-
> see the HF-direct path at the bottom of this doc.
|
| 28 |
|
| 29 |
---
|
| 30 |
|
|
@@ -56,9 +52,7 @@ Local ollama store (`/usr/share/ollama/.ollama/models/`)
|
|
| 56 |
ollama.com/kylebrodeur/<short-name>
|
| 57 |
```
|
| 58 |
|
| 59 |
-
Every box
|
| 60 |
-
steps (described below) are: generating an Ollama SSH keypair and registering
|
| 61 |
-
it on your ollama.com profile.
|
| 62 |
|
| 63 |
---
|
| 64 |
|
|
@@ -66,34 +60,24 @@ it on your ollama.com profile.
|
|
| 66 |
|
| 67 |
### 1. Ollama account on `ollama.com`
|
| 68 |
|
| 69 |
-
Sign up at <https://ollama.com>. The username you pick becomes the namespace
|
| 70 |
-
prefix on every model you publish (`<username>/<model>`). I used `kylebrodeur`
|
| 71 |
-
to match the GitHub / Hugging Face username β keeping the three handles in sync
|
| 72 |
-
makes documentation and cross-linking sane.
|
| 73 |
|
| 74 |
### 2. Generate an Ollama SSH key
|
| 75 |
|
| 76 |
-
The system Ollama daemon (typically `/usr/share/ollama/.ollama/`) does NOT own
|
| 77 |
-
your push credentials. The `ollama push` client looks for an ED25519 keypair in
|
| 78 |
-
`~/.ollama/` for the **invoking user**. Generate it once:
|
| 79 |
|
| 80 |
```bash
|
| 81 |
ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N "" -C "<your-handle>-ollama" -q
|
| 82 |
cat ~/.ollama/id_ed25519.pub
|
| 83 |
```
|
| 84 |
|
| 85 |
-
|
| 86 |
-
> daemon's home dir (e.g. `/usr/share/ollama/.ollama/`). When the daemon runs
|
| 87 |
-
> as a different user (typical on Linux package installs), the client-side
|
| 88 |
-
> push uses YOUR home dir β generate it there explicitly.
|
| 89 |
|
| 90 |
### 3. Register the public key on ollama.com
|
| 91 |
|
| 92 |
-
Open <https://ollama.com/settings/keys>, click
|
| 93 |
-
the contents of `~/.ollama/id_ed25519.pub`. Save.
|
| 94 |
|
| 95 |
-
That is the entire auth surface β no API token, no `ollama login`. Pushes
|
| 96 |
-
authenticate via that ED25519 signature on every request.
|
| 97 |
|
| 98 |
---
|
| 99 |
|
|
@@ -103,10 +87,7 @@ Given an adapter at `kylebrodeur/microfactory-node-lora-vN` on HF Hub:
|
|
| 103 |
|
| 104 |
### A. Build & upload the GGUF (Modal)
|
| 105 |
|
| 106 |
-
`learn/finetune/gguf_pipeline_modal.py` has three Modal functions
|
| 107 |
-
(`merge` on GPU β `convert_to_gguf` on CPU β `upload_to_hub` on CPU) and two
|
| 108 |
-
entrypoints (`::main` for the full pipeline, `::upload_only` for re-uploading
|
| 109 |
-
something already on the volume).
|
| 110 |
|
| 111 |
```bash
|
| 112 |
PYFILE=/home/kylebrodeur/projects/microfactory-lab/chief-engineer/learn/finetune/gguf_pipeline_modal.py
|
|
@@ -134,9 +115,7 @@ modal run "${PYFILE}::upload_only" \
|
|
| 134 |
|
| 135 |
### B. Pull from HF into local Ollama
|
| 136 |
|
| 137 |
-
Ollama's native HF integration is the fastest pull path
|
| 138 |
-
versus ~30 MB/min via `hf download` Xet warm-up for the same file). Always
|
| 139 |
-
specify the GGUF filename as a tag so Ollama picks the right one:
|
| 140 |
|
| 141 |
```bash
|
| 142 |
ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
|
|
@@ -150,8 +129,7 @@ hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf 5.3 GB
|
|
| 150 |
|
| 151 |
### C. Rename for the ollama.com namespace
|
| 152 |
|
| 153 |
-
Ollama push requires a `<username>/<name>[:<tag>]` ID. The `hf.co/...` ID is
|
| 154 |
-
not pushable as-is. Use `ollama cp` to create the publishable alias:
|
| 155 |
|
| 156 |
```bash
|
| 157 |
# default tag (becomes `:latest`)
|
|
@@ -170,9 +148,7 @@ ollama push kylebrodeur/microfactory-node-v3-qat
|
|
| 170 |
ollama push kylebrodeur/microfactory-node-v3-qat:q4_0
|
| 171 |
```
|
| 172 |
|
| 173 |
-
Push speed is gated by Ollama's CDN ingest (~5 MB/s in my run, ~15 min per
|
| 174 |
-
5 GB model). It chunks + dedups across all models you push, so the second
|
| 175 |
-
push of a model that shares any blobs with the first is much faster.
|
| 176 |
|
| 177 |
### E. Verify
|
| 178 |
|
|
@@ -188,15 +164,13 @@ ollama run kylebrodeur/microfactory-node-v3-qat
|
|
| 188 |
>>> PLA overhang at 22C, 45% humidity
|
| 189 |
```
|
| 190 |
|
| 191 |
-
The model card on `ollama.com/kylebrodeur/<name>` populates automatically from
|
| 192 |
-
the `Modelfile` metadata once the manifest is uploaded.
|
| 193 |
|
| 194 |
---
|
| 195 |
|
| 196 |
## Doing it for all 3 (or 4) variants in one shot
|
| 197 |
|
| 198 |
-
`/tmp/all-pushes.sh` is the script
|
| 199 |
-
`/tmp/all-pushes.log`, processes v3-qat β v2 β v1 serially (pull β cp β push):
|
| 200 |
|
| 201 |
```bash
|
| 202 |
#!/usr/bin/env bash
|
|
@@ -235,8 +209,7 @@ disown
|
|
| 235 |
tail -f /tmp/all-pushes.log
|
| 236 |
```
|
| 237 |
|
| 238 |
-
`setsid` + `</dev/null` + `disown` is what kept it alive across my own shell
|
| 239 |
-
exits during the ~60 min total runtime.
|
| 240 |
|
| 241 |
---
|
| 242 |
|
|
@@ -244,19 +217,11 @@ exits during the ~60 min total runtime.
|
|
| 244 |
|
| 245 |
### G1 β Ollama keys live in YOUR home, not the daemon's
|
| 246 |
|
| 247 |
-
On Linux package installs the daemon usually runs as the `ollama` user with
|
| 248 |
-
`HOME=/usr/share/ollama`. `ollama push` (client) signs with whatever user is
|
| 249 |
-
invoking it β so `~/.ollama/id_ed25519` must exist for *you*. Generating it
|
| 250 |
-
with `ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N ""` is faster than
|
| 251 |
-
poking at the daemon to lazy-create it.
|
| 252 |
|
| 253 |
### G2 β `hf download` via Xet was 20β30Γ slower than `ollama pull hf.co/...`
|
| 254 |
|
| 255 |
-
I started with `hf download kylebrodeur/microfactory-node-gguf microfactory-node-v3-qat.gguf`
|
| 256 |
-
expecting Xet acceleration; got ~30 MB in 60 s (looked stuck). Killed it and
|
| 257 |
-
ran `ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf`
|
| 258 |
-
instead β 10β11 MB/s immediately, ETA ~8 min. The HFβOllama path uses HTTP
|
| 259 |
-
range requests against the LFS-backed file, skipping the Xet handshake.
|
| 260 |
|
| 261 |
### G3 β `modal run gguf_pipeline_modal.py` fails after a second `@app.local_entrypoint()`
|
| 262 |
|
|
@@ -270,8 +235,7 @@ Error: > modal run /.../gguf_pipeline_modal.py::my_function [..args]
|
|
| 270 |
...
|
| 271 |
```
|
| 272 |
|
| 273 |
-
Always pass `::main` or `::upload_only` explicitly once that second entrypoint
|
| 274 |
-
exists.
|
| 275 |
|
| 276 |
### G4 β HF tokens stored as Modal secrets can carry trailing whitespace
|
| 277 |
|
|
@@ -281,24 +245,15 @@ The first upload attempt failed with:
|
|
| 281 |
httpx.LocalProtocolError: Illegal header value b'Bearer hf_xxx '
|
| 282 |
```
|
| 283 |
|
| 284 |
-
β¦because the value pasted into Modal's secret editor had a leading space and a
|
| 285 |
-
trailing newline. The upload helper now does `token.strip()` before passing it
|
| 286 |
-
to `HfApi`. Worth doing in any code reading Modal secrets generally.
|
| 287 |
|
| 288 |
### G5 β `nohup &` + `&&`-chained `modal run` lose cwd
|
| 289 |
|
| 290 |
-
Several backgrounded `modal run` invocations failed because Modal resolved a
|
| 291 |
-
stale `cwd` and tried to load the pipeline file from the wrong path. Two
|
| 292 |
-
mitigations: always pass an **absolute** path to the `.py` file, and chain via
|
| 293 |
-
`setsid` rather than the shell's job control.
|
| 294 |
|
| 295 |
### G6 β Overlapping filenames between Modal volume and HF repo
|
| 296 |
|
| 297 |
-
The Modal pipeline derives both the volume path (`/out/gguf/<name>.gguf`) and
|
| 298 |
-
the HF path (`<repo>/<name>.gguf`) from the same `--name`. Generating the q4_0
|
| 299 |
-
variant of a model that already has a q4_k_m on HF would silently overwrite
|
| 300 |
-
unless you split the names. Added `--as-name <hf-filename>` to `upload_only` so
|
| 301 |
-
the HF target can differ from the volume source:
|
| 302 |
|
| 303 |
```bash
|
| 304 |
modal run ...::upload_only --name microfactory-node-v3-qat \
|
|
@@ -306,24 +261,17 @@ modal run ...::upload_only --name microfactory-node-v3-qat \
|
|
| 306 |
--repo kylebrodeur/microfactory-node-gguf
|
| 307 |
```
|
| 308 |
|
| 309 |
-
For full pipeline runs, append `-q4_0` to `--name` directly so both layers stay
|
| 310 |
-
in sync (the volume keeps a copy too).
|
| 311 |
|
| 312 |
### G7 β `ollama push` needs `<user>/<name>` exactly
|
| 313 |
|
| 314 |
-
You cannot push a model whose ID starts with `hf.co/...` or any other
|
| 315 |
-
non-username prefix. `ollama cp` is the only way to rename, and the new name
|
| 316 |
-
must match `<username>/<model>` where `<username>` equals your registered
|
| 317 |
-
ollama.com handle.
|
| 318 |
|
| 319 |
---
|
| 320 |
|
| 321 |
## HF-direct path (alternative to ollama.com)
|
| 322 |
|
| 323 |
-
For users who just want to run the model locally without pulling from
|
| 324 |
-
ollama.com, the same GGUFs live on HF Hub and Ollama supports `hf.co/...` URIs
|
| 325 |
-
natively. The HF repo also holds `template`, `system`, and `params` files so
|
| 326 |
-
the chat template, persona, and sampling apply automatically:
|
| 327 |
|
| 328 |
```bash
|
| 329 |
ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
|
|
@@ -332,17 +280,13 @@ ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v2.gguf
|
|
| 332 |
ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
|
| 333 |
```
|
| 334 |
|
| 335 |
-
The `ollama.com/kylebrodeur/...` tags exist for discoverability via the public
|
| 336 |
-
registry; the HF-direct URIs exist as the canonical, single-source-of-truth
|
| 337 |
-
distribution. Both paths point at the same blobs.
|
| 338 |
|
| 339 |
---
|
| 340 |
|
| 341 |
## Cleanup
|
| 342 |
|
| 343 |
-
The pulled `hf.co/...` tags can be removed once they've been cp'd into the
|
| 344 |
-
`kylebrodeur/...` namespace β both share the same underlying blob in the
|
| 345 |
-
content-addressed Ollama store, so deleting the alias is free:
|
| 346 |
|
| 347 |
```bash
|
| 348 |
ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
|
|
@@ -351,5 +295,4 @@ ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v2.gguf
|
|
| 351 |
ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
|
| 352 |
```
|
| 353 |
|
| 354 |
-
The `kylebrodeur/...` tags keep working because the manifest still references
|
| 355 |
-
the same blob digests.
|
|
|
|
| 1 |
# Publishing GGUFs to ollama.com β runbook & decision log
|
| 2 |
|
| 3 |
+
Date: 2026-06-14. This documents how the Chief Engineer GGUFs got listed on the public Ollama registry at `ollama.com/kylebrodeur/microfactory-node-*`. I wrote it so the next adapter, quant, or mistake is reproducible.
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## TL;DR β what got pushed
|
| 6 |
|
|
|
|
| 11 |
| `kylebrodeur/microfactory-node-v2` | `microfactory-node-v2.gguf` | q4_k_m | 5.3 GB | Standard E4B fine-tune |
|
| 12 |
| `kylebrodeur/microfactory-node` | `microfactory-node.gguf` | q4_k_m | 5.3 GB | First fine-tune (v1, historical) |
|
| 13 |
|
| 14 |
+
All four are public on `https://ollama.com/kylebrodeur` and pullable with one command:
|
|
|
|
| 15 |
|
| 16 |
```bash
|
| 17 |
ollama run kylebrodeur/microfactory-node-v3-qat # recommended
|
|
|
|
| 20 |
ollama run kylebrodeur/microfactory-node
|
| 21 |
```
|
| 22 |
|
| 23 |
+
The same files are also pullable from HF directly (no ollama.com round-trip). See the HF-direct path at the bottom of this doc.
|
|
|
|
| 24 |
|
| 25 |
---
|
| 26 |
|
|
|
|
| 52 |
ollama.com/kylebrodeur/<short-name>
|
| 53 |
```
|
| 54 |
|
| 55 |
+
Every box left of `ollama.com` is automated. The two manual one-time steps are generating an Ollama SSH keypair and registering it on your ollama.com profile.
|
|
|
|
|
|
|
| 56 |
|
| 57 |
---
|
| 58 |
|
|
|
|
| 60 |
|
| 61 |
### 1. Ollama account on `ollama.com`
|
| 62 |
|
| 63 |
+
Sign up at <https://ollama.com>. The username you pick becomes the namespace prefix on every model you publish (`<username>/<model>`). I used `kylebrodeur` to match my GitHub and Hugging Face handles. Keeping the three in sync makes documentation and cross-linking sane.
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
### 2. Generate an Ollama SSH key
|
| 66 |
|
| 67 |
+
The system Ollama daemon (typically `/usr/share/ollama/.ollama/`) does NOT own your push credentials. The `ollama push` client looks for an ED25519 keypair in `~/.ollama/` for the invoking user. Generate it once:
|
|
|
|
|
|
|
| 68 |
|
| 69 |
```bash
|
| 70 |
ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N "" -C "<your-handle>-ollama" -q
|
| 71 |
cat ~/.ollama/id_ed25519.pub
|
| 72 |
```
|
| 73 |
|
| 74 |
+
Ollama itself will lazily create this on first push, but only inside the daemon's home dir (e.g. `/usr/share/ollama/.ollama/`). When the daemon runs as a different user (typical on Linux package installs), the client-side push uses YOUR home dir. Generate it there explicitly.
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
### 3. Register the public key on ollama.com
|
| 77 |
|
| 78 |
+
Open <https://ollama.com/settings/keys>, click Add Ollama Public Key, paste the contents of `~/.ollama/id_ed25519.pub`. Save.
|
|
|
|
| 79 |
|
| 80 |
+
That is the entire auth surface β no API token, no `ollama login`. Pushes authenticate via that ED25519 signature on every request.
|
|
|
|
| 81 |
|
| 82 |
---
|
| 83 |
|
|
|
|
| 87 |
|
| 88 |
### A. Build & upload the GGUF (Modal)
|
| 89 |
|
| 90 |
+
`learn/finetune/gguf_pipeline_modal.py` has three Modal functions (`merge` on GPU β `convert_to_gguf` on CPU β `upload_to_hub` on CPU) and two entrypoints (`::main` for the full pipeline, `::upload_only` for re-uploading something already on the volume).
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
```bash
|
| 93 |
PYFILE=/home/kylebrodeur/projects/microfactory-lab/chief-engineer/learn/finetune/gguf_pipeline_modal.py
|
|
|
|
| 115 |
|
| 116 |
### B. Pull from HF into local Ollama
|
| 117 |
|
| 118 |
+
Ollama's native HF integration is the fastest pull path. I saw 10 MB/s+ versus ~30 MB/min via `hf download` Xet warm-up for the same file. Always specify the GGUF filename as a tag so Ollama picks the right one:
|
|
|
|
|
|
|
| 119 |
|
| 120 |
```bash
|
| 121 |
ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
|
|
|
|
| 129 |
|
| 130 |
### C. Rename for the ollama.com namespace
|
| 131 |
|
| 132 |
+
Ollama push requires a `<username>/<name>[:<tag>]` ID. The `hf.co/...` ID is not pushable as-is. Use `ollama cp` to create the publishable alias:
|
|
|
|
| 133 |
|
| 134 |
```bash
|
| 135 |
# default tag (becomes `:latest`)
|
|
|
|
| 148 |
ollama push kylebrodeur/microfactory-node-v3-qat:q4_0
|
| 149 |
```
|
| 150 |
|
| 151 |
+
Push speed is gated by Ollama's CDN ingest (~5 MB/s in my run, ~15 min per 5 GB model). It chunks + dedups across all models you push, so the second push of a model that shares any blobs with the first is much faster.
|
|
|
|
|
|
|
| 152 |
|
| 153 |
### E. Verify
|
| 154 |
|
|
|
|
| 164 |
>>> PLA overhang at 22C, 45% humidity
|
| 165 |
```
|
| 166 |
|
| 167 |
+
The model card on `ollama.com/kylebrodeur/<name>` populates automatically from the `Modelfile` metadata once the manifest is uploaded.
|
|
|
|
| 168 |
|
| 169 |
---
|
| 170 |
|
| 171 |
## Doing it for all 3 (or 4) variants in one shot
|
| 172 |
|
| 173 |
+
`/tmp/all-pushes.sh` is the script I used. It runs detached, logs to `/tmp/all-pushes.log`, and processes v3-qat β v2 β v1 serially (pull β cp β push):
|
|
|
|
| 174 |
|
| 175 |
```bash
|
| 176 |
#!/usr/bin/env bash
|
|
|
|
| 209 |
tail -f /tmp/all-pushes.log
|
| 210 |
```
|
| 211 |
|
| 212 |
+
`setsid` + `</dev/null` + `disown` is what kept it alive across my own shell exits during the ~60 min total runtime.
|
|
|
|
| 213 |
|
| 214 |
---
|
| 215 |
|
|
|
|
| 217 |
|
| 218 |
### G1 β Ollama keys live in YOUR home, not the daemon's
|
| 219 |
|
| 220 |
+
On Linux package installs the daemon usually runs as the `ollama` user with `HOME=/usr/share/ollama`. `ollama push` (client) signs with whatever user is invoking it β so `~/.ollama/id_ed25519` must exist for you. Generating it with `ssh-keygen -t ed25519 -f ~/.ollama/id_ed25519 -N ""` is faster than poking at the daemon to lazy-create it.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
### G2 β `hf download` via Xet was 20β30Γ slower than `ollama pull hf.co/...`
|
| 223 |
|
| 224 |
+
I started with `hf download kylebrodeur/microfactory-node-gguf microfactory-node-v3-qat.gguf` expecting Xet acceleration; got ~30 MB in 60 s (looked stuck). Killed it and ran `ollama pull hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf` instead β 10β11 MB/s immediately, ETA ~8 min. The HFβOllama path uses HTTP range requests against the LFS-backed file, skipping the Xet handshake.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
### G3 β `modal run gguf_pipeline_modal.py` fails after a second `@app.local_entrypoint()`
|
| 227 |
|
|
|
|
| 235 |
...
|
| 236 |
```
|
| 237 |
|
| 238 |
+
Always pass `::main` or `::upload_only` explicitly once that second entrypoint exists.
|
|
|
|
| 239 |
|
| 240 |
### G4 β HF tokens stored as Modal secrets can carry trailing whitespace
|
| 241 |
|
|
|
|
| 245 |
httpx.LocalProtocolError: Illegal header value b'Bearer hf_xxx '
|
| 246 |
```
|
| 247 |
|
| 248 |
+
β¦because the value pasted into Modal's secret editor had a leading space and a trailing newline. The upload helper now does `token.strip()` before passing it to `HfApi`. Worth doing in any code reading Modal secrets generally.
|
|
|
|
|
|
|
| 249 |
|
| 250 |
### G5 β `nohup &` + `&&`-chained `modal run` lose cwd
|
| 251 |
|
| 252 |
+
Several backgrounded `modal run` invocations failed because Modal resolved a stale `cwd` and tried to load the pipeline file from the wrong path. Two mitigations: always pass an absolute path to the `.py` file, and chain via `setsid` rather than the shell's job control.
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
### G6 β Overlapping filenames between Modal volume and HF repo
|
| 255 |
|
| 256 |
+
The Modal pipeline derives both the volume path (`/out/gguf/<name>.gguf`) and the HF path (`<repo>/<name>.gguf`) from the same `--name`. Generating the q4_0 variant of a model that already has a q4_k_m on HF would silently overwrite unless you split the names. I added `--as-name <hf-filename>` to `upload_only` so the HF target can differ from the volume source:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
```bash
|
| 259 |
modal run ...::upload_only --name microfactory-node-v3-qat \
|
|
|
|
| 261 |
--repo kylebrodeur/microfactory-node-gguf
|
| 262 |
```
|
| 263 |
|
| 264 |
+
For full pipeline runs, append `-q4_0` to `--name` directly so both layers stay in sync. The volume keeps a copy too.
|
|
|
|
| 265 |
|
| 266 |
### G7 β `ollama push` needs `<user>/<name>` exactly
|
| 267 |
|
| 268 |
+
You cannot push a model whose ID starts with `hf.co/...` or any other non-username prefix. `ollama cp` is the only way to rename, and the new name must match `<username>/<model>` where `<username>` equals your registered ollama.com handle.
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
---
|
| 271 |
|
| 272 |
## HF-direct path (alternative to ollama.com)
|
| 273 |
|
| 274 |
+
For users who just want to run the model locally without pulling from ollama.com, the same GGUFs live on HF Hub and Ollama supports `hf.co/...` URIs natively. The HF repo also holds `template`, `system`, and `params` files so the chat template, persona, and sampling apply automatically:
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
```bash
|
| 277 |
ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
|
|
|
|
| 280 |
ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
|
| 281 |
```
|
| 282 |
|
| 283 |
+
The `ollama.com/kylebrodeur/...` tags exist for discoverability via the public registry. The HF-direct URIs are the canonical, single-source-of-truth distribution. Both paths point at the same blobs.
|
|
|
|
|
|
|
| 284 |
|
| 285 |
---
|
| 286 |
|
| 287 |
## Cleanup
|
| 288 |
|
| 289 |
+
The pulled `hf.co/...` tags can be removed once they've been cp'd into the `kylebrodeur/...` namespace. Both share the same underlying blob in the content-addressed Ollama store, so deleting the alias is free:
|
|
|
|
|
|
|
| 290 |
|
| 291 |
```bash
|
| 292 |
ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf
|
|
|
|
| 295 |
ollama rm hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node.gguf
|
| 296 |
```
|
| 297 |
|
| 298 |
+
The `kylebrodeur/...` tags keep working because the manifest still references the same blob digests.
|
|
|
learn/finetune/README.md
CHANGED
|
@@ -1,26 +1,15 @@
|
|
| 1 |
# Fine-tune track: the "Well-Tuned" frontier, realized (parallel, optional)
|
| 2 |
|
| 3 |
-
|
| 4 |
-
memory, by design). This track distills that same judgment into a **LoRA on a small Gemma** so it
|
| 5 |
-
can live in the weights too. It runs **in parallel** (train while you record) and **never touches
|
| 6 |
-
the live Space**: the demo stays the retrieval system. We only claim the **Well-Tuned** badge if a
|
| 7 |
-
held-out eval earns it.
|
| 8 |
|
| 9 |
## Files
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
committed. Offline targets are the deterministic advisor; start `ollama serve` first to distill
|
| 13 |
-
the live Gemma instead (higher fidelity, same format).
|
| 14 |
- `train_modal.py`: Modal LoRA SFT (A10G, ~1 hr cap), pushes the adapter to the Hub.
|
| 15 |
-
- `eval.py`: honest base-vs-LoRA scoring on the held-out rooms
|
| 16 |
-
- `gguf_pipeline_modal.py`: end-to-end LoRA β merged HF β quantized GGUF β
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
- `SERVING.md`: production hosting playbook β ZeroGPU, Modal-hosted OpenAI endpoint, model
|
| 20 |
-
switching in the Gradio app, current quant/HF/ollama table.
|
| 21 |
-
- `OLLAMA_PUBLISHING.md`: end-to-end walkthrough for publishing GGUFs to **ollama.com**
|
| 22 |
-
(one-time SSH-key setup, the `ollama pull β ollama cp β ollama push` flow, the seven
|
| 23 |
-
gotchas hit and the fixes β read this before publishing the next adapter).
|
| 24 |
|
| 25 |
## Published artifacts
|
| 26 |
|
|
@@ -30,15 +19,14 @@ held-out eval earns it.
|
|
| 30 |
- Modal inference API β `https://kylebrodeur--microfactory-node-inference-serve.modal.run`
|
| 31 |
|
| 32 |
## Honesty guardrails (non-negotiable)
|
|
|
|
| 33 |
- The live Space stays retrieval-based. The LoRA is the named frontier, realized, not a swap.
|
| 34 |
-
- Targets are the node's structured output
|
| 35 |
-
-
|
| 36 |
-
sampled advice reads as real judgment, not a memorized template. If it only parrots, report that
|
| 37 |
-
and stay "Not Well-Tuned." A thin badge is worse than an honest no.
|
| 38 |
|
| 39 |
## Budget
|
| 40 |
-
|
| 41 |
-
30 to 60 min run, well under $2.
|
| 42 |
|
| 43 |
---
|
| 44 |
|
|
|
|
| 1 |
# Fine-tune track: the "Well-Tuned" frontier, realized (parallel, optional)
|
| 2 |
|
| 3 |
+
I run the live node on retrieval plus a learned policy plus the deterministic Spine. That keeps the memory visible. This track bakes the same judgment into a small Gemma LoRA so it lives in the weights too. I train it in parallel while I record. It never touches the live Space. I only claim the Well-Tuned badge if a held-out eval earns it.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Files
|
| 6 |
+
|
| 7 |
+
- `prep_dataset.py`: builds `data/finetune/sft.{train,eval}.jsonl` (400 train / 80 held-out) by distilling the node's own Advice over a grid of material, geometry, and room. I already generated and committed this. Offline targets come from the deterministic advisor. Start `ollama serve` first to distill the live Gemma instead; the format stays the same.
|
|
|
|
|
|
|
| 8 |
- `train_modal.py`: Modal LoRA SFT (A10G, ~1 hr cap), pushes the adapter to the Hub.
|
| 9 |
+
- `eval.py`: honest base-vs-LoRA scoring on the held-out rooms. Checks JSON validity and Spine safety.
|
| 10 |
+
- `gguf_pipeline_modal.py`: end-to-end LoRA β merged HF β quantized GGUF β optional HF Hub upload. Two entrypoints: `::main` (full pipeline) and `::upload_only` (re-upload an existing volume file; use `--as-name` to rewrite the HF filename, e.g. q4_0 suffix).
|
| 11 |
+
- `SERVING.md`: production hosting playbook β ZeroGPU, Modal-hosted OpenAI endpoint, model switching in the Gradio app, current quant/HF/Ollama table.
|
| 12 |
+
- `OLLAMA_PUBLISHING.md`: end-to-end walkthrough for publishing GGUFs to ollama.com. Read this before publishing the next adapter.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
## Published artifacts
|
| 15 |
|
|
|
|
| 19 |
- Modal inference API β `https://kylebrodeur--microfactory-node-inference-serve.modal.run`
|
| 20 |
|
| 21 |
## Honesty guardrails (non-negotiable)
|
| 22 |
+
|
| 23 |
- The live Space stays retrieval-based. The LoRA is the named frontier, realized, not a swap.
|
| 24 |
+
- Targets are the node's structured output. I distill them faithfully. I do not invent data.
|
| 25 |
+
- I only claim Well-Tuned if `eval.py` shows TUNED >= BASE on json-valid AND spine-safe, and the sampled advice reads as real judgment, not a memorized template. If it only parrots, I report that and stay "Not Well-Tuned." A thin badge is worse than an honest no.
|
|
|
|
|
|
|
| 26 |
|
| 27 |
## Budget
|
| 28 |
+
|
| 29 |
+
I have ~$100 Modal available plus run credits. A LoRA on ~400 short examples on one A10G is roughly a 30 to 60 min run, well under $2. There is no reason to exceed that. The dataset is small on purpose.
|
| 30 |
|
| 31 |
---
|
| 32 |
|
learn/finetune/SERVING.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
-
# Serving
|
| 2 |
|
| 3 |
-
|
| 4 |
-
to Ollama, hosting inference on Modal, and adding on-demand model switching
|
| 5 |
-
to the Gradio app.
|
| 6 |
|
| 7 |
## Model Registry
|
| 8 |
|
|
@@ -17,9 +15,9 @@ to the Gradio app.
|
|
| 17 |
|
| 18 |
## 1. Ollama Publishing β Implemented
|
| 19 |
|
| 20 |
-
### Status:
|
| 21 |
|
| 22 |
-
|
| 23 |
|
| 24 |
| `ollama run β¦` | Quant | Size |
|
| 25 |
|----------------|-------|------|
|
|
@@ -28,7 +26,7 @@ to the Gradio app.
|
|
| 28 |
| [`kylebrodeur/microfactory-node-v2`](https://ollama.com/kylebrodeur/microfactory-node-v2) | q4_k_m | 5.3 GB |
|
| 29 |
| [`kylebrodeur/microfactory-node`](https://ollama.com/kylebrodeur/microfactory-node) | q4_k_m | 5.3 GB |
|
| 30 |
|
| 31 |
-
|
| 32 |
|
| 33 |
| File | Source adapter | Variant |
|
| 34 |
|------|----------------|---------|
|
|
@@ -37,35 +35,26 @@ to the Gradio app.
|
|
| 37 |
| [`microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf) (5.1 GB, q4_k_m) | [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) | Standard E4B fine-tune |
|
| 38 |
| [`microfactory-node.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node.gguf) (5.1 GB, q4_k_m) | `kylebrodeur/microfactory-node-lora` (v1) | First fine-tune (historical) |
|
| 39 |
|
| 40 |
-
The HF Hub repo also carries Ollama config files (`template`, `system`, `params`) so the
|
| 41 |
-
`hf.co/...` URI form of `ollama run` applies the right chat template, persona, and sampling
|
| 42 |
-
automatically (see [Ollama HF docs](https://huggingface.co/docs/hub/en/ollama)).
|
| 43 |
-
|
| 44 |
Users can pull either way:
|
| 45 |
```bash
|
| 46 |
ollama run kylebrodeur/microfactory-node-v3-qat # via ollama.com
|
| 47 |
ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf # via HF Hub
|
| 48 |
```
|
| 49 |
|
| 50 |
-
|
| 51 |
- [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) (35MB, Standard E4B)
|
| 52 |
- [`kylebrodeur/microfactory-node-lora-v3-qat`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v3-qat) (35MB, QAT-unquantized)
|
| 53 |
|
| 54 |
-
For the full ollama.com publishing walkthrough
|
| 55 |
-
`ollama cp` β `ollama push` flow, the gotchas hit and fixed), see
|
| 56 |
-
[`OLLAMA_PUBLISHING.md`](OLLAMA_PUBLISHING.md).
|
| 57 |
|
| 58 |
### Implemented: `gguf_pipeline_modal.py`
|
| 59 |
|
| 60 |
-
|
| 61 |
1. **GPU step** (`merge`): loads base model + LoRA adapter, merges via `merge_and_unload()`
|
| 62 |
-
2. **CPU step** (`convert_to_gguf`): clones llama.cpp, builds with cmake, runs
|
| 63 |
-
|
| 64 |
-
3. **CPU step** (`upload_to_hub`, optional): pushes the GGUF to a HF Hub model repo using
|
| 65 |
-
`chief-engineer-secrets` + `HF_TOKEN` Modal secrets
|
| 66 |
|
| 67 |
-
The `--upload <owner>/<repo>` flag triggers step 3 inline. Without it, the GGUF stays on the
|
| 68 |
-
Modal volume only.
|
| 69 |
|
| 70 |
**v3-qat with auto-upload (single command):**
|
| 71 |
```bash
|
|
@@ -75,8 +64,7 @@ modal run learn/finetune/gguf_pipeline_modal.py::main \
|
|
| 75 |
--upload kylebrodeur/microfactory-node-gguf
|
| 76 |
```
|
| 77 |
|
| 78 |
-
**Upload an existing GGUF already on the volume** (used to push v1/v2 which were converted
|
| 79 |
-
before the upload step existed):
|
| 80 |
```bash
|
| 81 |
modal run learn/finetune/gguf_pipeline_modal.py::upload_only \
|
| 82 |
--name microfactory-node-v2 \
|
|
@@ -98,13 +86,9 @@ modal run learn/finetune/gguf_pipeline_modal.py::main \
|
|
| 98 |
```
|
| 99 |
|
| 100 |
**Gotchas (encountered during the v1/v2/v3 push):**
|
| 101 |
-
- `modal run gguf_pipeline_modal.py` (without `::main`) fails once you add a second
|
| 102 |
-
|
| 103 |
-
-
|
| 104 |
-
the token (`token = token.strip()`); otherwise `httpx` raises
|
| 105 |
-
`LocalProtocolError: Illegal header value b'Bearer hf_xxx '`.
|
| 106 |
-
- Use absolute paths to the pipeline file when launching with `nohup &` β backgrounded
|
| 107 |
-
shells lose cwd faster than you'd expect.
|
| 108 |
|
| 109 |
**After download β Ollama import:**
|
| 110 |
```bash
|
|
@@ -125,24 +109,23 @@ ollama push kylebrodeur/microfactory-node-v2
|
|
| 125 |
```
|
| 126 |
|
| 127 |
### Decision: MergeβGGUF over adapter paths
|
| 128 |
-
|
| 129 |
-
Path B (Ollama ADAPTER command) because:
|
| 130 |
-
-
|
| 131 |
-
- Ollama ADAPTER command only documented for Gemma 1/2
|
| 132 |
-
- `convert_lora_to_gguf.py` compatibility with Gemma 4
|
| 133 |
-
- MergeβGGUF is the most battle-tested path
|
| 134 |
|
| 135 |
### Quantization
|
| 136 |
|
| 137 |
The GGUF pipeline uses `--outtype q4_k_m` by default:
|
| 138 |
-
- **Does NOT reduce parameter count**: 8B params stay 8B. Quantization reduces
|
| 139 |
-
weight precision from 16-bit (bf16) to 4-bit per weight.
|
| 140 |
- **File size**: ~5-6GB for 8B model (vs ~16GB for bf16 safetensors)
|
| 141 |
- **Quality**: q4_k_m is the recommended balance of size vs quality
|
| 142 |
-
- **v3 advantage**: QAT-trained model retains more quality after quantization
|
| 143 |
-
because it was trained with simulated quantization during fine-tuning
|
| 144 |
|
| 145 |
Other quantization options (pass `--outtype` to override):
|
|
|
|
| 146 |
| Format | Size (8B) | Quality | Use Case |
|
| 147 |
|--------|-----------|---------|----------|
|
| 148 |
| q8_0 | ~8.5GB | Near-lossless | Max quality, local GPU |
|
|
@@ -151,8 +134,8 @@ Other quantization options (pass `--outtype` to override):
|
|
| 151 |
| q4_0 | ~4.5GB | Lower | QAT-optimized (Google's QAT Q4_0 target) |
|
| 152 |
|
| 153 |
### Parallel GGUF runs
|
| 154 |
-
|
| 155 |
-
(merge on GPU, convert on CPU) and writes distinct output filenames via `--name`:
|
| 156 |
|
| 157 |
```bash
|
| 158 |
# Terminal 1 β v2:
|
|
@@ -173,14 +156,11 @@ Download both: `modal volume get microfactory-node-finetune gguf/ --force`
|
|
| 173 |
|
| 174 |
## 2. Modal Model Hosting β Implemented
|
| 175 |
|
| 176 |
-
### Status:
|
| 177 |
|
| 178 |
### Implemented: `modal_serve.py`
|
| 179 |
|
| 180 |
-
OpenAI-compatible `/v1/chat/completions` endpoint on Modal GPU.
|
| 181 |
-
Loads base model + LoRA adapter once at container start, keeps warm.
|
| 182 |
-
Auto-scales to zero after 5 min idle (`scaledown_window=300`).
|
| 183 |
-
Handles up to 10 concurrent requests (`@modal.concurrent(max_inputs=10)`).
|
| 184 |
|
| 185 |
Deploy:
|
| 186 |
```bash
|
|
@@ -200,12 +180,14 @@ FINETUNE_ADAPTER=kylebrodeur/microfactory-node-lora-v3-qat modal deploy learn/fi
|
|
| 200 |
```
|
| 201 |
|
| 202 |
### Modal API Deprecation Fixes Applied
|
| 203 |
-
|
|
|
|
| 204 |
1. `container_idle_timeout` β `scaledown_window` (deprecated 2025-02-24)
|
| 205 |
2. `allow_concurrent_inputs` β `@modal.concurrent(max_inputs=10)` decorator (deprecated 2025-04-09)
|
| 206 |
|
| 207 |
### Budget: Separate $100 serving budget
|
| 208 |
-
|
|
|
|
| 209 |
- A10G active: ~$5.04/hr
|
| 210 |
- With scale-to-zero: ~$0.50-2.00/day typical
|
| 211 |
- Health check endpoint at `/health` for monitoring
|
|
@@ -214,17 +196,15 @@ Distinct from the ~$11.54 training budget already spent. Serving costs:
|
|
| 214 |
|
| 215 |
## 3. Gradio Model Switching β Backend Implemented, UI Deferred
|
| 216 |
|
| 217 |
-
### Status:
|
| 218 |
|
| 219 |
### Implemented: `core/llm_zerogpu_lora.py`
|
| 220 |
|
| 221 |
-
LoRA-aware ZeroGPU backend.
|
| 222 |
-
but wraps the base model with `PeftModel.from_pretrained()` when `CHIEF_ENGINEER_LORA_REPO` is set.
|
| 223 |
-
Import-guarded β safe no-op if torch/transformers absent.
|
| 224 |
|
| 225 |
### Implemented: `app.py` backend infrastructure
|
| 226 |
|
| 227 |
-
|
| 228 |
- `MODEL_OPTIONS` list: "Base (Gemma 4 E4B)", "LoRA v2 (Standard E4B)", "LoRA v3 (QAT E4B)", "Modal API (remote)"
|
| 229 |
- `MODEL_LORA_MAP` dict: maps UI labels β HF Hub adapter repo IDs
|
| 230 |
- `_apply_model_choice()` function: sets `CHIEF_ENGINEER_LORA_REPO` and `CHIEF_ENGINEER_BACKEND` env vars, reloads `core.llm` module
|
|
@@ -232,22 +212,21 @@ Added to `app.py` (merged with UI agent's concurrent changes):
|
|
| 232 |
- `core.llm_zerogpu_lora` imported at Space startup alongside `core.llm_zerogpu`
|
| 233 |
|
| 234 |
### UI placement rolled back
|
| 235 |
-
Per user request (another agent is handling the Gradio UI), the dropdown widget
|
| 236 |
-
placement and HTML note were removed from `app.py`. The backend infrastructure
|
| 237 |
-
remains so the UI agent can wire it in.
|
| 238 |
|
| 239 |
-
|
|
|
|
|
|
|
| 240 |
|
| 241 |
**Already done (do NOT re-implement):**
|
| 242 |
-
-
|
| 243 |
-
-
|
| 244 |
-
-
|
| 245 |
-
-
|
| 246 |
|
| 247 |
**What the UI agent needs to do:**
|
| 248 |
1. Add a `gr.Dropdown` with `MODEL_OPTIONS` choices in the STUDIO tab
|
| 249 |
2. Wire `model_choice` into the `build_job` call in the event handler
|
| 250 |
-
3. Add info line: "
|
| 251 |
4. `_apply_model_choice()` handles all backend switching automatically
|
| 252 |
|
| 253 |
---
|
|
@@ -269,7 +248,7 @@ remains so the UI agent can wire it in.
|
|
| 269 |
## 5. Files Created/Modified
|
| 270 |
|
| 271 |
| File | Action | Purpose |
|
| 272 |
-
|------|--------|--------|
|
| 273 |
| `core/llm_zerogpu.py` | βοΈ Modified | E2BβE4B fix |
|
| 274 |
| `core/llm_zerogpu_lora.py` | β¨ Created | LoRA-aware ZeroGPU backend |
|
| 275 |
| `app.py` | βοΈ Modified | Add model selector dropdown + wiring |
|
|
|
|
| 1 |
+
# Serving and deployment: Ollama, Modal, and Gradio model switching
|
| 2 |
|
| 3 |
+
This is what I figured out for publishing fine-tuned LoRA adapters to Ollama, hosting inference on Modal, and adding on-demand model switching to the Gradio app.
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## Model Registry
|
| 6 |
|
|
|
|
| 15 |
|
| 16 |
## 1. Ollama Publishing β Implemented
|
| 17 |
|
| 18 |
+
### Status: GGUFs live on both HF Hub and ollama.com
|
| 19 |
|
| 20 |
+
The public Ollama registry is at [`ollama.com/kylebrodeur`](https://ollama.com/kylebrodeur):
|
| 21 |
|
| 22 |
| `ollama run β¦` | Quant | Size |
|
| 23 |
|----------------|-------|------|
|
|
|
|
| 26 |
| [`kylebrodeur/microfactory-node-v2`](https://ollama.com/kylebrodeur/microfactory-node-v2) | q4_k_m | 5.3 GB |
|
| 27 |
| [`kylebrodeur/microfactory-node`](https://ollama.com/kylebrodeur/microfactory-node) | q4_k_m | 5.3 GB |
|
| 28 |
|
| 29 |
+
The HF Hub repo is [`kylebrodeur/microfactory-node-gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf):
|
| 30 |
|
| 31 |
| File | Source adapter | Variant |
|
| 32 |
|------|----------------|---------|
|
|
|
|
| 35 |
| [`microfactory-node-v2.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node-v2.gguf) (5.1 GB, q4_k_m) | [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) | Standard E4B fine-tune |
|
| 36 |
| [`microfactory-node.gguf`](https://huggingface.co/kylebrodeur/microfactory-node-gguf/blob/main/microfactory-node.gguf) (5.1 GB, q4_k_m) | `kylebrodeur/microfactory-node-lora` (v1) | First fine-tune (historical) |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
Users can pull either way:
|
| 39 |
```bash
|
| 40 |
ollama run kylebrodeur/microfactory-node-v3-qat # via ollama.com
|
| 41 |
ollama run hf.co/kylebrodeur/microfactory-node-gguf:microfactory-node-v3-qat.gguf # via HF Hub
|
| 42 |
```
|
| 43 |
|
| 44 |
+
I keep the LoRA adapter repos on the Hub for further fine-tuning or inspection:
|
| 45 |
- [`kylebrodeur/microfactory-node-lora-v2`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v2) (35MB, Standard E4B)
|
| 46 |
- [`kylebrodeur/microfactory-node-lora-v3-qat`](https://huggingface.co/kylebrodeur/microfactory-node-lora-v3-qat) (35MB, QAT-unquantized)
|
| 47 |
|
| 48 |
+
For the full ollama.com publishing walkthrough, see [`OLLAMA_PUBLISHING.md`](OLLAMA_PUBLISHING.md).
|
|
|
|
|
|
|
| 49 |
|
| 50 |
### Implemented: `gguf_pipeline_modal.py`
|
| 51 |
|
| 52 |
+
You do not need a local llama.cpp build. The full mergeβGGUFβupload pipeline runs entirely on Modal:
|
| 53 |
1. **GPU step** (`merge`): loads base model + LoRA adapter, merges via `merge_and_unload()`
|
| 54 |
+
2. **CPU step** (`convert_to_gguf`): clones llama.cpp, builds with cmake, runs `convert_hf_to_gguf.py` β bf16 GGUF β `llama-quantize` to target type (q4_k_m default)
|
| 55 |
+
3. **CPU step** (`upload_to_hub`, optional): pushes the GGUF to a HF Hub model repo using `chief-engineer-secrets` + `HF_TOKEN` Modal secrets
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
The `--upload <owner>/<repo>` flag triggers step 3 inline. Without it, the GGUF stays on the Modal volume only.
|
|
|
|
| 58 |
|
| 59 |
**v3-qat with auto-upload (single command):**
|
| 60 |
```bash
|
|
|
|
| 64 |
--upload kylebrodeur/microfactory-node-gguf
|
| 65 |
```
|
| 66 |
|
| 67 |
+
**Upload an existing GGUF already on the volume** (used to push v1/v2 which were converted before the upload step existed):
|
|
|
|
| 68 |
```bash
|
| 69 |
modal run learn/finetune/gguf_pipeline_modal.py::upload_only \
|
| 70 |
--name microfactory-node-v2 \
|
|
|
|
| 86 |
```
|
| 87 |
|
| 88 |
**Gotchas (encountered during the v1/v2/v3 push):**
|
| 89 |
+
- `modal run gguf_pipeline_modal.py` (without `::main`) fails once you add a second `@app.local_entrypoint()` β always pass `::main` or `::upload_only`.
|
| 90 |
+
- Modal secret values can include trailing whitespace/newlines β the upload helper now strips the token (`token = token.strip()`); otherwise `httpx` raises `LocalProtocolError: Illegal header value b'Bearer hf_xxx '`.
|
| 91 |
+
- Use absolute paths to the pipeline file when launching with `nohup &` β backgrounded shells lose cwd faster than you'd expect.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
**After download β Ollama import:**
|
| 94 |
```bash
|
|
|
|
| 109 |
```
|
| 110 |
|
| 111 |
### Decision: MergeβGGUF over adapter paths
|
| 112 |
+
|
| 113 |
+
I chose the merge path (single GGUF file) over Path A (LoRAβGGUF adapter) and Path B (Ollama ADAPTER command) because:
|
| 114 |
+
- A single GGUF file means no runtime adapter complexity.
|
| 115 |
+
- Ollama ADAPTER command is only documented for Gemma 1/2. I have not verified it for Gemma 4.
|
| 116 |
+
- I have not tested `convert_lora_to_gguf.py` compatibility with Gemma 4.
|
| 117 |
+
- MergeβGGUF is the most battle-tested path.
|
| 118 |
|
| 119 |
### Quantization
|
| 120 |
|
| 121 |
The GGUF pipeline uses `--outtype q4_k_m` by default:
|
| 122 |
+
- **Does NOT reduce parameter count**: 8B params stay 8B. Quantization reduces weight precision from 16-bit (bf16) to 4-bit per weight.
|
|
|
|
| 123 |
- **File size**: ~5-6GB for 8B model (vs ~16GB for bf16 safetensors)
|
| 124 |
- **Quality**: q4_k_m is the recommended balance of size vs quality
|
| 125 |
+
- **v3 advantage**: QAT-trained model retains more quality after quantization because it was trained with simulated quantization during fine-tuning
|
|
|
|
| 126 |
|
| 127 |
Other quantization options (pass `--outtype` to override):
|
| 128 |
+
|
| 129 |
| Format | Size (8B) | Quality | Use Case |
|
| 130 |
|--------|-----------|---------|----------|
|
| 131 |
| q8_0 | ~8.5GB | Near-lossless | Max quality, local GPU |
|
|
|
|
| 134 |
| q4_0 | ~4.5GB | Lower | QAT-optimized (Google's QAT Q4_0 target) |
|
| 135 |
|
| 136 |
### Parallel GGUF runs
|
| 137 |
+
|
| 138 |
+
Both tracks can run at the same time. The pipeline uses separate Modal functions (merge on GPU, convert on CPU) and writes distinct output filenames via `--name`:
|
| 139 |
|
| 140 |
```bash
|
| 141 |
# Terminal 1 β v2:
|
|
|
|
| 156 |
|
| 157 |
## 2. Modal Model Hosting β Implemented
|
| 158 |
|
| 159 |
+
### Status: Deploying (`ap-60wirJOd35PZl1ZIKakD9v`)
|
| 160 |
|
| 161 |
### Implemented: `modal_serve.py`
|
| 162 |
|
| 163 |
+
OpenAI-compatible `/v1/chat/completions` endpoint on Modal GPU. It loads base model + LoRA adapter once at container start and keeps warm. It auto-scales to zero after 5 min idle (`scaledown_window=300`). It handles up to 10 concurrent requests (`@modal.concurrent(max_inputs=10)`).
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
Deploy:
|
| 166 |
```bash
|
|
|
|
| 180 |
```
|
| 181 |
|
| 182 |
### Modal API Deprecation Fixes Applied
|
| 183 |
+
|
| 184 |
+
During deployment I hit two Modal SDK deprecations and fixed them:
|
| 185 |
1. `container_idle_timeout` β `scaledown_window` (deprecated 2025-02-24)
|
| 186 |
2. `allow_concurrent_inputs` β `@modal.concurrent(max_inputs=10)` decorator (deprecated 2025-04-09)
|
| 187 |
|
| 188 |
### Budget: Separate $100 serving budget
|
| 189 |
+
|
| 190 |
+
This is separate from the ~$11.54 training budget I already spent. Serving costs:
|
| 191 |
- A10G active: ~$5.04/hr
|
| 192 |
- With scale-to-zero: ~$0.50-2.00/day typical
|
| 193 |
- Health check endpoint at `/health` for monitoring
|
|
|
|
| 196 |
|
| 197 |
## 3. Gradio Model Switching β Backend Implemented, UI Deferred
|
| 198 |
|
| 199 |
+
### Status: Backend ready, UI placement deferred to other agent
|
| 200 |
|
| 201 |
### Implemented: `core/llm_zerogpu_lora.py`
|
| 202 |
|
| 203 |
+
LoRA-aware ZeroGPU backend. It has the same API as `llm_zerogpu.py` (`chat_json`, `warm`, `backend_status`) but wraps the base model with `PeftModel.from_pretrained()` when `CHIEF_ENGINEER_LORA_REPO` is set. It is import-guarded β safe no-op if torch/transformers are absent.
|
|
|
|
|
|
|
| 204 |
|
| 205 |
### Implemented: `app.py` backend infrastructure
|
| 206 |
|
| 207 |
+
I added this to `app.py` (merged with UI agent's concurrent changes):
|
| 208 |
- `MODEL_OPTIONS` list: "Base (Gemma 4 E4B)", "LoRA v2 (Standard E4B)", "LoRA v3 (QAT E4B)", "Modal API (remote)"
|
| 209 |
- `MODEL_LORA_MAP` dict: maps UI labels β HF Hub adapter repo IDs
|
| 210 |
- `_apply_model_choice()` function: sets `CHIEF_ENGINEER_LORA_REPO` and `CHIEF_ENGINEER_BACKEND` env vars, reloads `core.llm` module
|
|
|
|
| 212 |
- `core.llm_zerogpu_lora` imported at Space startup alongside `core.llm_zerogpu`
|
| 213 |
|
| 214 |
### UI placement rolled back
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
+
Per user request (another agent is handling the Gradio UI), I removed the dropdown widget placement and HTML note from `app.py`. The backend infrastructure remains so the UI agent can wire it in.
|
| 217 |
+
|
| 218 |
+
### UI Agent Handoff (2026-06-14)
|
| 219 |
|
| 220 |
**Already done (do NOT re-implement):**
|
| 221 |
+
- `core/llm_zerogpu_lora.py` β LoRA-aware ZeroGPU backend
|
| 222 |
+
- `app.py` β `_apply_model_choice()` function, `MODEL_OPTIONS` list, `MODEL_LORA_MAP` dict
|
| 223 |
+
- `app.py` β `build_job()` now accepts `model_choice` parameter
|
| 224 |
+
- `app.py` β `core.llm_zerogpu_lora` imported at startup
|
| 225 |
|
| 226 |
**What the UI agent needs to do:**
|
| 227 |
1. Add a `gr.Dropdown` with `MODEL_OPTIONS` choices in the STUDIO tab
|
| 228 |
2. Wire `model_choice` into the `build_job` call in the event handler
|
| 229 |
+
3. Add info line: "Local users: get LoRA models from HF Hub or `ollama pull`"
|
| 230 |
4. `_apply_model_choice()` handles all backend switching automatically
|
| 231 |
|
| 232 |
---
|
|
|
|
| 248 |
## 5. Files Created/Modified
|
| 249 |
|
| 250 |
| File | Action | Purpose |
|
| 251 |
+
|------|--------|---------|
|
| 252 |
| `core/llm_zerogpu.py` | βοΈ Modified | E2BβE4B fix |
|
| 253 |
| `core/llm_zerogpu_lora.py` | β¨ Created | LoRA-aware ZeroGPU backend |
|
| 254 |
| `app.py` | βοΈ Modified | Add model selector dropdown + wiring |
|
scripts/export_deliberation.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export the Chief Engineer's multi-persona DELIBERATION as a HF-ready trace.
|
| 2 |
+
|
| 3 |
+
The lesson ledger (scripts/export_trace.py) shares *what the agent learned*. This
|
| 4 |
+
shares *how the agent thinks*: the turn-by-turn argument between the personas on
|
| 5 |
+
each job β O'Brien proposes, the Spine vetoes unsafe values, La Forge gives a
|
| 6 |
+
skeptical second opinion (and can dispute β the operator overrides), the
|
| 7 |
+
deterministic world prints, La Forge grades each run, then delivers a run verdict.
|
| 8 |
+
|
| 9 |
+
Our own schema (one row per turn): session_id, track, turn, agent, role, act, stance,
|
| 10 |
+
content, + the job context (material/geometry/bed/env) so each row is self-describing.
|
| 11 |
+
|
| 12 |
+
Side-effect-free: runs against a throwaway ledger + policy in a temp dir, so the
|
| 13 |
+
shipped state is never touched. Offline-safe: with no LLM the personas fall back to
|
| 14 |
+
their deterministic voices, so the trace is fully reproducible.
|
| 15 |
+
|
| 16 |
+
Run: `make deliberation` (or `uv run python -m scripts.export_deliberation`) β dist/deliberation/
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import sys
|
| 22 |
+
import tempfile
|
| 23 |
+
from datetime import datetime, timedelta, timezone
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) # repo root on path
|
| 27 |
+
|
| 28 |
+
from core import inspector, seed_lessons
|
| 29 |
+
from core.chief_engineer import advise
|
| 30 |
+
from core.ledger import LedgerManager
|
| 31 |
+
from core.models import Advice, Environment, Job, PrintSettings
|
| 32 |
+
from core.spine import SpineValidator
|
| 33 |
+
from learn.loop import run_iteration
|
| 34 |
+
from learn.policy import LearnedPolicy
|
| 35 |
+
|
| 36 |
+
try: # ingestion is optional / removable (mirrors app.py)
|
| 37 |
+
from ingest.distill import reference_block
|
| 38 |
+
except Exception:
|
| 39 |
+
def reference_block(_material): # type: ignore
|
| 40 |
+
return []
|
| 41 |
+
|
| 42 |
+
DIST = Path(__file__).resolve().parent.parent / "dist" / "deliberation"
|
| 43 |
+
HF_REPO = "kylebrodeur/chief-engineer-deliberation"
|
| 44 |
+
|
| 45 |
+
# Representative jobs, chosen to exercise the full range of La Forge's stances
|
| 46 |
+
# (concur / caution / disputeβoverride) and the print-loop's climb to clean.
|
| 47 |
+
JOBS = [
|
| 48 |
+
("ABS", "overhang", "edge", 26.0, 60.0, 4), # off-center ABS + thin fan β dispute β override
|
| 49 |
+
("PETG", "overhang", "center", 24.0, 55.0, 3), # thin fan for an overhang β caution
|
| 50 |
+
("PLA", "adhesion", "center", 21.0, 45.0, 3), # inside sane bounds β concur
|
| 51 |
+
("TPU", "stringing", "edge", 23.0, 65.0, 3), # humid + short retraction β caution
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
ROLE = {
|
| 55 |
+
"O'Brien": "Chief Engineer",
|
| 56 |
+
"La Forge": "QA Inspector",
|
| 57 |
+
"Spine": "Safety Spine",
|
| 58 |
+
"World": "Outcome Simulator",
|
| 59 |
+
"Operator": "Operator",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
CARD = """---
|
| 63 |
+
license: mit
|
| 64 |
+
task_categories: [text-generation]
|
| 65 |
+
language: [en]
|
| 66 |
+
tags: [3d-printing, additive-manufacturing, agent-trace, multi-agent, deliberation, build-small-hackathon]
|
| 67 |
+
pretty_name: Chief Engineer β Deliberation Traces
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
# Chief Engineer β Deliberation Traces
|
| 71 |
+
|
| 72 |
+
Turn-by-turn **multi-persona deliberation** from **The Chief Engineer**, a small local
|
| 73 |
+
Gemma agent built for the HF Build Small hackathon (Backyard AI). Where the
|
| 74 |
+
[lesson ledger](https://huggingface.co/datasets/kylebrodeur/chief-engineer-ledger)
|
| 75 |
+
records *what the agent learned*, this records *how it reasons*: the argument between
|
| 76 |
+
the personas on each job. It grows two ways: a reproducible static export
|
| 77 |
+
(`make deliberation`) and **live turns logged on every run of the Space** (gated on
|
| 78 |
+
`HF_TOKEN`; config + agent reasoning only, never PII or uploaded files).
|
| 79 |
+
|
| 80 |
+
Each row is one **turn**:
|
| 81 |
+
|
| 82 |
+
- **O'Brien** (Chief Engineer) β proposes settings + reasoning over precedent.
|
| 83 |
+
- **Spine** (Safety Spine) β deterministically vetoes/clamps unsafe values.
|
| 84 |
+
- **La Forge** (QA Inspector) β a separate, skeptical voice: second opinion before the
|
| 85 |
+
print (`concur` / `caution` / `dispute`), a grade on each run, and a run verdict.
|
| 86 |
+
- **Operator** β the human, who can override a `dispute` and proceed.
|
| 87 |
+
- **World** (Outcome Simulator) β the deterministic physics-lite world that reports the
|
| 88 |
+
actual print outcome (the agent never grades its own work).
|
| 89 |
+
|
| 90 |
+
The integrity rule made literal: the proposer never marks its own homework.
|
| 91 |
+
|
| 92 |
+
## Schema
|
| 93 |
+
|
| 94 |
+
`session_id, track, turn, agent, role, act, stance, content, material, geometry,
|
| 95 |
+
bed_position, env_temp, env_humidity, ts`
|
| 96 |
+
|
| 97 |
+
`track` is the phase β `preflight` (propose β veto β second opinion β override),
|
| 98 |
+
`print-loop` (simulate β grade, per iteration), `review` (run verdict).
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _settings_line(s: PrintSettings) -> str:
|
| 103 |
+
return (f"nozzle {s.nozzle_temp:.0f}Β°C, bed {s.bed_temp:.0f}Β°C, fan {s.fan_pct:.0f}%, "
|
| 104 |
+
f"first-layer fan {s.first_layer_fan_pct:.0f}%, retraction {s.retraction_mm:.1f}mm")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def export() -> Path:
|
| 108 |
+
DIST.mkdir(parents=True, exist_ok=True)
|
| 109 |
+
out = DIST / "deliberations.jsonl"
|
| 110 |
+
|
| 111 |
+
# throwaway state so the shipped ledger/policy are never mutated
|
| 112 |
+
tmp = Path(tempfile.mkdtemp(prefix="ce-delib-"))
|
| 113 |
+
ledger = LedgerManager(path=tmp / "ledger.jsonl")
|
| 114 |
+
seed_lessons.ensure_seeded(ledger)
|
| 115 |
+
spine = SpineValidator()
|
| 116 |
+
|
| 117 |
+
import json
|
| 118 |
+
|
| 119 |
+
clock = datetime(2026, 6, 14, 12, 0, 0, tzinfo=timezone.utc)
|
| 120 |
+
rows: list[dict] = []
|
| 121 |
+
|
| 122 |
+
def emit(job_id, track, turn, agent, act, content, *, ctx, stance=""):
|
| 123 |
+
nonlocal clock
|
| 124 |
+
clock += timedelta(seconds=7)
|
| 125 |
+
rows.append({
|
| 126 |
+
"session_id": job_id, "track": track, "turn": turn,
|
| 127 |
+
"agent": agent, "role": ROLE[agent], "act": act, "stance": stance,
|
| 128 |
+
"content": content.strip(),
|
| 129 |
+
"material": ctx["material"], "geometry": ctx["geometry"],
|
| 130 |
+
"bed_position": ctx["bed_position"],
|
| 131 |
+
"env_temp": ctx["env_temp"], "env_humidity": ctx["env_humidity"],
|
| 132 |
+
"ts": clock.isoformat(),
|
| 133 |
+
})
|
| 134 |
+
|
| 135 |
+
for material, geometry, bed, temp, hum, iters in JOBS:
|
| 136 |
+
job_id = f"{material}-{geometry}-{bed}".lower()
|
| 137 |
+
job = Job(geometry_type=geometry, material=material, bed_position=bed)
|
| 138 |
+
env = Environment(temp=temp, humidity=hum)
|
| 139 |
+
ctx = {"material": material, "geometry": geometry, "bed_position": bed,
|
| 140 |
+
"env_temp": temp, "env_humidity": hum}
|
| 141 |
+
# fresh policy per job so the loop's climb starts from baseline each time
|
| 142 |
+
policy = LearnedPolicy(path=tmp / f"policy-{job_id}.json")
|
| 143 |
+
|
| 144 |
+
# ββ preflight: propose β veto β second opinion β (override) ββ
|
| 145 |
+
retrieved = ledger.retrieve(material, geometry, env.temp, env.humidity)
|
| 146 |
+
rec = advise(job, env, retrieved, reference_block(material),
|
| 147 |
+
policy.policy_note(material, geometry, env))
|
| 148 |
+
checked = spine.check(rec.advice.settings, material)
|
| 149 |
+
t = 1
|
| 150 |
+
emit(job_id, "preflight", t, "O'Brien", "propose",
|
| 151 |
+
f"{rec.advice.reasoning}\nProposed: {_settings_line(checked.settings)}.", ctx=ctx)
|
| 152 |
+
t += 1
|
| 153 |
+
emit(job_id, "preflight", t, "Spine", "veto",
|
| 154 |
+
("Clamped: " + " Β· ".join(checked.vetoes)) if checked.vetoes
|
| 155 |
+
else "Within the safe envelope for this material β no clamp.", ctx=ctx,
|
| 156 |
+
stance="clamped" if checked.requires_approval else "clear")
|
| 157 |
+
t += 1
|
| 158 |
+
verdict = inspector.second_opinion(job, env, checked.settings, rec.advice)
|
| 159 |
+
emit(job_id, "preflight", t, "La Forge", "second_opinion",
|
| 160 |
+
f"{verdict.headline} β {verdict.detail}", ctx=ctx, stance=verdict.stance)
|
| 161 |
+
if verdict.stance.lower() == "dispute":
|
| 162 |
+
t += 1
|
| 163 |
+
emit(job_id, "preflight", t, "Operator", "override",
|
| 164 |
+
"Acknowledged La Forge's objection. Proceeding to print on the operator's call.",
|
| 165 |
+
ctx=ctx, stance="override")
|
| 166 |
+
|
| 167 |
+
# ββ print-loop: simulate β grade, per iteration ββ
|
| 168 |
+
for n in range(1, iters + 1):
|
| 169 |
+
t += 1
|
| 170 |
+
r = run_iteration(job, env, policy, ledger, n, record=False)
|
| 171 |
+
clamp = " (Spine clamped a setting)" if r.clamped else ""
|
| 172 |
+
emit(job_id, "print-loop", t, "World", "simulate",
|
| 173 |
+
f"Iteration {n}: {r.result.detail}.{clamp} Policy: {r.learned}.", ctx=ctx,
|
| 174 |
+
stance=r.result.outcome)
|
| 175 |
+
t += 1
|
| 176 |
+
g = inspector.grade_iteration(geometry, r.result)
|
| 177 |
+
emit(job_id, "print-loop", t, "La Forge", "grade",
|
| 178 |
+
f"{g.headline} β {g.detail}", ctx=ctx, stance=g.stance)
|
| 179 |
+
|
| 180 |
+
# ββ review: one verdict across the run ββ
|
| 181 |
+
# rebuild records for the summary from a fresh deterministic pass
|
| 182 |
+
sess_records = []
|
| 183 |
+
rpolicy = LearnedPolicy(path=tmp / f"policy-rev-{job_id}.json")
|
| 184 |
+
for n in range(1, iters + 1):
|
| 185 |
+
sess_records.append(run_iteration(job, env, rpolicy, ledger, n, record=False))
|
| 186 |
+
summary = inspector.summarize_run(sess_records, material=material, geometry=geometry)
|
| 187 |
+
t += 1
|
| 188 |
+
emit(job_id, "review", t, "La Forge", "verdict",
|
| 189 |
+
f"{summary.headline} β {summary.detail}", ctx=ctx, stance=summary.stance)
|
| 190 |
+
|
| 191 |
+
with out.open("w", encoding="utf-8") as f:
|
| 192 |
+
for row in rows:
|
| 193 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 194 |
+
(DIST / "README.md").write_text(CARD, encoding="utf-8")
|
| 195 |
+
|
| 196 |
+
jobs = len({r["session_id"] for r in rows})
|
| 197 |
+
print(f"exported {len(rows)} turns across {jobs} jobs β {out}")
|
| 198 |
+
print(f"dataset card β {DIST / 'README.md'}")
|
| 199 |
+
print(f"publish: hf upload {HF_REPO} {DIST} . --repo-type dataset")
|
| 200 |
+
return out
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
if __name__ == "__main__":
|
| 204 |
+
export()
|