diff --git a/ai_mouse/data_adapters/__init__.py b/ai_mouse/data_adapters/__init__.py new file mode 100644 index 0000000..202278a --- /dev/null +++ b/ai_mouse/data_adapters/__init__.py @@ -0,0 +1 @@ +"""Data adapters: convert external datasets to the project's traces.jsonl format.""" diff --git a/ai_mouse/data_adapters/balabit.py b/ai_mouse/data_adapters/balabit.py new file mode 100644 index 0000000..1febc2e --- /dev/null +++ b/ai_mouse/data_adapters/balabit.py @@ -0,0 +1,79 @@ +"""Adapter for the Balabit Mouse Dynamics Challenge dataset. + +Source: https://github.com/balabit/Mouse-Dynamics-Challenge + +Each session is a CSV file with columns: + record timestamp, client timestamp, button, state, x, y + +Where: + state ∈ {Move, Pressed, Released, Drag, Scroll} + button ∈ {NoButton, Left, Right, Wheel} + +We extract "click-anchored" trajectory segments: each Pressed event +defines a target, and the W ms of Move events preceding it form one +training trace. +""" +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass +from pathlib import Path + +logger = logging.getLogger(__name__) + + +@dataclass +class MouseEvent: + """A single mouse event from a Balabit CSV row.""" + + t_ms: int # client timestamp in milliseconds (relative to session start) + button: str # "NoButton", "Left", "Right", "Wheel" + state: str # "Move", "Pressed", "Released", "Drag", "Scroll" + x: int + y: int + + +@dataclass +class Segment: + """A click-anchored trajectory segment ready to be written to JSONL.""" + + events: list[MouseEvent] # only Move events, sorted by t_ms ascending + click_x: int # the Pressed event's x coordinate + click_y: int # the Pressed event's y coordinate + click_t_ms: int # the Pressed event's timestamp + session_id: str # e.g. "user7_session_42" + + +def parse_session_csv(path: Path) -> list[MouseEvent]: + """Stub — implemented in Task 3.""" + raise NotImplementedError + + +def segment_by_clicks( + events: list[MouseEvent], + window_ms: int, + session_id: str, +) -> list[Segment]: + """Stub — implemented in Task 4.""" + raise NotImplementedError + + +def filter_segments( + segments: list[Segment], + min_events: int, + min_dist: int, + max_span_ms: int, + max_gap_ms: int, +) -> list[Segment]: + """Stub — implemented in Task 5.""" + raise NotImplementedError + + +def process_session( + csv_path: Path, + output_jsonl: Path, + config, +) -> int: + """Stub — implemented in Task 6.""" + raise NotImplementedError diff --git a/tests/test_balabit_adapter.py b/tests/test_balabit_adapter.py new file mode 100644 index 0000000..c959769 --- /dev/null +++ b/tests/test_balabit_adapter.py @@ -0,0 +1,36 @@ +"""Tests for Balabit Mouse Dynamics Challenge data adapter.""" +from __future__ import annotations + +from pathlib import Path + +import pytest + + +def test_module_exports(): + """The adapter module must export the public functions used by CLI.""" + from ai_mouse.data_adapters import balabit + assert hasattr(balabit, "parse_session_csv") + assert hasattr(balabit, "segment_by_clicks") + assert hasattr(balabit, "filter_segments") + assert hasattr(balabit, "process_session") + assert hasattr(balabit, "MouseEvent") + assert hasattr(balabit, "Segment") + + +def test_mouse_event_dataclass(): + """MouseEvent has expected fields.""" + from ai_mouse.data_adapters.balabit import MouseEvent + e = MouseEvent(t_ms=100, button="NoButton", state="Move", x=300, y=400) + assert e.t_ms == 100 + assert e.state == "Move" + assert e.x == 300 + + +def test_segment_dataclass(): + """Segment has expected fields.""" + from ai_mouse.data_adapters.balabit import MouseEvent, Segment + events = [MouseEvent(t_ms=0, button="NoButton", state="Move", x=10, y=20)] + s = Segment(events=events, click_x=100, click_y=200, click_t_ms=500, session_id="user1_s1") + assert s.events == events + assert s.click_x == 100 + assert s.session_id == "user1_s1"