"""Append-only JSONL log of user interactions (queries, follow-ups, feedback). One line per event in ``/interactions-YYYYMMDD.jsonl``. When FIXSENSE_LOG_REPO (a *private* HF dataset repo id) and HF_TOKEN are set, a background huggingface_hub.CommitScheduler pushes the folder every FIXSENSE_LOG_PUSH_MIN minutes (default 5) -- the Space's own disk is ephemeral, so this is what makes the log durable. A crash can lose at most the last interval. Each Space replica writes under its own subfolder (``path_in_repo``) so concurrent replicas never overwrite each other's files. """ from __future__ import annotations import json import logging import os import socket import threading from datetime import datetime, timezone from pathlib import Path logger = logging.getLogger(__name__) class InteractionLogger: def __init__(self, directory: str = "logs") -> None: self.dir = Path(directory) self.dir.mkdir(parents=True, exist_ok=True) self._lock = threading.Lock() self._scheduler = None repo = os.environ.get("FIXSENSE_LOG_REPO", "").strip() token = os.environ.get("HF_TOKEN", "").strip() if repo and token: try: from huggingface_hub import CommitScheduler self._scheduler = CommitScheduler( repo_id=repo, repo_type="dataset", folder_path=self.dir, path_in_repo=f"interactions/{socket.gethostname()}", every=float(os.environ.get("FIXSENSE_LOG_PUSH_MIN", "5")), private=True, token=token, ) self._lock = self._scheduler.lock # recommended: share the scheduler's lock logger.info("Interaction log will sync to private dataset %s", repo) except Exception: logger.exception("Could not start CommitScheduler; logging locally only") elif repo and not token: logger.warning("FIXSENSE_LOG_REPO is set but HF_TOKEN is not; logging locally only") def log(self, event: dict) -> None: now = datetime.now(timezone.utc) record = {"ts": now.isoformat(timespec="milliseconds"), **event} path = self.dir / f"interactions-{now:%Y%m%d}.jsonl" line = json.dumps(record, ensure_ascii=False, default=str) try: with self._lock: with open(path, "a", encoding="utf-8") as f: f.write(line + "\n") except Exception: logger.exception("Failed to write interaction log") def flush(self) -> None: """Push pending lines now (used on shutdown).""" if self._scheduler is not None: try: self._scheduler.trigger().result() except Exception: logger.exception("Final log push failed")