Upload app/agents/behavioral_agent.py with huggingface_hub
Browse files- app/agents/behavioral_agent.py +549 -0
app/agents/behavioral_agent.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Behavioral Agent - Updated without camera eye tracking
|
| 3 |
+
|
| 4 |
+
Tracks behavioral signals through:
|
| 5 |
+
- Mouse movements (hesitation, click patterns)
|
| 6 |
+
- Scroll behavior (reversals, speed)
|
| 7 |
+
- Keyboard patterns (typing speed, corrections)
|
| 8 |
+
- Time patterns (learning duration, break frequency)
|
| 9 |
+
- Hand gestures (trained by user)
|
| 10 |
+
- Page focus/blur events
|
| 11 |
+
- Selection patterns
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import time
|
| 15 |
+
from typing import Dict, List, Any, Optional
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
from datetime import datetime
|
| 18 |
+
from collections import deque
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class MouseData:
|
| 24 |
+
"""Mouse movement data"""
|
| 25 |
+
x: float
|
| 26 |
+
y: float
|
| 27 |
+
velocity: float
|
| 28 |
+
acceleration: float
|
| 29 |
+
click_count: int
|
| 30 |
+
selection_count: int
|
| 31 |
+
right_click: bool
|
| 32 |
+
timestamp: datetime
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class ScrollData:
|
| 37 |
+
"""Scroll behavior data"""
|
| 38 |
+
scroll_up_count: int
|
| 39 |
+
scroll_down_count: int
|
| 40 |
+
reversal_count: int
|
| 41 |
+
avg_scroll_speed: float
|
| 42 |
+
max_scroll_speed: float
|
| 43 |
+
direction_changes: int
|
| 44 |
+
timestamp: datetime
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass
|
| 48 |
+
class KeyboardData:
|
| 49 |
+
"""Keyboard activity data"""
|
| 50 |
+
key_presses: int
|
| 51 |
+
backspaces: int
|
| 52 |
+
typing_speed: float
|
| 53 |
+
corrections: int
|
| 54 |
+
pause_duration: float
|
| 55 |
+
timestamp: datetime
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class FocusData:
|
| 60 |
+
"""Tab/window focus data"""
|
| 61 |
+
is_focused: bool
|
| 62 |
+
visible_duration: float
|
| 63 |
+
hidden_duration: float
|
| 64 |
+
tab_switches: int
|
| 65 |
+
timestamp: datetime
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
@dataclass
|
| 69 |
+
class BehavioralSignal:
|
| 70 |
+
"""Aggregated behavioral signal"""
|
| 71 |
+
signal_type: str
|
| 72 |
+
value: float
|
| 73 |
+
timestamp: datetime
|
| 74 |
+
source: str
|
| 75 |
+
metadata: Dict = field(default_factory=dict)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@dataclass
|
| 79 |
+
class TabActivity:
|
| 80 |
+
"""Tab activity tracking"""
|
| 81 |
+
tab_id: str
|
| 82 |
+
url: str
|
| 83 |
+
title: str
|
| 84 |
+
active_time: float = 0
|
| 85 |
+
scroll_depth: float = 0
|
| 86 |
+
clicks: int = 0
|
| 87 |
+
keystrokes: int = 0
|
| 88 |
+
focus_count: int = 0
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class BehavioralAgent:
|
| 92 |
+
"""
|
| 93 |
+
Agent that tracks behavioral signals without camera access.
|
| 94 |
+
|
| 95 |
+
Signal Sources:
|
| 96 |
+
1. Mouse - movements, clicks, selections
|
| 97 |
+
2. Scroll - patterns, reversals, speed
|
| 98 |
+
3. Keyboard - typing, corrections, pauses
|
| 99 |
+
4. Focus - tab switches, visibility
|
| 100 |
+
5. Hand Gestures - trained by user
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
def __init__(self, user_id: str, config: Optional[Dict] = None):
|
| 104 |
+
self.user_id = user_id
|
| 105 |
+
self.config = config or {}
|
| 106 |
+
|
| 107 |
+
self.signal_buffer = deque(maxlen=100)
|
| 108 |
+
self.mouse_buffer = deque(maxlen=50)
|
| 109 |
+
self.scroll_buffer = deque(maxlen=50)
|
| 110 |
+
self.keyboard_buffer = deque(maxlen=50)
|
| 111 |
+
self.focus_buffer = deque(maxlen=50)
|
| 112 |
+
self.gesture_buffer = deque(maxlen=20)
|
| 113 |
+
|
| 114 |
+
self.baseline_established = False
|
| 115 |
+
self.baseline_metrics = {}
|
| 116 |
+
|
| 117 |
+
self.tab_tracking: Dict[str, TabActivity] = {}
|
| 118 |
+
self.current_tab: Optional[str] = None
|
| 119 |
+
|
| 120 |
+
self.session_start = datetime.now()
|
| 121 |
+
self.total_active_time = 0
|
| 122 |
+
self.total_break_time = 0
|
| 123 |
+
|
| 124 |
+
self.last_mouse_move = None
|
| 125 |
+
self.last_scroll_time = None
|
| 126 |
+
self.last_keypress = None
|
| 127 |
+
self.scroll_direction = 1
|
| 128 |
+
|
| 129 |
+
def add_mouse_data(self, data: Dict) -> MouseData:
|
| 130 |
+
"""Add mouse movement data"""
|
| 131 |
+
mouse_data = MouseData(
|
| 132 |
+
x=data.get('x', 0),
|
| 133 |
+
y=data.get('y', 0),
|
| 134 |
+
velocity=data.get('velocity', 0),
|
| 135 |
+
acceleration=data.get('acceleration', 0),
|
| 136 |
+
click_count=data.get('click_count', 0),
|
| 137 |
+
selection_count=data.get('selection_count', 0),
|
| 138 |
+
right_click=data.get('right_click', False),
|
| 139 |
+
timestamp=datetime.now()
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
self.mouse_buffer.append(mouse_data)
|
| 143 |
+
|
| 144 |
+
if self.last_mouse_move:
|
| 145 |
+
dt = (mouse_data.timestamp - self.last_mouse_move).total_seconds()
|
| 146 |
+
if dt > 2:
|
| 147 |
+
signal = BehavioralSignal(
|
| 148 |
+
signal_type='hesitation',
|
| 149 |
+
value=min(dt / 10, 1.0),
|
| 150 |
+
timestamp=mouse_data.timestamp,
|
| 151 |
+
source='mouse',
|
| 152 |
+
metadata={'duration': dt}
|
| 153 |
+
)
|
| 154 |
+
self.signal_buffer.append(signal)
|
| 155 |
+
|
| 156 |
+
self.last_mouse_move = mouse_data.timestamp
|
| 157 |
+
return mouse_data
|
| 158 |
+
|
| 159 |
+
def add_scroll_data(self, data: Dict) -> ScrollData:
|
| 160 |
+
"""Add scroll behavior data"""
|
| 161 |
+
direction = data.get('direction', 'down')
|
| 162 |
+
|
| 163 |
+
if self.last_scroll_time:
|
| 164 |
+
dt = (datetime.now() - self.last_scroll_time).total_seconds()
|
| 165 |
+
if dt > 0.5 and direction != self.scroll_direction:
|
| 166 |
+
reversal = BehavioralSignal(
|
| 167 |
+
signal_type='scroll_reversal',
|
| 168 |
+
value=0.5,
|
| 169 |
+
timestamp=datetime.now(),
|
| 170 |
+
source='scroll'
|
| 171 |
+
)
|
| 172 |
+
self.signal_buffer.append(reversal)
|
| 173 |
+
|
| 174 |
+
self.scroll_direction = 1 if direction == 'down' else -1
|
| 175 |
+
self.last_scroll_time = datetime.now()
|
| 176 |
+
|
| 177 |
+
scroll_data = ScrollData(
|
| 178 |
+
scroll_up_count=data.get('scroll_up_count', 0),
|
| 179 |
+
scroll_down_count=data.get('scroll_down_count', 0),
|
| 180 |
+
reversal_count=data.get('reversal_count', 0),
|
| 181 |
+
avg_scroll_speed=data.get('avg_scroll_speed', 0),
|
| 182 |
+
max_scroll_speed=data.get('max_scroll_speed', 0),
|
| 183 |
+
direction_changes=data.get('direction_changes', 0),
|
| 184 |
+
timestamp=datetime.now()
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
self.scroll_buffer.append(scroll_data)
|
| 188 |
+
return scroll_data
|
| 189 |
+
|
| 190 |
+
def add_keyboard_data(self, data: Dict) -> KeyboardData:
|
| 191 |
+
"""Add keyboard activity data"""
|
| 192 |
+
keyboard_data = KeyboardData(
|
| 193 |
+
key_presses=data.get('key_presses', 0),
|
| 194 |
+
backspaces=data.get('backspaces', 0),
|
| 195 |
+
typing_speed=data.get('typing_speed', 0),
|
| 196 |
+
corrections=data.get('corrections', 0),
|
| 197 |
+
pause_duration=data.get('pause_duration', 0),
|
| 198 |
+
timestamp=datetime.now()
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
self.keyboard_buffer.append(keyboard_data)
|
| 202 |
+
|
| 203 |
+
if keyboard_data.pause_duration > 5:
|
| 204 |
+
signal = BehavioralSignal(
|
| 205 |
+
signal_type='typing_pause',
|
| 206 |
+
value=min(keyboard_data.pause_duration / 30, 1.0),
|
| 207 |
+
timestamp=keyboard_data.timestamp,
|
| 208 |
+
source='keyboard',
|
| 209 |
+
metadata={'pause_duration': keyboard_data.pause_duration}
|
| 210 |
+
)
|
| 211 |
+
self.signal_buffer.append(signal)
|
| 212 |
+
|
| 213 |
+
return keyboard_data
|
| 214 |
+
|
| 215 |
+
def add_focus_data(self, data: Dict) -> FocusData:
|
| 216 |
+
"""Add tab/window focus data"""
|
| 217 |
+
focus_data = FocusData(
|
| 218 |
+
is_focused=data.get('is_focused', True),
|
| 219 |
+
visible_duration=data.get('visible_duration', 0),
|
| 220 |
+
hidden_duration=data.get('hidden_duration', 0),
|
| 221 |
+
tab_switches=data.get('tab_switches', 0),
|
| 222 |
+
timestamp=datetime.now()
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
self.focus_buffer.append(focus_data)
|
| 226 |
+
|
| 227 |
+
if not focus_data.is_focused:
|
| 228 |
+
signal = BehavioralSignal(
|
| 229 |
+
signal_type='unfocused',
|
| 230 |
+
value=0.8,
|
| 231 |
+
timestamp=focus_data.timestamp,
|
| 232 |
+
source='focus'
|
| 233 |
+
)
|
| 234 |
+
self.signal_buffer.append(signal)
|
| 235 |
+
|
| 236 |
+
return focus_data
|
| 237 |
+
|
| 238 |
+
def add_tab_data(self, data: Dict):
|
| 239 |
+
"""Track tab activity"""
|
| 240 |
+
tab_id = data.get('tab_id')
|
| 241 |
+
if not tab_id:
|
| 242 |
+
return
|
| 243 |
+
|
| 244 |
+
if tab_id not in self.tab_tracking:
|
| 245 |
+
self.tab_tracking[tab_id] = TabActivity(
|
| 246 |
+
tab_id=tab_id,
|
| 247 |
+
url=data.get('url', ''),
|
| 248 |
+
title=data.get('title', '')
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
tab = self.tab_tracking[tab_id]
|
| 252 |
+
|
| 253 |
+
if 'scroll_depth' in data:
|
| 254 |
+
tab.scroll_depth = data['scroll_depth']
|
| 255 |
+
if 'clicks' in data:
|
| 256 |
+
tab.clicks += data['clicks']
|
| 257 |
+
if 'keystrokes' in data:
|
| 258 |
+
tab.keystrokes += data['keystrokes']
|
| 259 |
+
if 'focus' in data and data['focus']:
|
| 260 |
+
tab.focus_count += 1
|
| 261 |
+
|
| 262 |
+
def add_gesture_signal(self, gesture_signal: Dict):
|
| 263 |
+
"""Add hand gesture signal"""
|
| 264 |
+
signal = BehavioralSignal(
|
| 265 |
+
signal_type=gesture_signal.get('signal_type', 'unknown'),
|
| 266 |
+
value=gesture_signal.get('confidence', 0.5),
|
| 267 |
+
timestamp=datetime.fromisoformat(gesture_signal.get('timestamp', datetime.now().isoformat())),
|
| 268 |
+
source='gesture',
|
| 269 |
+
metadata={
|
| 270 |
+
'gesture_name': gesture_signal.get('gesture_name', ''),
|
| 271 |
+
'raw_confidence': gesture_signal.get('raw_confidence', 0),
|
| 272 |
+
'description': gesture_signal.get('description', '')
|
| 273 |
+
}
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
self.gesture_buffer.append(signal)
|
| 277 |
+
self.signal_buffer.append(signal)
|
| 278 |
+
|
| 279 |
+
def process_signals(self, data: Dict) -> List[BehavioralSignal]:
|
| 280 |
+
"""Process all incoming behavioral signals"""
|
| 281 |
+
signals = []
|
| 282 |
+
|
| 283 |
+
if 'mouse' in data:
|
| 284 |
+
mouse_data = self.add_mouse_data(data['mouse'])
|
| 285 |
+
signal = self._analyze_mouse_pattern(mouse_data)
|
| 286 |
+
signals.append(signal)
|
| 287 |
+
|
| 288 |
+
if 'scroll' in data:
|
| 289 |
+
scroll_data = self.add_scroll_data(data['scroll'])
|
| 290 |
+
signal = self._analyze_scroll_behavior(scroll_data)
|
| 291 |
+
signals.append(signal)
|
| 292 |
+
|
| 293 |
+
if 'keyboard' in data:
|
| 294 |
+
keyboard_data = self.add_keyboard_data(data['keyboard'])
|
| 295 |
+
signal = self._analyze_keyboard_pattern(keyboard_data)
|
| 296 |
+
signals.append(signal)
|
| 297 |
+
|
| 298 |
+
if 'focus' in data:
|
| 299 |
+
focus_data = self.add_focus_data(data['focus'])
|
| 300 |
+
signal = self._analyze_focus(focus_data)
|
| 301 |
+
signals.append(signal)
|
| 302 |
+
|
| 303 |
+
if 'gesture' in data:
|
| 304 |
+
self.add_gesture_signal(data['gesture'])
|
| 305 |
+
|
| 306 |
+
if 'tab' in data:
|
| 307 |
+
self.add_tab_data(data['tab'])
|
| 308 |
+
|
| 309 |
+
return signals
|
| 310 |
+
|
| 311 |
+
def _analyze_mouse_pattern(self, data: MouseData) -> BehavioralSignal:
|
| 312 |
+
"""Analyze mouse patterns"""
|
| 313 |
+
hesitation_threshold = 50
|
| 314 |
+
velocity_threshold = 100
|
| 315 |
+
|
| 316 |
+
hesitation_score = min(data.acceleration / hesitation_threshold, 1.0)
|
| 317 |
+
velocity_score = min(data.velocity / velocity_threshold, 1.0)
|
| 318 |
+
|
| 319 |
+
combined = (hesitation_score * 0.6 + velocity_score * 0.4)
|
| 320 |
+
|
| 321 |
+
if data.right_click:
|
| 322 |
+
combined = max(combined, 0.6)
|
| 323 |
+
|
| 324 |
+
return BehavioralSignal(
|
| 325 |
+
signal_type='mouse_activity',
|
| 326 |
+
value=combined,
|
| 327 |
+
timestamp=data.timestamp,
|
| 328 |
+
source='mouse',
|
| 329 |
+
metadata={
|
| 330 |
+
'hesitation': hesitation_score,
|
| 331 |
+
'velocity': velocity_score,
|
| 332 |
+
'clicks': data.click_count
|
| 333 |
+
}
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def _analyze_scroll_behavior(self, data: ScrollData) -> BehavioralSignal:
|
| 337 |
+
"""Analyze scroll behavior"""
|
| 338 |
+
if data.scroll_up_count + data.scroll_down_count == 0:
|
| 339 |
+
return BehavioralSignal(
|
| 340 |
+
signal_type='scroll_inactive',
|
| 341 |
+
value=0.0,
|
| 342 |
+
timestamp=data.timestamp,
|
| 343 |
+
source='scroll'
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
reversal_ratio = data.reversal_count / max(
|
| 347 |
+
data.scroll_up_count + data.scroll_down_count, 1
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
reversal_score = min(reversal_ratio * 2, 1.0)
|
| 351 |
+
|
| 352 |
+
speed_variation = 0
|
| 353 |
+
if data.max_scroll_speed > 0:
|
| 354 |
+
speed_variation = abs(data.avg_scroll_speed - data.max_scroll_speed) / data.max_scroll_speed
|
| 355 |
+
|
| 356 |
+
combined = reversal_score * 0.7 + speed_variation * 0.3
|
| 357 |
+
|
| 358 |
+
return BehavioralSignal(
|
| 359 |
+
signal_type='scroll_pattern',
|
| 360 |
+
value=combined,
|
| 361 |
+
timestamp=data.timestamp,
|
| 362 |
+
source='scroll',
|
| 363 |
+
metadata={
|
| 364 |
+
'reversal_ratio': reversal_ratio,
|
| 365 |
+
'speed_variation': speed_variation
|
| 366 |
+
}
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
def _analyze_keyboard_pattern(self, data: KeyboardData) -> BehavioralSignal:
|
| 370 |
+
"""Analyze keyboard patterns"""
|
| 371 |
+
if data.key_presses == 0:
|
| 372 |
+
return BehavioralSignal(
|
| 373 |
+
signal_type='no_keyboard',
|
| 374 |
+
value=0.0,
|
| 375 |
+
timestamp=data.timestamp,
|
| 376 |
+
source='keyboard'
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
correction_rate = data.corrections / max(data.key_presses, 1)
|
| 380 |
+
|
| 381 |
+
correction_score = min(correction_rate * 3, 1.0)
|
| 382 |
+
|
| 383 |
+
pause_score = min(data.pause_duration / 20, 1.0)
|
| 384 |
+
|
| 385 |
+
combined = correction_score * 0.6 + pause_score * 0.4
|
| 386 |
+
|
| 387 |
+
return BehavioralSignal(
|
| 388 |
+
signal_type='typing_pattern',
|
| 389 |
+
value=combined,
|
| 390 |
+
timestamp=data.timestamp,
|
| 391 |
+
source='keyboard',
|
| 392 |
+
metadata={
|
| 393 |
+
'correction_rate': correction_rate,
|
| 394 |
+
'pause_duration': data.pause_duration
|
| 395 |
+
}
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
def _analyze_focus(self, data: FocusData) -> BehavioralSignal:
|
| 399 |
+
"""Analyze focus patterns"""
|
| 400 |
+
if data.tab_switches > 0:
|
| 401 |
+
return BehavioralSignal(
|
| 402 |
+
signal_type='tab_switch',
|
| 403 |
+
value=0.7,
|
| 404 |
+
timestamp=data.timestamp,
|
| 405 |
+
source='focus',
|
| 406 |
+
metadata={'switches': data.tab_switches}
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
return BehavioralSignal(
|
| 410 |
+
signal_type='focus_level',
|
| 411 |
+
value=1.0 if data.is_focused else 0.3,
|
| 412 |
+
timestamp=data.timestamp,
|
| 413 |
+
source='focus'
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
def calculate_confusion_score(self, signals: List[BehavioralSignal]) -> float:
|
| 417 |
+
"""Calculate combined confusion score"""
|
| 418 |
+
if not signals:
|
| 419 |
+
return 0.0
|
| 420 |
+
|
| 421 |
+
scores = []
|
| 422 |
+
weights = []
|
| 423 |
+
|
| 424 |
+
mouse_signals = [s for s in signals if s.source == 'mouse']
|
| 425 |
+
scroll_signals = [s for s in signals if s.source == 'scroll']
|
| 426 |
+
keyboard_signals = [s for s in signals if s.source == 'keyboard']
|
| 427 |
+
gesture_signals = [s for s in signals if s.source == 'gesture']
|
| 428 |
+
|
| 429 |
+
for s in mouse_signals:
|
| 430 |
+
if s.signal_type == 'hesitation':
|
| 431 |
+
scores.append(s.value)
|
| 432 |
+
weights.append(0.2)
|
| 433 |
+
|
| 434 |
+
for s in scroll_signals:
|
| 435 |
+
if s.signal_type == 'scroll_reversal':
|
| 436 |
+
scores.append(s.value)
|
| 437 |
+
weights.append(0.25)
|
| 438 |
+
|
| 439 |
+
for s in keyboard_signals:
|
| 440 |
+
if s.signal_type == 'typing_pause':
|
| 441 |
+
scores.append(s.value)
|
| 442 |
+
weights.append(0.15)
|
| 443 |
+
|
| 444 |
+
for s in gesture_signals:
|
| 445 |
+
if s.signal_type in ['confusion', 'cognitive_load']:
|
| 446 |
+
scores.append(s.value)
|
| 447 |
+
weights.append(0.3)
|
| 448 |
+
|
| 449 |
+
if not scores:
|
| 450 |
+
return 0.0
|
| 451 |
+
|
| 452 |
+
total_weight = sum(weights) if weights else 1
|
| 453 |
+
weighted_sum = sum(s * w for s, w in zip(scores, weights))
|
| 454 |
+
|
| 455 |
+
return min(weighted_sum / total_weight if total_weight > 0 else 0, 1.0)
|
| 456 |
+
|
| 457 |
+
def calculate_engagement_score(self) -> float:
|
| 458 |
+
"""Calculate overall engagement score"""
|
| 459 |
+
if not self.signal_buffer:
|
| 460 |
+
return 0.0
|
| 461 |
+
|
| 462 |
+
recent_signals = list(self.signal_buffer)[-20:]
|
| 463 |
+
|
| 464 |
+
mouse_count = sum(1 for s in recent_signals if s.source == 'mouse')
|
| 465 |
+
keyboard_count = sum(1 for s in recent_signals if s.source == 'keyboard')
|
| 466 |
+
scroll_count = sum(1 for s in recent_signals if s.source == 'scroll')
|
| 467 |
+
|
| 468 |
+
activity_score = (mouse_count + keyboard_count + scroll_count) / 30
|
| 469 |
+
|
| 470 |
+
focused_signals = [s for s in recent_signals if s.signal_type == 'focus_level' and s.value > 0.5]
|
| 471 |
+
focus_score = len(focused_signals) / max(len(recent_signals), 1)
|
| 472 |
+
|
| 473 |
+
gesture_count = sum(1 for s in recent_signals if s.source == 'gesture')
|
| 474 |
+
gesture_score = min(gesture_count / 5, 1.0)
|
| 475 |
+
|
| 476 |
+
return min((activity_score * 0.4 + focus_score * 0.4 + gesture_score * 0.2), 1.0)
|
| 477 |
+
|
| 478 |
+
def get_engagement_level(self) -> str:
|
| 479 |
+
"""Get engagement level description"""
|
| 480 |
+
score = self.calculate_engagement_score()
|
| 481 |
+
|
| 482 |
+
if score > 0.7:
|
| 483 |
+
return "highly_engaged"
|
| 484 |
+
elif score > 0.4:
|
| 485 |
+
return "moderately_engaged"
|
| 486 |
+
elif score > 0.2:
|
| 487 |
+
return "low_engagement"
|
| 488 |
+
else:
|
| 489 |
+
return "disengaged"
|
| 490 |
+
|
| 491 |
+
def get_session_summary(self) -> Dict:
|
| 492 |
+
"""Get session summary"""
|
| 493 |
+
duration = (datetime.now() - self.session_start).total_seconds()
|
| 494 |
+
|
| 495 |
+
return {
|
| 496 |
+
'duration_seconds': duration,
|
| 497 |
+
'active_time': self.total_active_time,
|
| 498 |
+
'break_time': self.total_break_time,
|
| 499 |
+
'total_signals': len(self.signal_buffer),
|
| 500 |
+
'mouse_events': len(self.mouse_buffer),
|
| 501 |
+
'scroll_events': len(self.scroll_buffer),
|
| 502 |
+
'keyboard_events': len(self.keyboard_buffer),
|
| 503 |
+
'gesture_signals': len(self.gesture_buffer),
|
| 504 |
+
'tabs_tracked': len(self.tab_tracking),
|
| 505 |
+
'engagement_score': self.calculate_engagement_score(),
|
| 506 |
+
'engagement_level': self.get_engagement_level(),
|
| 507 |
+
'confusion_score': self.calculate_confusion_score(list(self.signal_buffer)[-20:])
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
def establish_baseline(self, data_points: int = 50):
|
| 511 |
+
"""Establish baseline from comfortable learning periods"""
|
| 512 |
+
if len(self.signal_buffer) < data_points:
|
| 513 |
+
return
|
| 514 |
+
|
| 515 |
+
recent = list(self.signal_buffer)[-data_points:]
|
| 516 |
+
|
| 517 |
+
confusion_scores = [s.value for s in recent if 'confusion' in s.signal_type]
|
| 518 |
+
|
| 519 |
+
self.baseline_metrics = {
|
| 520 |
+
'avg_confusion': np.mean(confusion_scores) if confusion_scores else 0.3,
|
| 521 |
+
'std_confusion': np.std(confusion_scores) if confusion_scores else 0.2,
|
| 522 |
+
'baseline_established': True
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
self.baseline_established = True
|
| 526 |
+
|
| 527 |
+
def get_behavior_summary(self) -> Dict:
|
| 528 |
+
"""Get summary of behavioral analysis"""
|
| 529 |
+
recent_signals = list(self.signal_buffer)[-20:]
|
| 530 |
+
|
| 531 |
+
gesture_signals = [s for s in recent_signals if s.source == 'gesture']
|
| 532 |
+
last_gesture = gesture_signals[-1] if gesture_signals else None
|
| 533 |
+
|
| 534 |
+
return {
|
| 535 |
+
'total_signals': len(self.signal_buffer),
|
| 536 |
+
'recent_confusion': self.calculate_confusion_score(recent_signals),
|
| 537 |
+
'engagement_score': self.calculate_engagement_score(),
|
| 538 |
+
'engagement_level': self.get_engagement_level(),
|
| 539 |
+
'gesture_recognition_active': len(self.gesture_buffer) > 0,
|
| 540 |
+
'last_gesture': last_gesture.metadata if last_gesture else None,
|
| 541 |
+
'baseline_established': self.baseline_established,
|
| 542 |
+
'session_summary': self.get_session_summary(),
|
| 543 |
+
'signal_rates': {
|
| 544 |
+
'mouse': len([s for s in recent_signals if s.source == 'mouse']),
|
| 545 |
+
'scroll': len([s for s in recent_signals if s.source == 'scroll']),
|
| 546 |
+
'keyboard': len([s for s in recent_signals if s.source == 'keyboard']),
|
| 547 |
+
'gesture': len([s for s in recent_signals if s.source == 'gesture'])
|
| 548 |
+
}
|
| 549 |
+
}
|