asa / scripts /lib /chat_eval /state_io.py
defenestrate's picture
ASA Space: squashed history (instructions and settings live in the private pack)
526950e
Raw History Blame Contribute Delete
4.66 kB
"""HF Dataset I/O for the retrieval-tuning loop.
Three files are exchanged between the GitHub Actions tuner and the
chat-logs HF Dataset:
- turns.jsonl (chat writes; tuner annotates with judges)
- retrieval_tuning.json (tuner writes; chat reads at boot)
- tuning_log.jsonl (tuner-only audit trail)
fetch_state() downloads all three to derived/student-chat/, materializing
defaults for any file missing from the dataset (cold start).
push_state() uploads the same three local paths back.
Lazy imports of huggingface_hub via re-exported wrappers so tests can patch
this module's symbols and so this module imports without huggingface_hub
installed (local dev).
"""
from __future__ import annotations
import json
import logging
import shutil
from pathlib import Path
from scripts.lib.chat_eval.tuning import empty_state
from scripts.lib.turns_merge import is_not_found_error
logger = logging.getLogger(__name__)
_DATASET_FILES = ("turns.jsonl", "retrieval_tuning.json", "tuning_log.jsonl")
def hf_hub_download(*args, **kwargs):
"""Re-export so tests can patch this module's reference."""
from huggingface_hub import hf_hub_download as _real
return _real(*args, **kwargs)
def HfApi(*args, **kwargs):
"""Re-export so tests can patch this module's reference."""
from huggingface_hub import HfApi as _real
return _real(*args, **kwargs)
def fetch_state(*, repo_id: str, token: str, derived_dir: Path) -> None:
"""Download the three dataset files into derived_dir.
Missing files get sensible defaults so the rest of the loop has real
files to read:
- turns.jsonl missing -> empty file (no turns to judge)
- retrieval_tuning.json missing -> empty_state()
- tuning_log.jsonl missing -> empty file
"""
derived_dir.mkdir(parents=True, exist_ok=True)
for fname in _DATASET_FILES:
local = derived_dir / fname
try:
tmp_path = hf_hub_download(
repo_id=repo_id,
filename=fname,
repo_type="dataset",
token=token,
)
shutil.copyfile(tmp_path, local)
logger.info("state_io: fetched %s", fname)
except Exception as e:
if not is_not_found_error(e):
raise
if fname == "retrieval_tuning.json":
local.write_text(json.dumps(empty_state()), encoding="utf-8")
else:
local.write_text("", encoding="utf-8")
logger.info("state_io: %s missing on remote; wrote default.", fname)
def push_state(*, repo_id: str, token: str, derived_dir: Path,
commit_message: str) -> None:
"""Upload the three files from derived_dir to the dataset.
For turns.jsonl: refetch remote and merge by turn_id (local wins on
conflict; remote-only turns preserved). retrieval_tuning.json and
tuning_log.jsonl are tuner-only writes and overwrite remote.
Raises FileNotFoundError if any file is missing locally — we never
push an empty over a populated remote by accident.
"""
api = HfApi()
for fname in _DATASET_FILES:
local = derived_dir / fname
if not local.is_file():
raise FileNotFoundError(
f"state_io.push_state: missing local file {local}; "
f"refusing to push."
)
staging: Path | None = None
if fname == "turns.jsonl":
merged_bytes = _merge_turns_with_remote(
local, repo_id=repo_id, token=token,
)
staging = local.with_suffix(local.suffix + ".merged")
staging.write_bytes(merged_bytes)
upload_path = str(staging)
else:
upload_path = str(local)
try:
api.upload_file(
path_or_fileobj=upload_path,
path_in_repo=fname,
repo_id=repo_id,
repo_type="dataset",
token=token,
commit_message=commit_message,
)
logger.info("state_io: pushed %s", fname)
finally:
if staging is not None:
try:
staging.unlink()
except OSError:
pass
def _merge_turns_with_remote(local_path: Path, *, repo_id: str,
token: str) -> bytes:
"""Tuner-side adapter: read local file, delegate to turns_merge."""
from scripts.lib.turns_merge import merge_turns_with_remote
return merge_turns_with_remote(local_path.read_bytes(),
repo_id=repo_id, token=token)