| """Logging and tracing module for pipeline transparency. |
| |
| Stores JSONL event logs and complete pipeline traces for debugging, |
| demo narration, and possible trace publication. |
| """ |
|
|
| import json |
| import uuid |
| from datetime import datetime, timezone |
| from pathlib import Path |
| from typing import Any, Optional |
|
|
|
|
| |
| VALID_EVENT_TYPES = [ |
| "task_revealed", |
| "task_completed", |
| "hint_used", |
| "task_skipped", |
| "photo_uploaded", |
| "journal_recorded", |
| "score_updated", |
| "game_finished", |
| ] |
|
|
|
|
| def _ensure_log_dir(log_dir: str) -> Path: |
| """Ensure the log directory exists and return its Path.""" |
| path = Path(log_dir) |
| path.mkdir(parents=True, exist_ok=True) |
| return path |
|
|
|
|
| def _now_iso() -> str: |
| """Return current time as ISO-8601 string.""" |
| return datetime.now(timezone.utc).isoformat() |
|
|
|
|
| def log_event( |
| session_id: str, |
| event_type: str, |
| payload: dict, |
| team_id: str = "team-a", |
| log_dir: str = "app/logs", |
| ) -> dict: |
| """Log a gameplay event to a JSONL file. |
| |
| Each event is appended as a single JSON line to ``events.jsonl`` inside |
| *log_dir*. Returns the full event dict that was written so callers can |
| pass it onward (e.g. into the scoring pipeline). |
| |
| Args: |
| session_id: Session identifier. |
| event_type: One of the ``VALID_EVENT_TYPES``. |
| payload: Event-specific data dictionary. |
| team_id: Team that generated the event (default ``"team-a"``). |
| log_dir: Directory to store logs. |
| |
| Returns: |
| The complete event dict that was persisted. |
| |
| Raises: |
| ValueError: If *event_type* is not in the allowed set. |
| """ |
| if event_type not in VALID_EVENT_TYPES: |
| raise ValueError( |
| f"Invalid event_type '{event_type}'. " |
| f"Must be one of: {VALID_EVENT_TYPES}" |
| ) |
|
|
| event = { |
| "event_id": str(uuid.uuid4()), |
| "timestamp": _now_iso(), |
| "session_id": session_id, |
| "team_id": team_id, |
| "event_type": event_type, |
| "payload": payload, |
| } |
|
|
| log_path = _ensure_log_dir(log_dir) / "events.jsonl" |
| with open(log_path, "a", encoding="utf-8") as fh: |
| fh.write(json.dumps(event, ensure_ascii=False) + "\n") |
|
|
| return event |
|
|
|
|
| def load_events(session_id: Optional[str] = None, log_dir: str = "app/logs") -> list[dict]: |
| """Load events from the JSONL log file, optionally filtered by session. |
| |
| Args: |
| session_id: If provided, only return events for this session. |
| log_dir: Directory containing event logs. |
| |
| Returns: |
| List of event dicts. |
| """ |
| log_path = Path(log_dir) / "events.jsonl" |
| if not log_path.exists(): |
| return [] |
|
|
| events: list[dict] = [] |
| with open(log_path, "r", encoding="utf-8") as fh: |
| for line in fh: |
| line = line.strip() |
| if not line: |
| continue |
| try: |
| event = json.loads(line) |
| except json.JSONDecodeError: |
| continue |
| if session_id and event.get("session_id") != session_id: |
| continue |
| events.append(event) |
|
|
| return events |
|
|
|
|
| def log_generation_trace( |
| session_id: str, |
| config: dict, |
| retrieved_examples: list[dict], |
| game: dict, |
| validation_passed: bool, |
| validation_failures: list[str], |
| repaired_game: Optional[dict] = None, |
| log_dir: str = "app/logs", |
| ) -> dict: |
| """Log the full generation pipeline trace for one session. |
| |
| Captures config → retrieval → generation → validation → repair in a |
| single JSON file for debugging and publication. |
| |
| Args: |
| session_id: Session identifier. |
| config: User-provided game configuration. |
| retrieved_examples: Examples retrieved by the grounding module. |
| game: Generated game JSON. |
| validation_passed: Whether the game passed validation. |
| validation_failures: Any validation failure messages. |
| repaired_game: Post-repair game dict, or None if no repair was needed. |
| log_dir: Directory to store logs. |
| |
| Returns: |
| The complete trace dict. |
| """ |
| trace = { |
| "session_id": session_id, |
| "timestamp": _now_iso(), |
| "config": config, |
| "retrieved_examples": retrieved_examples, |
| "generated_game": game, |
| "validation_passed": validation_passed, |
| "validation_failures": validation_failures, |
| "repaired_game": repaired_game, |
| } |
|
|
| trace_path = _ensure_log_dir(log_dir) / f"trace_{session_id}.json" |
| with open(trace_path, "w", encoding="utf-8") as fh: |
| json.dump(trace, fh, indent=2, ensure_ascii=False) |
|
|
| return trace |
|
|
|
|
| def save_trace(trace_data: dict, output_path: str) -> None: |
| """Save a complete pipeline trace for debugging and publication. |
| |
| Args: |
| trace_data: Dictionary containing all pipeline data. |
| output_path: Where to save the trace file. |
| """ |
| out = Path(output_path) |
| out.parent.mkdir(parents=True, exist_ok=True) |
| with open(out, "w", encoding="utf-8") as fh: |
| json.dump(trace_data, fh, indent=2, ensure_ascii=False) |
|
|