File size: 12,695 Bytes
cd8c2bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import json
import numpy as np
from typing import Dict, List, Any, Optional
from datetime import datetime
from .knowledge_tracing import KnowledgeTracer, ItemResponse, SkillMastery
from .rag.knowledge_base import KnowledgeBase
from .rag.retriever import KnowledgeRetriever
from .rag.rag_prompts import RAGEnhancedPrompts
from .inference import run_prompt

class AdaptiveTutor:
    """RAG-enhanced adaptive tutoring system with knowledge tracing."""
    
    def __init__(self, user_id: str = "default"):
        self.user_id = user_id
        self.knowledge_tracer = KnowledgeTracer()
        self.knowledge_base = KnowledgeBase()
        self.retriever = KnowledgeRetriever(self.knowledge_base)
        self.rag_prompts = RAGEnhancedPrompts()
        
        # Session tracking
        self.session_start = datetime.now()
        self.session_responses = []
    
    def process_student_response(self, item_id: str, skill: str, question: str,
                                user_answer: str, correct_answer: str,
                                response_time: float, hints_used: int = 0) -> Dict[str, Any]:
        """Process a student response and update knowledge tracing."""
        
        # Determine correctness
        is_correct = self._evaluate_answer(user_answer, correct_answer)
        
        # Create item response
        difficulty = self._estimate_item_difficulty(skill, question)
        response = ItemResponse(
            item_id=item_id,
            skill=skill,
            correct=is_correct,
            response_time=response_time,
            hints_used=hints_used,
            difficulty=difficulty,
            timestamp=datetime.now()
        )
        
        # Update knowledge tracing
        new_theta = self.knowledge_tracer.update_mastery(response)
        mastery_prob = self.knowledge_tracer.get_mastery_probability(skill)
        
        # Generate RAG-enhanced explanation
        explanation = self.generate_rag_explanation(question, user_answer, correct_answer)
        
        # Track session
        self.session_responses.append(response)
        
        return {
            "correct": is_correct,
            "mastery_theta": new_theta,
            "mastery_probability": mastery_prob,
            "explanation": explanation,
            "next_recommendations": self.get_next_items(skill)
        }
    
    def generate_rag_explanation(self, question: str, user_answer: str, 
                                correct_answer: str) -> Dict[str, Any]:
        """Generate explanation with knowledge grounding."""
        
        # Retrieve relevant knowledge
        knowledge_context = self.retriever.get_explanation_with_citations(
            question, user_answer, correct_answer
        )
        
        # Prepare input for RAG-enhanced prompt
        prompt_input = {
            "question": question,
            "user_answer": user_answer,
            "solution": correct_answer,
            "facts": knowledge_context["facts"],
            "sources": knowledge_context["sources"]
        }
        
        # Generate explanation using RAG prompt
        try:
            explanation = run_prompt(
                "item_explanation_with_rag",
                prompt_input,
                model_id="Qwen/Qwen3-7B-Instruct"
            )
            
            # Add citations from knowledge base
            explanation["knowledge_citations"] = knowledge_context["citations"]
            explanation["fact_sources"] = knowledge_context["facts"]
            
        except Exception as e:
            # Fallback to basic explanation
            explanation = {
                "hint": "Review the problem steps carefully.",
                "guided": "Compare your answer with the correct solution.",
                "full": f"The correct answer is {correct_answer}. Please review the method.",
                "knowledge_citations": [],
                "fact_sources": []
            }
        
        return explanation
    
    def generate_adaptive_hints(self, question: str, hint_level: int = 1) -> List[str]:
        """Generate contextual hints using RAG."""
        return self.retriever.get_contextual_hints(question, hint_level)
    
    def generate_adaptive_question(self, skill: str, difficulty: Optional[float] = None) -> Dict[str, Any]:
        """Generate an adaptive question based on current mastery."""
        
        if difficulty is None:
            mastery = self.knowledge_tracer.get_mastery_probability(skill)
            difficulty = 1.0 - mastery  # Inverse relationship
        
        # Retrieve relevant knowledge for the skill
        knowledge_items = self.knowledge_base.retrieve_by_skill(skill, limit=3)
        
        # Prepare input for question generation
        prompt_input = {
            "skill": skill,
            "mastery_level": 1.0 - difficulty,
            "knowledge_content": [item["content"] for item in knowledge_items],
            "difficulty": difficulty
        }
        
        try:
            question = run_prompt(
                "adaptive_question_generation",
                prompt_input,
                model_id="Qwen/Qwen3-7B-Instruct"
            )
            
            # Add knowledge citations
            question["knowledge_sources"] = [item["id"] for item in knowledge_items]
            
        except Exception as e:
            # Fallback question template
            question = {
                "question": f"Practice problem for {skill} at difficulty {difficulty:.2f}",
                "answer": "Answer to be determined",
                "explanation": "Explanation to be provided",
                "difficulty": difficulty,
                "skill": skill,
                "knowledge_sources": []
            }
        
        return question
    
    def get_next_items(self, current_skill: str = None, max_items: int = 5) -> List[Dict[str, Any]]:
        """Get next item recommendations using entropy-based scheduling."""
        
        # Generate candidate items
        candidates = []
        skills = ["algebra_simplification", "linear_equations", "fraction_operations", "ratios"]
        
        for skill in skills:
            for i in range(3):  # 3 items per skill
                mastery = self.knowledge_tracer.get_mastery_probability(skill)
                difficulty = 1.0 - mastery + np.random.normal(0, 0.1)  # Add noise
                
                candidates.append({
                    "item_id": f"{skill}_{i}",
                    "skill": skill,
                    "difficulty": np.clip(difficulty, 0.1, 0.9),
                    "type": "practice"
                })
        
        # Get recommendations from knowledge tracer
        recommendations = self.knowledge_tracer.get_next_item_recommendations(
            candidates, max_items
        )
        
        # Add adaptive questions for top recommendations
        for rec in recommendations:
            if rec["type"] == "practice":
                adaptive_q = self.generate_adaptive_question(rec["skill"], rec["difficulty"])
                rec.update(adaptive_q)
        
        return recommendations
    
    def evaluate_mastery_with_irt(self, skill: str) -> Dict[str, Any]:
        """Evaluate mastery using IRT parameters."""
        
        # Get recent responses for the skill
        skill_responses = [r for r in self.session_responses if r.skill == skill]
        
        if not skill_responses:
            # Get from database if no session responses
            mastery_prob = self.knowledge_tracer.get_mastery_probability(skill)
            return {
                "theta": 0.0,
                "sem": 1.0,
                "mastery": mastery_prob,
                "confidence_interval": [-1.96, 1.96]
            }
        
        # Prepare input for IRT evaluation
        responses_data = []
        for r in skill_responses[-10:]:  # Last 10 responses
            responses_data.append({
                "correct": r.correct,
                "difficulty": r.difficulty,
                "response_time": r.response_time,
                "hints": r.hints_used
            })
        
        prompt_input = {
            "skill": skill,
            "responses": responses_data
        }
        
        try:
            irt_result = run_prompt(
                "mastery_diagnostic_with_irt",
                prompt_input,
                model_id="Qwen/Qwen3-7B-Instruct"
            )
            return irt_result
        except:
            # Fallback to knowledge tracer estimates
            if skill in self.knowledge_tracer.skill_masteries:
                mastery = self.knowledge_tracer.skill_masteries[skill]
                return {
                    "theta": mastery.theta,
                    "sem": mastery.sem,
                    "mastery": self.knowledge_tracer.get_mastery_probability(skill),
                    "confidence_interval": [
                        mastery.theta - 1.96 * mastery.sem,
                        mastery.theta + 1.96 * mastery.sem
                    ]
                }
            else:
                return {
                    "theta": 0.0,
                    "sem": 1.0,
                    "mastery": 0.5,
                    "confidence_interval": [-1.96, 1.96]
                }
    
    def get_research_metrics(self) -> Dict[str, Any]:
        """Get comprehensive research metrics for evaluation."""
        
        # Basic session metrics
        session_duration = (datetime.now() - self.session_start).total_seconds()
        total_responses = len(self.session_responses)
        correct_responses = sum(1 for r in self.session_responses if r.correct)
        
        # Get detailed metrics from knowledge tracer
        tracer_metrics = self.knowledge_tracer.get_research_metrics()
        
        # Calculate additional session-based metrics
        if total_responses > 0:
            session_accuracy = correct_responses / total_responses
            avg_session_time = np.mean([r.response_time for r in self.session_responses])
            hints_per_response = np.mean([r.hints_used for r in self.session_responses])
        else:
            session_accuracy = 0.0
            avg_session_time = 0.0
            hints_per_response = 0.0
        
        # Learning gain calculation
        if len(self.session_responses) >= 10:
            early_accuracy = sum(1 for r in self.session_responses[:5] if r.correct) / 5
            late_accuracy = sum(1 for r in self.session_responses[-5:] if r.correct) / 5
            session_learning_gain = late_accuracy - early_accuracy
        else:
            session_learning_gain = 0.0
        
        # Combine all metrics
        research_metrics = {
            "session_metrics": {
                "duration_seconds": session_duration,
                "total_responses": total_responses,
                "accuracy": session_accuracy,
                "avg_response_time": avg_session_time,
                "hints_per_response": hints_per_response,
                "learning_gain": session_learning_gain
            },
            "cumulative_metrics": tracer_metrics,
            "knowledge_tracing": {
                "tracked_skills": len(self.knowledge_tracer.skill_masteries),
                "skill_masteries": {
                    skill: {
                        "theta": mastery.theta,
                        "mastery_prob": self.knowledge_tracer.get_mastery_probability(skill),
                        "practice_count": mastery.practice_count
                    }
                    for skill, mastery in self.knowledge_tracer.skill_masteries.items()
                }
            }
        }
        
        return research_metrics
    
    def _evaluate_answer(self, user_answer: str, correct_answer: str) -> bool:
        """Evaluate if user answer is correct."""
        # Simple string comparison - can be enhanced with semantic matching
        return user_answer.strip().lower() == correct_answer.strip().lower()
    
    def _estimate_item_difficulty(self, skill: str, question: str) -> float:
        """Estimate item difficulty based on skill and question complexity."""
        # Base difficulty on skill type
        skill_difficulties = {
            "algebra_simplification": 0.3,
            "linear_equations": 0.5,
            "fraction_operations": 0.6,
            "ratios": 0.5
        }
        
        base_difficulty = skill_difficulties.get(skill, 0.5)
        
        # Adjust based on question length (proxy for complexity)
        length_factor = min(len(question) / 100.0, 0.3)
        
        return np.clip(base_difficulty + length_factor, 0.1, 0.9)