""" telemetry/logger.py — append-only JSONL logging for Q&A turns. Usage: from analyzer.telemetry.logger import QALogger log = QALogger("logs/chat.jsonl") log.write(user="find farming grants", intent="search", args={"keyword":"farming"}, answer_md="...", ok=True, latency_ms=321, meta={"model":"gpt-4.1-mini"}) """ from __future__ import annotations from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any, Dict, Optional import json, os, time, threading, uuid, datetime as dt @dataclass class QATurn: ts: str session_id: str turn_id: str user: str intent: str args: Dict[str, Any] = field(default_factory=dict) answer_md: str = "" ok: bool = True latency_ms: Optional[int] = None meta: Dict[str, Any] = field(default_factory=dict) class QALogger: def __init__(self, path: str | Path): self.path = Path(path) self.path.parent.mkdir(parents=True, exist_ok=True) self._lock = threading.Lock() self._session = os.getenv("CHAT_SESSION_ID") or str(uuid.uuid4()) def write(self, *, user: str, intent: str, args: Dict[str, Any], answer_md: str, ok: bool, latency_ms: Optional[int], meta: Optional[Dict[str, Any]] = None) -> None: turn = QATurn( ts=dt.datetime.utcnow().isoformat(timespec="seconds") + "Z", session_id=self._session, turn_id=str(uuid.uuid4()), user=str(user), intent=str(intent), args=args or {}, answer_md=str(answer_md or ""), ok=bool(ok), latency_ms=int(latency_ms) if latency_ms is not None else None, meta=meta or {}, ) line = json.dumps(asdict(turn), ensure_ascii=False) with self._lock: with open(self.path, "a", encoding="utf-8") as fh: fh.write(line + "\n")