namish10 commited on
Commit
a3d136a
·
verified ·
1 Parent(s): c14f8fe

Upload app/agents/recall_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app/agents/recall_agent.py +293 -0
app/agents/recall_agent.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Recall Agent - Spaced Repetition with SM-2 Algorithm
3
+
4
+ Manages spaced repetition reviews using the SM-2 algorithm:
5
+ - Calculates optimal review intervals
6
+ - Tracks mastery levels
7
+ - Schedules reviews
8
+ """
9
+
10
+ from typing import Dict, List, Any, Optional
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timedelta
13
+ import math
14
+
15
+
16
+ @dataclass
17
+ class RecallCard:
18
+ """A card for spaced repetition review"""
19
+ card_id: str
20
+ front: str
21
+ back: str
22
+ topic: str
23
+ ease_factor: float = 2.5
24
+ interval: int = 1
25
+ repetitions: int = 0
26
+ next_review: datetime = field(default_factory=datetime.now)
27
+ last_review: Optional[datetime] = None
28
+ quality_history: List[int] = field(default_factory=list)
29
+
30
+
31
+ @dataclass
32
+ class ReviewResult:
33
+ """Result of a review session"""
34
+ card_id: str
35
+ quality: int
36
+ new_ease_factor: float
37
+ new_interval: int
38
+ new_repetitions: int
39
+ next_review: datetime
40
+ xp_earned: int
41
+
42
+
43
+ class RecallAgent:
44
+ """
45
+ Spaced repetition agent using SM-2 algorithm.
46
+
47
+ SM-2 Algorithm:
48
+ - Quality: 0-5 (0-2 = fail, 3 = hard, 4 = good, 5 = easy)
49
+ - Ease Factor: starts at 2.5, adjusted after each review
50
+ - Interval: starts at 1 day, multiplied by EF each success
51
+ """
52
+
53
+ def __init__(self, user_id: str, config: Optional[Dict] = None):
54
+ self.user_id = user_id
55
+ self.config = config or {}
56
+
57
+ self.cards: Dict[str, RecallCard] = {}
58
+ self.review_history: List[Dict] = []
59
+
60
+ def create_card(
61
+ self,
62
+ front: str,
63
+ back: str,
64
+ topic: str,
65
+ metadata: Optional[Dict] = None
66
+ ) -> RecallCard:
67
+ """Create a new recall card from a doubt"""
68
+ card_id = f"card_{datetime.now().timestamp()}"
69
+
70
+ card = RecallCard(
71
+ card_id=card_id,
72
+ front=front,
73
+ back=back,
74
+ topic=topic
75
+ )
76
+
77
+ self.cards[card_id] = card
78
+ return card
79
+
80
+ def create_cards_from_doubt(self, doubt_data: Dict) -> List[RecallCard]:
81
+ """Create recall cards from a captured doubt"""
82
+ cards = []
83
+
84
+ front = doubt_data.get('formattedTitle', doubt_data.get('rawText', ''))
85
+ back = doubt_data.get('formattedSummary', '')
86
+ topic = doubt_data.get('topic', 'General')
87
+
88
+ card = self.create_card(front, back, topic, doubt_data)
89
+ cards.append(card)
90
+
91
+ if doubt_data.get('conceptTags'):
92
+ for tag in doubt_data['conceptTags'][:2]:
93
+ tag_card = self.create_card(
94
+ f"How does '{tag}' relate to '{topic}'?",
95
+ f"{tag} is a key concept within {topic}...",
96
+ topic
97
+ )
98
+ cards.append(tag_card)
99
+
100
+ return cards
101
+
102
+ async def get_due_recalls(self, topic: Optional[str] = None) -> List[RecallCard]:
103
+ """Get cards due for review"""
104
+ now = datetime.now()
105
+ due_cards = []
106
+
107
+ for card in self.cards.values():
108
+ if topic and card.topic != topic:
109
+ continue
110
+
111
+ if card.next_review <= now:
112
+ due_cards.append(card)
113
+
114
+ due_cards.sort(key=lambda c: c.next_review)
115
+ return due_cards
116
+
117
+ async def complete_review(
118
+ self,
119
+ card_id: str,
120
+ quality: int
121
+ ) -> Optional[ReviewResult]:
122
+ """
123
+ Complete a review using SM-2 algorithm.
124
+
125
+ Quality ratings:
126
+ 0 - Complete blackout
127
+ 1 - Incorrect, but recognized answer
128
+ 2 - Incorrect, but easy to recall
129
+ 3 - Correct with difficulty
130
+ 4 - Correct after hesitation
131
+ 5 - Perfect recall
132
+ """
133
+ if card_id not in self.cards:
134
+ return None
135
+
136
+ card = self.cards[card_id]
137
+
138
+ card.last_review = datetime.now()
139
+ card.quality_history.append(quality)
140
+
141
+ if quality < 3:
142
+ card.repetitions = 0
143
+ card.interval = 1
144
+ else:
145
+ if card.repetitions == 0:
146
+ card.interval = 1
147
+ elif card.repetitions == 1:
148
+ card.interval = 6
149
+ else:
150
+ card.interval = round(card.interval * card.ease_factor)
151
+
152
+ card.repetitions += 1
153
+
154
+ card.ease_factor = max(
155
+ 1.3,
156
+ card.ease_factor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02))
157
+ )
158
+
159
+ card.next_review = datetime.now() + timedelta(days=card.interval)
160
+
161
+ xp_earned = self._calculate_xp(quality)
162
+
163
+ result = ReviewResult(
164
+ card_id=card_id,
165
+ quality=quality,
166
+ new_ease_factor=card.ease_factor,
167
+ new_interval=card.interval,
168
+ new_repetitions=card.repetitions,
169
+ next_review=card.next_review,
170
+ xp_earned=xp_earned
171
+ )
172
+
173
+ self.review_history.append({
174
+ 'card_id': card_id,
175
+ 'quality': quality,
176
+ 'timestamp': datetime.now().isoformat(),
177
+ 'xp_earned': xp_earned
178
+ })
179
+
180
+ return result
181
+
182
+ def _calculate_xp(self, quality: int) -> int:
183
+ """Calculate XP earned for review quality"""
184
+ xp_map = {0: 1, 1: 2, 2: 3, 3: 5, 4: 10, 5: 15}
185
+ return xp_map.get(quality, 1)
186
+
187
+ def get_review_stats(self) -> Dict:
188
+ """Get review statistics"""
189
+ now = datetime.now()
190
+
191
+ total_cards = len(self.cards)
192
+ due_cards = sum(1 for c in self.cards.values() if c.next_review <= now)
193
+ mastered_cards = sum(1 for c in self.cards.values()
194
+ if c.ease_factor > 2.5 and c.repetitions >= 5)
195
+
196
+ total_reviews = len(self.review_history)
197
+ avg_quality = 0
198
+ if self.review_history:
199
+ avg_quality = sum(r['quality'] for r in self.review_history) / total_reviews
200
+
201
+ return {
202
+ 'total_cards': total_cards,
203
+ 'due_today': due_cards,
204
+ 'mastered': mastered_cards,
205
+ 'total_reviews': total_reviews,
206
+ 'average_quality': round(avg_quality, 2),
207
+ 'total_xp': sum(r['xp_earned'] for r in self.review_history)
208
+ }
209
+
210
+ def get_learning_progress(self) -> Dict:
211
+ """Get learning progress for dashboard"""
212
+ stats = self.get_review_stats()
213
+
214
+ progress = {
215
+ 'cards_created': stats['total_cards'],
216
+ 'cards_mastered': stats['mastered'],
217
+ 'mastery_percentage': (
218
+ stats['mastered'] / stats['total_cards'] * 100
219
+ if stats['total_cards'] > 0 else 0
220
+ ),
221
+ 'reviews_completed': stats['total_reviews'],
222
+ 'due_today': stats['due_today'],
223
+ 'topic_breakdown': self._get_topic_breakdown()
224
+ }
225
+
226
+ return progress
227
+
228
+ def _get_topic_breakdown(self) -> Dict[str, Dict]:
229
+ """Get breakdown by topic"""
230
+ topics = {}
231
+
232
+ for card in self.cards.values():
233
+ if card.topic not in topics:
234
+ topics[card.topic] = {
235
+ 'total': 0,
236
+ 'due': 0,
237
+ 'mastered': 0,
238
+ 'avg_ease': 0
239
+ }
240
+
241
+ topics[card.topic]['total'] += 1
242
+
243
+ if card.next_review <= datetime.now():
244
+ topics[card.topic]['due'] += 1
245
+
246
+ if card.ease_factor > 2.5 and card.repetitions >= 5:
247
+ topics[card.topic]['mastered'] += 1
248
+
249
+ topics[card.topic]['avg_ease'] += card.ease_factor
250
+
251
+ for topic in topics:
252
+ if topics[topic]['total'] > 0:
253
+ topics[topic]['avg_ease'] /= topics[topic]['total']
254
+
255
+ return topics
256
+
257
+ def export_cards(self) -> List[Dict]:
258
+ """Export all cards for backup"""
259
+ return [
260
+ {
261
+ 'card_id': c.card_id,
262
+ 'front': c.front,
263
+ 'back': c.back,
264
+ 'topic': c.topic,
265
+ 'ease_factor': c.ease_factor,
266
+ 'interval': c.interval,
267
+ 'repetitions': c.repetitions,
268
+ 'next_review': c.next_review.isoformat(),
269
+ 'last_review': c.last_review.isoformat() if c.last_review else None,
270
+ 'quality_history': c.quality_history
271
+ }
272
+ for c in self.cards.values()
273
+ ]
274
+
275
+ def import_cards(self, cards_data: List[Dict]):
276
+ """Import cards from backup"""
277
+ for card_data in cards_data:
278
+ card = RecallCard(
279
+ card_id=card_data['card_id'],
280
+ front=card_data['front'],
281
+ back=card_data['back'],
282
+ topic=card_data['topic'],
283
+ ease_factor=card_data.get('ease_factor', 2.5),
284
+ interval=card_data.get('interval', 1),
285
+ repetitions=card_data.get('repetitions', 0),
286
+ next_review=datetime.fromisoformat(card_data['next_review']),
287
+ last_review=(
288
+ datetime.fromisoformat(card_data['last_review'])
289
+ if card_data.get('last_review') else None
290
+ ),
291
+ quality_history=card_data.get('quality_history', [])
292
+ )
293
+ self.cards[card.card_id] = card