namish10 commited on
Commit
690ed7d
·
verified ·
1 Parent(s): 74632ff

Upload app/agents/behavioral_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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
+ }