Skip to main content

synheart_sensor_agent/flux/
adapter.rs

1//! Adapter for converting sensor agent data to synheart-flux format.
2//!
3//! This module bridges the gap between the sensor agent's keyboard/mouse
4//! events and the behavior module's event types.
5
6use crate::collector::types::{KeyboardEvent, MouseEvent, MouseEventType, ShortcutEvent};
7use crate::core::windowing::EventWindow;
8use synheart_flux::behavior::types::{ScrollEvent, TapEvent, TypingEvent};
9use synheart_flux::behavior::{BehaviorEvent, BehaviorEventType, BehaviorSession};
10
11/// Adapter for converting sensor events to behavioral session format.
12pub struct SensorBehaviorAdapter {
13    device_id: String,
14    timezone: String,
15}
16
17impl SensorBehaviorAdapter {
18    /// Create a new adapter with device and timezone info.
19    pub fn new(device_id: String, timezone: String) -> Self {
20        Self {
21            device_id,
22            timezone,
23        }
24    }
25
26    /// Create with system defaults.
27    pub fn with_defaults() -> Self {
28        Self {
29            device_id: format!("sensor-{}", uuid::Uuid::new_v4()),
30            timezone: "UTC".to_string(),
31        }
32    }
33
34    /// Convert an event window to a behavior session.
35    pub fn convert(&self, session_id: &str, window: &EventWindow) -> BehaviorSession {
36        let mut events = Vec::new();
37
38        // Convert keyboard events to typing events
39        for kb_event in &window.keyboard_events {
40            if kb_event.is_key_down {
41                events.push(self.keyboard_to_behavior(kb_event));
42            }
43        }
44
45        // Convert shortcut events to typing events for behavioral analysis
46        for shortcut_event in &window.shortcut_events {
47            events.push(self.shortcut_to_behavior(shortcut_event));
48        }
49
50        // Convert mouse events to behavioral events
51        for mouse_event in &window.mouse_events {
52            if let Some(behavior_event) = self.mouse_to_behavior(mouse_event) {
53                events.push(behavior_event);
54            }
55        }
56
57        // Sort by timestamp
58        events.sort_by_key(|e| e.timestamp);
59
60        BehaviorSession {
61            session_id: session_id.to_string(),
62            device_id: self.device_id.clone(),
63            timezone: self.timezone.clone(),
64            start_time: window.start,
65            end_time: window.end,
66            events,
67        }
68    }
69
70    /// Convert a keyboard event to a typing behavior event.
71    fn keyboard_to_behavior(&self, kb: &KeyboardEvent) -> BehaviorEvent {
72        BehaviorEvent {
73            timestamp: kb.timestamp,
74            event_type: BehaviorEventType::Typing,
75            scroll: None,
76            tap: None,
77            swipe: None,
78            interruption: None,
79            typing: Some(TypingEvent {
80                typing_speed_cpm: None, // Will be computed at session level
81                cadence_stability: None,
82                duration_sec: None,
83                pause_count: None,
84                start_at: None,
85                end_at: None,
86                typing_tap_count: None,
87                mean_inter_tap_interval_ms: None,
88                typing_cadence_variability: None,
89                typing_cadence_stability: None,
90                typing_gap_count: None,
91                typing_gap_ratio: None,
92                typing_burstiness: None,
93                typing_activity_ratio: None,
94                typing_interaction_intensity: None,
95                deep_typing: None,
96                number_of_backspace: None,
97                number_of_delete: None,
98                number_of_cut: None,
99                number_of_paste: None,
100                number_of_copy: None,
101            }),
102            app_switch: None,
103        }
104    }
105
106    /// Convert a shortcut event to a typing behavior event.
107    fn shortcut_to_behavior(&self, shortcut: &ShortcutEvent) -> BehaviorEvent {
108        BehaviorEvent {
109            timestamp: shortcut.timestamp,
110            event_type: BehaviorEventType::Typing,
111            scroll: None,
112            tap: None,
113            swipe: None,
114            interruption: None,
115            typing: Some(TypingEvent {
116                typing_speed_cpm: None,
117                cadence_stability: None,
118                duration_sec: None,
119                pause_count: None,
120                start_at: None,
121                end_at: None,
122                typing_tap_count: None,
123                mean_inter_tap_interval_ms: None,
124                typing_cadence_variability: None,
125                typing_cadence_stability: None,
126                typing_gap_count: None,
127                typing_gap_ratio: None,
128                typing_burstiness: None,
129                typing_activity_ratio: None,
130                typing_interaction_intensity: None,
131                deep_typing: None,
132                number_of_backspace: None,
133                number_of_delete: None,
134                number_of_cut: None,
135                number_of_paste: None,
136                number_of_copy: None,
137            }),
138            app_switch: None,
139        }
140    }
141
142    /// Convert a mouse event to a behavioral event.
143    fn mouse_to_behavior(&self, mouse: &MouseEvent) -> Option<BehaviorEvent> {
144        match mouse.event_type {
145            MouseEventType::Move => {
146                // Convert mouse movement to a scroll-like event for behavioral analysis
147                // This captures interaction intensity
148                Some(BehaviorEvent {
149                    timestamp: mouse.timestamp,
150                    event_type: BehaviorEventType::Scroll,
151                    scroll: Some(ScrollEvent {
152                        velocity: mouse.delta_magnitude,
153                        direction: None, // Cursor movement doesn't have direction
154                        direction_reversal: false,
155                    }),
156                    tap: None,
157                    swipe: None,
158                    interruption: None,
159                    typing: None,
160                    app_switch: None,
161                })
162            }
163            MouseEventType::LeftClick | MouseEventType::RightClick => {
164                Some(BehaviorEvent {
165                    timestamp: mouse.timestamp,
166                    event_type: BehaviorEventType::Tap,
167                    scroll: None,
168                    tap: Some(TapEvent {
169                        tap_duration_ms: Some(100), // Estimated click duration
170                        long_press: false,
171                    }),
172                    swipe: None,
173                    interruption: None,
174                    typing: None,
175                    app_switch: None,
176                })
177            }
178            MouseEventType::Scroll => {
179                Some(BehaviorEvent {
180                    timestamp: mouse.timestamp,
181                    event_type: BehaviorEventType::Scroll,
182                    scroll: Some(ScrollEvent {
183                        velocity: mouse.delta_magnitude,
184                        direction: None, // Could be inferred from scroll_direction
185                        direction_reversal: false,
186                    }),
187                    tap: None,
188                    swipe: None,
189                    interruption: None,
190                    typing: None,
191                    app_switch: None,
192                })
193            }
194        }
195    }
196}
197
198/// Convenience function to convert an event window to a behavior session.
199pub fn convert_to_behavior_session(
200    session_id: &str,
201    window: &EventWindow,
202    device_id: Option<&str>,
203) -> BehaviorSession {
204    let adapter = match device_id {
205        Some(id) => SensorBehaviorAdapter::new(id.to_string(), "UTC".to_string()),
206        None => SensorBehaviorAdapter::with_defaults(),
207    };
208    adapter.convert(session_id, window)
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use chrono::{Duration, Utc};
215
216    #[test]
217    fn test_adapter_creation() {
218        let adapter = SensorBehaviorAdapter::with_defaults();
219        assert!(adapter.device_id.starts_with("sensor-"));
220    }
221
222    #[test]
223    fn test_empty_window_conversion() {
224        let adapter = SensorBehaviorAdapter::with_defaults();
225        let window = EventWindow::new(Utc::now(), Duration::seconds(10));
226        let session = adapter.convert("test-session", &window);
227
228        assert_eq!(session.session_id, "test-session");
229        assert!(session.events.is_empty());
230    }
231}