# raw_session.py """ Modèle RawSession v1 pour l'agent_v0. Ce module définit la structure des données RawSession et la sérialisation en JSON au format rawsession_v1. """ from __future__ import annotations import json import os import time import uuid from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from config import AGENT_VERSION, SESSIONS_ROOT @dataclass class ScreenshotMeta: screenshot_id: str relative_path: str captured_at: str @dataclass class Event: t: float type: str window: Dict[str, Any] screenshot_id: Optional[str] = None # champs optionnels selon type button: Optional[str] = None pos: Optional[List[int]] = None keys: Optional[List[str]] = None idle_ms: Optional[int] = None scroll_delta: Optional[List[int]] = None # [dx, dy] pour la molette @dataclass class RawSession: schema_version: str session_id: str agent_version: str environment: Dict[str, Any] user: Dict[str, Any] context: Dict[str, Any] started_at: str ended_at: Optional[str] = None events: List[Event] = field(default_factory=list) screenshots: List[ScreenshotMeta] = field(default_factory=list) _start_time_monotonic: float = field(default_factory=time.monotonic, repr=False) @classmethod def create( cls, user_id: str, user_label: str = "", customer: str = "", training_label: str = "", notes: str = "", platform: str = "unknown", hostname: str = "unknown", screen_resolution: Optional[List[int]] = None, display_scale: float = 1.0, ) -> "RawSession": """Fabrique une nouvelle session avec les champs de base remplis.""" session_id = f"sess_{time.strftime('%Y%m%dT%H%M%S')}_{uuid.uuid4().hex[:8]}" started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) environment = { "platform": platform, "hostname": hostname, "screen": { "primary_resolution": screen_resolution or [0, 0], "display_scale": display_scale, }, } user = { "id": user_id, "label": user_label or user_id, } context = { "customer": customer, "training_label": training_label, "notes": notes, } return cls( schema_version="rawsession_v1", session_id=session_id, agent_version=AGENT_VERSION, environment=environment, user=user, context=context, started_at=started_at, ) # --- API temps relatif --- def now_relative(self) -> float: """Temps relatif (en secondes) depuis le début de la session.""" return time.monotonic() - self._start_time_monotonic # --- API screenshots --- def add_screenshot( self, screenshot_id: str, relative_path: str, captured_at: Optional[str] = None, ) -> None: """Enregistre les métadonnées d'un screenshot.""" captured_at = captured_at or time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) self.screenshots.append( ScreenshotMeta( screenshot_id=screenshot_id, relative_path=relative_path, captured_at=captured_at, ) ) # --- API événements --- def add_mouse_click_event( self, button: str, pos: List[int], window_title: str, app_name: str, screenshot_id: Optional[str], ) -> None: self.events.append( Event( t=self.now_relative(), type="mouse_click", button=button, pos=pos, window={"title": window_title, "app_name": app_name}, screenshot_id=screenshot_id, ) ) def add_key_combo_event( self, keys: List[str], window_title: str, app_name: str, screenshot_id: Optional[str], ) -> None: self.events.append( Event( t=self.now_relative(), type="key_combo", keys=keys, window={"title": window_title, "app_name": app_name}, screenshot_id=screenshot_id, ) ) def add_hover_idle_event( self, pos: List[int], idle_ms: int, window_title: str, app_name: str, screenshot_id: Optional[str], ) -> None: self.events.append( Event( t=self.now_relative(), type="hover_idle", pos=pos, idle_ms=idle_ms, window={"title": window_title, "app_name": app_name}, screenshot_id=screenshot_id, ) ) def add_scroll_event( self, pos: List[int], delta: List[int], window_title: str, app_name: str, screenshot_id: Optional[str], ) -> None: """delta = [dx, dy]""" self.events.append( Event( t=self.now_relative(), type="scroll", pos=pos, scroll_delta=delta, window={"title": window_title, "app_name": app_name}, screenshot_id=screenshot_id, ) ) def close(self) -> None: """Marque la fin de la session.""" self.ended_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) # --- Sérialisation --- def to_dict(self) -> Dict[str, Any]: return { "schema_version": self.schema_version, "session_id": self.session_id, "agent_version": self.agent_version, "environment": self.environment, "user": self.user, "context": self.context, "started_at": self.started_at, "ended_at": self.ended_at, "events": [e.__dict__ for e in self.events], "screenshots": [s.__dict__ for s in self.screenshots], } def save_json(self, base_dir: Optional[str] = None) -> str: """ Sauvegarde le JSON rawsession_v1 dans un sous-dossier de SESSIONS_ROOT. Retourne le chemin du fichier JSON. """ base_dir = base_dir or SESSIONS_ROOT session_dir = os.path.join(base_dir, self.session_id) os.makedirs(session_dir, exist_ok=True) json_path = os.path.join(session_dir, f"{self.session_id}.json") with open(json_path, "w", encoding="utf-8") as f: json.dump(self.to_dict(), f, ensure_ascii=False, indent=2) return json_path