microfactory-lab / core /field_log.py
kylebrodeur's picture
space: clean sync from chief-engineer root incl. screenshots and latest README
d7337a8 verified
Raw
History Blame Contribute Delete
6.39 kB
"""Field log — append-only interaction logging for the live Space.
Each BUILD appends one JSONL row to a local file; CommitScheduler pushes it
to a separate HF Dataset repo every N minutes. Gated on HF_TOKEN — if the
secret isn't set, nothing is written or pushed (local/offline unaffected).
Design per docs/RESEARCH-NEEDS.md "Capturing live Space interactions":
- Logs job config only (material, geometry, env, settings, risks, backend)
- Never logs PII or uploaded mesh files
- Rows are candidates, never auto-promoted into the curated ledger
- Privacy disclosure shown in UI when active
"""
from __future__ import annotations
import json
import os
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
FIELD_LOG_DIR = Path(__file__).resolve().parent.parent / "field_logs"
FIELD_LOG_FILE = FIELD_LOG_DIR / "interactions.jsonl"
FIELD_LOG_REPO = "build-small-hackathon/chief-engineer-field-log"
FLUSH_MINUTES = 5
_scheduler: Any = None
_lock = threading.Lock()
def _get_scheduler():
"""Lazy-init the CommitScheduler. Returns None if HF_TOKEN is missing."""
global _scheduler
token = os.environ.get("HF_TOKEN", "").strip()
if not token:
return None
if _scheduler is None:
with _lock:
if _scheduler is None:
try:
from huggingface_hub import CommitScheduler
except ImportError:
return None
FIELD_LOG_DIR.mkdir(parents=True, exist_ok=True)
# Ensure the file exists so the scheduler has something to track
if not FIELD_LOG_FILE.exists():
FIELD_LOG_FILE.write_text("", encoding="utf-8")
_scheduler = CommitScheduler(
repo_id=FIELD_LOG_REPO,
repo_type="dataset",
folder_path=str(FIELD_LOG_DIR),
every=FLUSH_MINUTES,
token=token,
allow_patterns=["*.jsonl"],
)
return _scheduler
def is_active() -> bool:
"""True if field logging is live (HF_TOKEN present + scheduler initialized)."""
return _get_scheduler() is not None
# Canonical FLAT schema — every row carries exactly these keys (None when N/A), all
# scalars/strings (no nested dicts/lists). This is what makes the HF dataset viewer
# render cleanly: a rectangular, well-typed table instead of ragged/nested JSON.
_CANON = (
"ts", "kind", "material", "geometry", "env_temp", "env_humidity",
"bed_position", "printer", "backend", "used_fallback",
"nozzle_temp", "bed_temp", "fan_pct", "retraction_mm", "first_layer_fan_pct",
"risks", "risk_count", "inspector_stance", "inspector_headline", "agreement",
"outcome", "quality", "iterations", "q_start", "q_end", "first_clean",
)
def _write_row(fields: dict) -> bool:
"""Normalize to the canonical flat schema (drop unknown keys, fill missing with
None) and append one JSONL line. Gated + exception-safe — never breaks a run."""
try:
sched = _get_scheduler()
if sched is None:
return False
row = {k: None for k in _CANON}
row.update({k: v for k, v in fields.items() if k in _CANON})
row["ts"] = datetime.now(timezone.utc).isoformat()
with _lock:
with FIELD_LOG_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
try:
sched.trigger()
except Exception:
pass
return True
except Exception:
return False # logging is best-effort — never break a run
def log_event(kind: str, payload: dict) -> bool:
"""Append one interaction row of any KIND — build | second_opinion | simulate |
record | print_run | print_override — normalized to the canonical flat schema.
Same gate (HF_TOKEN) + privacy rules: config/outcomes only, never PII or files."""
return _write_row({**payload, "kind": kind})
def log_build(job: dict, env: dict, settings: dict, advice: dict,
backend: str, used_fallback: bool) -> bool:
"""Append one BUILD row (flattened settings + risks-as-string for the viewer)."""
risks = advice.get("risks", []) or []
return _write_row({
"kind": "build",
"material": job.get("material"), "geometry": job.get("geometry_type"),
"env_temp": env.get("temp"), "env_humidity": env.get("humidity"),
"bed_position": job.get("bed_position"),
"printer": job.get("printer", "Creality Ender 3 V2"),
"backend": backend, "used_fallback": used_fallback,
"nozzle_temp": settings.get("nozzle_temp"), "bed_temp": settings.get("bed_temp"),
"fan_pct": settings.get("fan_pct"), "retraction_mm": settings.get("retraction_mm"),
"first_layer_fan_pct": settings.get("first_layer_fan_pct"),
"risks": ", ".join(str(r.get("risk")) for r in risks if isinstance(r, dict)) or None,
"risk_count": len(risks),
})
def log_print_override(job: dict, env: dict, overrides: dict) -> bool:
"""Append one PRINT_OVERRIDE row with the operator-changed settings flattened.
The canonical schema only accepts flat scalars, so this helper unpacks the
override object rather than nesting it under a 'settings' key.
"""
return _write_row({
"kind": "print_override",
"material": job.get("material"), "geometry": job.get("geometry_type"),
"env_temp": env.get("temp"), "env_humidity": env.get("humidity"),
"bed_position": job.get("bed_position"),
"printer": job.get("printer", "Creality Ender 3 V2"),
"nozzle_temp": overrides.get("nozzle_temp"),
"bed_temp": overrides.get("bed_temp"),
"fan_pct": overrides.get("fan_pct"),
"retraction_mm": overrides.get("retraction_mm"),
"first_layer_fan_pct": overrides.get("first_layer_fan_pct"),
})
def privacy_notice() -> str:
"""One-line UI disclosure, shown only when logging is active."""
return (
"<div class='ce-sub' style='font-size:10px;opacity:0.6;margin-top:4px;'>"
"🔒 Job config logged to improve the model (no personal data, no uploaded files). "
f"<a href='https://huggingface.co/datasets/{FIELD_LOG_REPO}' target='_blank'>"
"View the field log →</a></div>"
)