| """ |
| Study Orchestrator Agent |
| |
| The central coordination agent that orchestrates all learning agents: |
| 1. Coordinates DoubtPredictorAgent for proactive doubt capture |
| 2. Manages BehavioralAgent for signal analysis |
| 3. Triggers KnowledgeGraphAgent for graph updates |
| 4. Schedules RecallAgent for spaced repetition |
| 5. Integrates with Notion for permanent storage |
| 6. Syncs with Supabase for cross-device access |
| """ |
|
|
| import asyncio |
| from typing import Dict, List, Any, Optional |
| from dataclasses import dataclass, field |
| from datetime import datetime |
| from enum import Enum |
|
|
| from .doubt_predictor import DoubtPredictorAgent, DoubtPrediction |
| from .behavioral_agent import BehavioralAgent, BehavioralSignal |
| from .knowledge_graph_agent import KnowledgeGraphAgent |
| from .recall_agent import RecallAgent, RecallCard |
| from .peer_learning_agent import PeerLearningAgent |
|
|
|
|
| class SessionPhase(Enum): |
| PRE_LEARNING = "pre_learning" |
| ACTIVE_LEARNING = "active_learning" |
| REVIEW = "review" |
| BREAK = "break" |
| POST_LEARNING = "post_learning" |
|
|
|
|
| @dataclass |
| class LearningSession: |
| session_id: str |
| user_id: str |
| topic: str |
| phase: SessionPhase |
| started_at: datetime |
| ended_at: Optional[datetime] = None |
| predictions: List[DoubtPrediction] = field(default_factory=list) |
| captured_doubts: List[Dict] = field(default_factory=list) |
| behavioral_signals: List[BehavioralSignal] = field(default_factory=list) |
| recommendations: List[str] = field(default_factory=list) |
| xp_earned: int = 0 |
| notes: str = "" |
|
|
|
|
| @dataclass |
| class OrchestratorState: |
| current_session: Optional[LearningSession] = None |
| active_predictions: List[DoubtPrediction] = field(default_factory=list) |
| pending_recalls: List[RecallCard] = field(default_factory=list) |
| peer_insights: List[Dict] = field(default_factory=list) |
| gamification_state: Dict = field(default_factory=dict) |
|
|
|
|
| class StudyOrchestrator: |
| """ |
| Central orchestration agent that coordinates all learning agents. |
| |
| Workflow: |
| 1. PRE_LEARNING: Load predictions, check recall queue, get peer insights |
| 2. ACTIVE_LEARNING: Monitor behavioral signals, update predictions, capture doubts |
| 3. REVIEW: Trigger spaced repetition, update knowledge graph |
| 4. POST_LEARNING: Sync to Notion, update gamification, generate session summary |
| """ |
| |
| def __init__(self, user_id: str, config: Optional[Dict] = None): |
| self.user_id = user_id |
| self.config = config or {} |
| |
| self.doubt_predictor = DoubtPredictorAgent(user_id, config) |
| self.behavioral_agent = BehavioralAgent(user_id, config) |
| self.knowledge_graph = KnowledgeGraphAgent(user_id, config) |
| self.recall_agent = RecallAgent(user_id, config) |
| self.peer_agent = PeerLearningAgent(user_id, config) |
| |
| self.state = OrchestratorState() |
| |
| self.session_history = [] |
| |
| async def start_session(self, topic: str, subtopic: str = "") -> LearningSession: |
| """Start a new learning session""" |
| session = LearningSession( |
| session_id=f"session_{datetime.now().timestamp()}", |
| user_id=self.user_id, |
| topic=topic, |
| phase=SessionPhase.PRE_LEARNING, |
| started_at=datetime.now() |
| ) |
| |
| self.state.current_session = session |
| |
| learning_context = await self._build_learning_context(topic, subtopic) |
| |
| predictions = self.doubt_predictor.predict_doubts(learning_context, top_k=5) |
| session.predictions = predictions |
| self.state.active_predictions = predictions |
| |
| recalls = await self.recall_agent.get_due_recalls(topic) |
| self.state.pending_recalls = recalls |
| |
| peer_insights = await self.peer_agent.get_peer_insights(topic) |
| self.state.peer_insights = peer_insights |
| |
| return session |
| |
| async def update_session( |
| self, |
| behavioral_data: Dict, |
| captured_doubt: Optional[Dict] = None |
| ): |
| """Update session with new behavioral data and captured doubts""" |
| if not self.state.current_session: |
| return |
| |
| gesture_signal = behavioral_data.get('gesture_signal') |
| if gesture_signal: |
| self.behavioral_agent.add_gesture_signal(gesture_signal) |
| |
| learning_context = await self._build_learning_context( |
| self.state.current_session.topic, |
| '' |
| ) |
| learning_context['gesture_signal'] = gesture_signal |
| |
| if gesture_signal.get('signal_type') in ['confusion', 'cognitive_load', 'doubt_intent']: |
| new_predictions = self.doubt_predictor.predict_doubts( |
| learning_context, |
| top_k=3, |
| gesture_influence=gesture_signal.get('confidence', 0.5) |
| ) |
| for pred in new_predictions: |
| if pred.confidence > 0.5: |
| self.state.active_predictions.append(pred) |
| |
| signals = self.behavioral_agent.process_signals(behavioral_data) |
| self.state.current_session.behavioral_signals.extend(signals) |
| |
| if captured_doubt: |
| self.state.current_session.captured_doubts.append(captured_doubt) |
| |
| self.doubt_predictor.update_policy( |
| state=self.doubt_predictor.get_current_state(behavioral_data), |
| predicted_doubt=captured_doubt.get('predicted_from', ''), |
| actual_doubt=captured_doubt.get('doubt_text', ''), |
| reward=captured_doubt.get('reward', 1.0) |
| ) |
| |
| await self.knowledge_graph.add_doubt_to_graph(captured_doubt) |
| |
| confusion = self.behavioral_agent.calculate_confusion_score(signals) |
| |
| if confusion > 0.7 and len(self.state.current_session.captured_doubts) < 3: |
| learning_context = await self._build_learning_context( |
| self.state.current_session.topic, |
| '' |
| ) |
| learning_context['confusion_score'] = confusion |
| |
| new_predictions = self.doubt_predictor.predict_doubts(learning_context, top_k=3) |
| |
| for pred in new_predictions: |
| if pred.confidence > 0.5: |
| self.state.active_predictions.append(pred) |
| |
| async def trigger_review(self) -> List[RecallCard]: |
| """Trigger spaced repetition review""" |
| recalls = await self.recall_agent.get_due_recalls( |
| self.state.current_session.topic if self.state.current_session else None |
| ) |
| return recalls |
| |
| async def complete_review( |
| self, |
| recall_id: str, |
| quality: int |
| ) -> Dict: |
| """Complete a recall card review""" |
| result = await self.recall_agent.complete_review(recall_id, quality) |
| |
| if self.state.current_session: |
| xp = self._calculate_xp_for_review(quality) |
| self.state.current_session.xp_earned += xp |
| |
| self.state.gamification_state = await self._update_gamification(xp) |
| |
| return result |
| |
| async def end_session(self) -> Dict: |
| """End the current session and generate summary""" |
| if not self.state.current_session: |
| return {} |
| |
| session = self.state.current_session |
| session.ended_at = datetime.now() |
| |
| session_summary = { |
| 'session_id': session.session_id, |
| 'duration': (session.ended_at - session.started_at).total_seconds(), |
| 'topic': session.topic, |
| 'doubts_captured': len(session.captured_doubts), |
| 'predictions_made': len(session.predictions), |
| 'xp_earned': session.xp_earned, |
| 'predictions_accuracy': self._calculate_prediction_accuracy(session), |
| 'confusion_peaks': self._find_confusion_peaks(session.behavioral_signals), |
| 'topics_covered': list(set([ |
| d.get('topic', '') for d in session.captured_doubts |
| ])) |
| } |
| |
| self.session_history.append(session_summary) |
| |
| await self.knowledge_graph.sync_to_graph() |
| |
| await self._sync_to_notion(session) |
| |
| await self._sync_to_supabase(session_summary) |
| |
| self.state.current_session = None |
| |
| return session_summary |
| |
| async def _build_learning_context( |
| self, |
| topic: str, |
| subtopic: str |
| ) -> Dict: |
| """Build comprehensive learning context""" |
| context = { |
| 'topic': topic, |
| 'subtopic': subtopic, |
| 'progress': 0.0, |
| 'time_spent': 0, |
| 'confusion_score': 0.0, |
| 'eye_confidence': 0.0, |
| 'scroll_reversals': 0, |
| 'selections': 0, |
| 'prev_doubts': 0, |
| 'mastery': 0.0, |
| 'difficulty': 0.5, |
| 'streak': 0 |
| } |
| |
| if self.state.current_session: |
| context['time_spent'] = ( |
| datetime.now() - self.state.current_session.started_at |
| ).total_seconds() |
| context['prev_doubts'] = len(self.state.current_session.captured_doubts) |
| |
| if self.state.current_session.behavioral_signals: |
| signals = self.state.current_session.behavioral_signals[-10:] |
| context['confusion_score'] = self.behavioral_agent.calculate_confusion_score(signals) |
| |
| return context |
| |
| def _calculate_xp_for_review(self, quality: int) -> int: |
| """Calculate XP earned for review""" |
| base_xp = {1: 5, 2: 8, 3: 10, 4: 15, 5: 25} |
| return base_xp.get(quality, 5) |
| |
| async def _update_gamification(self, xp: int) -> Dict: |
| """Update gamification state""" |
| if 'total_xp' not in self.state.gamification_state: |
| self.state.gamification_state = { |
| 'total_xp': 0, |
| 'level': 1, |
| 'streak': 0, |
| 'fish_xp': 0, |
| 'achievements': [] |
| } |
| |
| self.state.gamification_state['total_xp'] += xp |
| self.state.gamification_state['fish_xp'] += xp // 2 |
| |
| self.state.gamification_state['level'] = self._calculate_level( |
| self.state.gamification_state['total_xp'] |
| ) |
| |
| return self.state.gamification_state |
| |
| def _calculate_level(self, xp: int) -> int: |
| """Calculate level from XP""" |
| level_thresholds = [0, 100, 300, 600, 1000, 1500, 2200, 3000, 4000, 5500] |
| for i, threshold in enumerate(level_thresholds): |
| if xp < threshold: |
| return max(1, i) |
| return len(level_thresholds) |
| |
| def _calculate_prediction_accuracy(self, session: LearningSession) -> float: |
| """Calculate accuracy of doubt predictions""" |
| if not session.predictions: |
| return 0.0 |
| |
| correct = 0 |
| for captured in session.captured_doubts: |
| predicted = captured.get('predicted_from', '') |
| actual = captured.get('doubt_text', '') |
| |
| for pred in session.predictions: |
| if pred.predicted_doubt.lower() in actual.lower(): |
| correct += 1 |
| break |
| |
| return correct / max(len(session.captured_doubts), 1) |
| |
| def _find_confusion_peaks(self, signals: List[BehavioralSignal]) -> List[Dict]: |
| """Find moments of peak confusion""" |
| peaks = [] |
| |
| confusion_values = [ |
| self.behavioral_agent.calculate_confusion_score([s]) |
| for s in signals |
| ] |
| |
| threshold = 0.7 |
| in_peak = False |
| peak_start = 0 |
| |
| for i, val in enumerate(confusion_values): |
| if val > threshold and not in_peak: |
| in_peak = True |
| peak_start = i |
| elif val < threshold and in_peak: |
| in_peak = False |
| peaks.append({ |
| 'start_index': peak_start, |
| 'end_index': i, |
| 'max_value': max(confusion_values[peak_start:i]) |
| }) |
| |
| return peaks |
| |
| async def _sync_to_notion(self, session: LearningSession): |
| """Sync session data to Notion""" |
| pass |
| |
| async def _sync_to_supabase(self, session_summary: Dict): |
| """Sync session data to Supabase""" |
| pass |
| |
| def get_active_insights(self) -> Dict: |
| """Get current active insights for display""" |
| return { |
| 'predictions': [ |
| { |
| 'doubt': p.predicted_doubt, |
| 'confidence': p.confidence, |
| 'explanation': p.suggested_explanation |
| } |
| for p in self.state.active_predictions[:3] |
| ], |
| 'pending_reviews': len(self.state.pending_recalls), |
| 'peer_insights_count': len(self.state.peer_insights), |
| 'gamification': self.state.gamification_state, |
| 'session_active': self.state.current_session is not None |
| } |
|
|