| """
|
| trace_log.py — capture model prompt→response traces for later fine-tuning.
|
|
|
| Every real narration turn is appended as one JSON line to a dated file under
|
| the trace directory. Each record is already in the standard SFT chat shape
|
| (a `messages` list ending in the assistant's raw response), with extra
|
| `context` fields alongside for curating/filtering the dataset later:
|
|
|
| {"ts": "...", "model": "...", "context": {...},
|
| "messages": [{"role":"system",...},{"role":"user",...},
|
| {"role":"assistant","content": "<raw model output>"}]}
|
|
|
| To build a training set, read the lines and keep `record["messages"]`
|
| (drop the trailing assistant turn at inference time).
|
|
|
| Controls:
|
| ARS_FABULA_TRACE = "1"|"on" (default) to log, "0"|"off"|"false" to disable
|
| ARS_FABULA_TRACE_DIR = output directory (default: "logs/traces")
|
|
|
| Mock/scripted responses are NOT logged — the engine skips logging when the
|
| model reports `is_mock` (see run_turn), so tests and mock fallbacks don't
|
| pollute the dataset.
|
| """
|
| from __future__ import annotations
|
| import os
|
| import json
|
| from datetime import datetime, date
|
| from pathlib import Path
|
| from typing import Optional
|
|
|
|
|
| _DISABLED = {"0", "off", "false", "no", ""}
|
|
|
|
|
| def traces_enabled() -> bool:
|
| return os.getenv("ARS_FABULA_TRACE", "1").strip().lower() not in _DISABLED
|
|
|
|
|
| def _trace_dir() -> Path:
|
| return Path(os.getenv("ARS_FABULA_TRACE_DIR", "logs/traces"))
|
|
|
|
|
| def log_turn(messages: list[dict], response: str, *,
|
| model: str = "", context: Optional[dict] = None) -> None:
|
| """Append one prompt→response trace as a JSON line.
|
|
|
| Best-effort: any IO/serialization error is swallowed so a logging
|
| problem can never break a turn. `messages` is the prompt sent to the
|
| model (system + user); the assistant's raw `response` is appended so the
|
| stored `messages` list is directly usable for supervised fine-tuning.
|
| """
|
| if not traces_enabled():
|
| return
|
| try:
|
| rec = {
|
| "ts": datetime.now().isoformat(timespec="seconds"),
|
| "model": model,
|
| "context": context or {},
|
| "messages": list(messages) + [{"role": "assistant", "content": response}],
|
| }
|
| out_dir = _trace_dir()
|
| out_dir.mkdir(parents=True, exist_ok=True)
|
| path = out_dir / f"turns-{date.today().isoformat()}.jsonl"
|
| with open(path, "a", encoding="utf-8") as f:
|
| f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
| except Exception:
|
|
|
| pass
|
|
|