"""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")