BonelliLab commited on
Commit
cd8c2bb
·
1 Parent(s): f6c853c

Push existing cognitive tutor project

Browse files
Files changed (37) hide show
  1. README.md +79 -12
  2. __pycache__/cognitive_llm.cpython-313.pyc +0 -0
  3. cog_tutor/__init__.py +3 -0
  4. cog_tutor/__pycache__/__init__.cpython-313.pyc +0 -0
  5. cog_tutor/__pycache__/adaptive_tutor.cpython-313.pyc +0 -0
  6. cog_tutor/__pycache__/cache.cpython-313.pyc +0 -0
  7. cog_tutor/__pycache__/inference.cpython-313.pyc +0 -0
  8. cog_tutor/__pycache__/knowledge_tracing.cpython-313.pyc +0 -0
  9. cog_tutor/__pycache__/prompts.cpython-313.pyc +0 -0
  10. cog_tutor/__pycache__/schemas.cpython-313.pyc +0 -0
  11. cog_tutor/__pycache__/validation.cpython-313.pyc +0 -0
  12. cog_tutor/adapters/__init__.py +2 -0
  13. cog_tutor/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
  14. cog_tutor/adapters/__pycache__/qwen_adapter.cpython-313.pyc +0 -0
  15. cog_tutor/adapters/qwen_adapter.py +46 -0
  16. cog_tutor/adaptive_tutor.py +316 -0
  17. cog_tutor/cache.py +25 -0
  18. cog_tutor/inference.py +144 -0
  19. cog_tutor/knowledge_tracing.py +311 -0
  20. cog_tutor/prompts.py +60 -0
  21. cog_tutor/rag/__init__.py +5 -0
  22. cog_tutor/rag/__pycache__/__init__.cpython-313.pyc +0 -0
  23. cog_tutor/rag/__pycache__/knowledge_base.cpython-313.pyc +0 -0
  24. cog_tutor/rag/__pycache__/rag_prompts.cpython-313.pyc +0 -0
  25. cog_tutor/rag/__pycache__/retriever.cpython-313.pyc +0 -0
  26. cog_tutor/rag/knowledge_base.py +173 -0
  27. cog_tutor/rag/rag_prompts.py +88 -0
  28. cog_tutor/rag/retriever.py +118 -0
  29. cog_tutor/schemas.py +107 -0
  30. cog_tutor/validation.py +10 -0
  31. cognitive_llm.py +117 -0
  32. knowledge_base.sqlite +0 -0
  33. knowledge_tracing.sqlite +0 -0
  34. requirements.txt +6 -0
  35. research_output.json +89 -0
  36. test_cog_tutor.py +11 -0
  37. test_rag_tutor.py +191 -0
README.md CHANGED
@@ -1,12 +1,79 @@
1
- ---
2
- title: Eidolon CognitiveTutor
3
- emoji: 🌖
4
- colorFrom: red
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: RAG-enhanced cognitive tutor demo built with Qwen and FastAP
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cognitive LLM with Qwen3
2
+
3
+ A simple implementation of a cognitive language model using Qwen3-7B-Instruct from Hugging Face.
4
+
5
+ ## Features
6
+
7
+ - Easy-to-use Python interface for Qwen3-7B-Instruct
8
+ - Optimized for both CUDA and CPU
9
+ - 4-bit quantization for reduced memory usage
10
+ - Interactive command-line interface
11
+ - Configurable generation parameters
12
+
13
+ ## Prerequisites
14
+
15
+ - Python 3.8 or higher
16
+ - PyTorch (will be installed via requirements.txt)
17
+ - CUDA-compatible GPU (recommended) or CPU
18
+
19
+ ## Installation
20
+
21
+ 1. Clone this repository
22
+ 2. Install the required packages:
23
+ ```bash
24
+ pip install -r requirements.txt
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ 1. Run the interactive CLI:
30
+ ```bash
31
+ python cognitive_llm.py
32
+ ```
33
+
34
+ 2. Enter your prompt when prompted with `>>` and press Enter
35
+ 3. Type 'quit' or 'exit' to exit the program
36
+
37
+ ### Example Usage
38
+
39
+ ```python
40
+ from cognitive_llm import CognitiveLLM
41
+
42
+ # Initialize the LLM
43
+ llm = CognitiveLLM()
44
+
45
+ # Generate text
46
+ response = llm.generate(
47
+ "Explain quantum computing in simple terms.",
48
+ max_new_tokens=256,
49
+ temperature=0.7
50
+ )
51
+ print(response)
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ You can customize the model and generation parameters:
57
+
58
+ ```python
59
+ llm = CognitiveLLM(
60
+ model_name="Qwen/Qwen3-7B-Instruct", # Model name or path
61
+ device="cuda" # 'cuda', 'mps', or 'cpu'
62
+ )
63
+
64
+ # Generate with custom parameters
65
+ response = llm.generate(
66
+ "Your prompt here",
67
+ max_new_tokens=512,
68
+ temperature=0.7,
69
+ top_p=0.9,
70
+ do_sample=True
71
+ )
72
+ ```
73
+
74
+ ## Note
75
+
76
+ - First run will download the model weights (several GB)
77
+ - A CUDA-compatible GPU is recommended for reasonable performance
78
+ - Ensure you have sufficient disk space for the model weights
79
+ - Internet connection is required for the initial download
__pycache__/cognitive_llm.cpython-313.pyc ADDED
Binary file (4.35 kB). View file
 
cog_tutor/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .inference import run_prompt
2
+ __all__=['run_prompt']
3
+
cog_tutor/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (242 Bytes). View file
 
cog_tutor/__pycache__/adaptive_tutor.cpython-313.pyc ADDED
Binary file (12.9 kB). View file
 
cog_tutor/__pycache__/cache.cpython-313.pyc ADDED
Binary file (2.11 kB). View file
 
cog_tutor/__pycache__/inference.cpython-313.pyc ADDED
Binary file (5.51 kB). View file
 
cog_tutor/__pycache__/knowledge_tracing.cpython-313.pyc ADDED
Binary file (14.6 kB). View file
 
cog_tutor/__pycache__/prompts.cpython-313.pyc ADDED
Binary file (3.45 kB). View file
 
cog_tutor/__pycache__/schemas.cpython-313.pyc ADDED
Binary file (7.26 kB). View file
 
cog_tutor/__pycache__/validation.cpython-313.pyc ADDED
Binary file (735 Bytes). View file
 
cog_tutor/adapters/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .qwen_adapter import QwenAdapter
2
+ __all__ = ["QwenAdapter"]
cog_tutor/adapters/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (255 Bytes). View file
 
cog_tutor/adapters/__pycache__/qwen_adapter.cpython-313.pyc ADDED
Binary file (2.1 kB). View file
 
cog_tutor/adapters/qwen_adapter.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List
2
+ from cognitive_llm import CognitiveLLM
3
+
4
+ class QwenAdapter:
5
+ def __init__(self, model_name: str = "Qwen/Qwen3-7B-Instruct"):
6
+ # Store model name for lazy initialization
7
+ self.model_name = model_name
8
+ self.client = None
9
+
10
+ def _initialize_client(self):
11
+ # Lazy initialization of the CognitiveLLM client
12
+ if self.client is None:
13
+ self.client = CognitiveLLM(model_name=self.model_name)
14
+
15
+ def generate(
16
+ self,
17
+ system: str,
18
+ user: str,
19
+ *,
20
+ temperature: float = 0.0,
21
+ max_tokens: int = 512,
22
+ stop: Optional[List[str]] = None,
23
+ seed: Optional[int] = None,
24
+ ) -> str:
25
+ # Initialize client if not already done
26
+ self._initialize_client()
27
+
28
+ # Compose a strict prompt: JSON only, no commentary
29
+ prompt = f"System: {system}\nReturn JSON only. No commentary.\nInput: {user}"
30
+
31
+ # Use the existing generate method with appropriate parameters
32
+ text = self.client.generate(
33
+ prompt,
34
+ max_new_tokens=max_tokens,
35
+ temperature=max(0.1, temperature),
36
+ top_p=0.9,
37
+ do_sample=temperature > 0.3
38
+ )
39
+
40
+ if stop:
41
+ for s in stop:
42
+ i = text.find(s)
43
+ if i != -1:
44
+ text = text[:i]
45
+ break
46
+ return text.strip()
cog_tutor/adaptive_tutor.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import numpy as np
3
+ from typing import Dict, List, Any, Optional
4
+ from datetime import datetime
5
+ from .knowledge_tracing import KnowledgeTracer, ItemResponse, SkillMastery
6
+ from .rag.knowledge_base import KnowledgeBase
7
+ from .rag.retriever import KnowledgeRetriever
8
+ from .rag.rag_prompts import RAGEnhancedPrompts
9
+ from .inference import run_prompt
10
+
11
+ class AdaptiveTutor:
12
+ """RAG-enhanced adaptive tutoring system with knowledge tracing."""
13
+
14
+ def __init__(self, user_id: str = "default"):
15
+ self.user_id = user_id
16
+ self.knowledge_tracer = KnowledgeTracer()
17
+ self.knowledge_base = KnowledgeBase()
18
+ self.retriever = KnowledgeRetriever(self.knowledge_base)
19
+ self.rag_prompts = RAGEnhancedPrompts()
20
+
21
+ # Session tracking
22
+ self.session_start = datetime.now()
23
+ self.session_responses = []
24
+
25
+ def process_student_response(self, item_id: str, skill: str, question: str,
26
+ user_answer: str, correct_answer: str,
27
+ response_time: float, hints_used: int = 0) -> Dict[str, Any]:
28
+ """Process a student response and update knowledge tracing."""
29
+
30
+ # Determine correctness
31
+ is_correct = self._evaluate_answer(user_answer, correct_answer)
32
+
33
+ # Create item response
34
+ difficulty = self._estimate_item_difficulty(skill, question)
35
+ response = ItemResponse(
36
+ item_id=item_id,
37
+ skill=skill,
38
+ correct=is_correct,
39
+ response_time=response_time,
40
+ hints_used=hints_used,
41
+ difficulty=difficulty,
42
+ timestamp=datetime.now()
43
+ )
44
+
45
+ # Update knowledge tracing
46
+ new_theta = self.knowledge_tracer.update_mastery(response)
47
+ mastery_prob = self.knowledge_tracer.get_mastery_probability(skill)
48
+
49
+ # Generate RAG-enhanced explanation
50
+ explanation = self.generate_rag_explanation(question, user_answer, correct_answer)
51
+
52
+ # Track session
53
+ self.session_responses.append(response)
54
+
55
+ return {
56
+ "correct": is_correct,
57
+ "mastery_theta": new_theta,
58
+ "mastery_probability": mastery_prob,
59
+ "explanation": explanation,
60
+ "next_recommendations": self.get_next_items(skill)
61
+ }
62
+
63
+ def generate_rag_explanation(self, question: str, user_answer: str,
64
+ correct_answer: str) -> Dict[str, Any]:
65
+ """Generate explanation with knowledge grounding."""
66
+
67
+ # Retrieve relevant knowledge
68
+ knowledge_context = self.retriever.get_explanation_with_citations(
69
+ question, user_answer, correct_answer
70
+ )
71
+
72
+ # Prepare input for RAG-enhanced prompt
73
+ prompt_input = {
74
+ "question": question,
75
+ "user_answer": user_answer,
76
+ "solution": correct_answer,
77
+ "facts": knowledge_context["facts"],
78
+ "sources": knowledge_context["sources"]
79
+ }
80
+
81
+ # Generate explanation using RAG prompt
82
+ try:
83
+ explanation = run_prompt(
84
+ "item_explanation_with_rag",
85
+ prompt_input,
86
+ model_id="Qwen/Qwen3-7B-Instruct"
87
+ )
88
+
89
+ # Add citations from knowledge base
90
+ explanation["knowledge_citations"] = knowledge_context["citations"]
91
+ explanation["fact_sources"] = knowledge_context["facts"]
92
+
93
+ except Exception as e:
94
+ # Fallback to basic explanation
95
+ explanation = {
96
+ "hint": "Review the problem steps carefully.",
97
+ "guided": "Compare your answer with the correct solution.",
98
+ "full": f"The correct answer is {correct_answer}. Please review the method.",
99
+ "knowledge_citations": [],
100
+ "fact_sources": []
101
+ }
102
+
103
+ return explanation
104
+
105
+ def generate_adaptive_hints(self, question: str, hint_level: int = 1) -> List[str]:
106
+ """Generate contextual hints using RAG."""
107
+ return self.retriever.get_contextual_hints(question, hint_level)
108
+
109
+ def generate_adaptive_question(self, skill: str, difficulty: Optional[float] = None) -> Dict[str, Any]:
110
+ """Generate an adaptive question based on current mastery."""
111
+
112
+ if difficulty is None:
113
+ mastery = self.knowledge_tracer.get_mastery_probability(skill)
114
+ difficulty = 1.0 - mastery # Inverse relationship
115
+
116
+ # Retrieve relevant knowledge for the skill
117
+ knowledge_items = self.knowledge_base.retrieve_by_skill(skill, limit=3)
118
+
119
+ # Prepare input for question generation
120
+ prompt_input = {
121
+ "skill": skill,
122
+ "mastery_level": 1.0 - difficulty,
123
+ "knowledge_content": [item["content"] for item in knowledge_items],
124
+ "difficulty": difficulty
125
+ }
126
+
127
+ try:
128
+ question = run_prompt(
129
+ "adaptive_question_generation",
130
+ prompt_input,
131
+ model_id="Qwen/Qwen3-7B-Instruct"
132
+ )
133
+
134
+ # Add knowledge citations
135
+ question["knowledge_sources"] = [item["id"] for item in knowledge_items]
136
+
137
+ except Exception as e:
138
+ # Fallback question template
139
+ question = {
140
+ "question": f"Practice problem for {skill} at difficulty {difficulty:.2f}",
141
+ "answer": "Answer to be determined",
142
+ "explanation": "Explanation to be provided",
143
+ "difficulty": difficulty,
144
+ "skill": skill,
145
+ "knowledge_sources": []
146
+ }
147
+
148
+ return question
149
+
150
+ def get_next_items(self, current_skill: str = None, max_items: int = 5) -> List[Dict[str, Any]]:
151
+ """Get next item recommendations using entropy-based scheduling."""
152
+
153
+ # Generate candidate items
154
+ candidates = []
155
+ skills = ["algebra_simplification", "linear_equations", "fraction_operations", "ratios"]
156
+
157
+ for skill in skills:
158
+ for i in range(3): # 3 items per skill
159
+ mastery = self.knowledge_tracer.get_mastery_probability(skill)
160
+ difficulty = 1.0 - mastery + np.random.normal(0, 0.1) # Add noise
161
+
162
+ candidates.append({
163
+ "item_id": f"{skill}_{i}",
164
+ "skill": skill,
165
+ "difficulty": np.clip(difficulty, 0.1, 0.9),
166
+ "type": "practice"
167
+ })
168
+
169
+ # Get recommendations from knowledge tracer
170
+ recommendations = self.knowledge_tracer.get_next_item_recommendations(
171
+ candidates, max_items
172
+ )
173
+
174
+ # Add adaptive questions for top recommendations
175
+ for rec in recommendations:
176
+ if rec["type"] == "practice":
177
+ adaptive_q = self.generate_adaptive_question(rec["skill"], rec["difficulty"])
178
+ rec.update(adaptive_q)
179
+
180
+ return recommendations
181
+
182
+ def evaluate_mastery_with_irt(self, skill: str) -> Dict[str, Any]:
183
+ """Evaluate mastery using IRT parameters."""
184
+
185
+ # Get recent responses for the skill
186
+ skill_responses = [r for r in self.session_responses if r.skill == skill]
187
+
188
+ if not skill_responses:
189
+ # Get from database if no session responses
190
+ mastery_prob = self.knowledge_tracer.get_mastery_probability(skill)
191
+ return {
192
+ "theta": 0.0,
193
+ "sem": 1.0,
194
+ "mastery": mastery_prob,
195
+ "confidence_interval": [-1.96, 1.96]
196
+ }
197
+
198
+ # Prepare input for IRT evaluation
199
+ responses_data = []
200
+ for r in skill_responses[-10:]: # Last 10 responses
201
+ responses_data.append({
202
+ "correct": r.correct,
203
+ "difficulty": r.difficulty,
204
+ "response_time": r.response_time,
205
+ "hints": r.hints_used
206
+ })
207
+
208
+ prompt_input = {
209
+ "skill": skill,
210
+ "responses": responses_data
211
+ }
212
+
213
+ try:
214
+ irt_result = run_prompt(
215
+ "mastery_diagnostic_with_irt",
216
+ prompt_input,
217
+ model_id="Qwen/Qwen3-7B-Instruct"
218
+ )
219
+ return irt_result
220
+ except:
221
+ # Fallback to knowledge tracer estimates
222
+ if skill in self.knowledge_tracer.skill_masteries:
223
+ mastery = self.knowledge_tracer.skill_masteries[skill]
224
+ return {
225
+ "theta": mastery.theta,
226
+ "sem": mastery.sem,
227
+ "mastery": self.knowledge_tracer.get_mastery_probability(skill),
228
+ "confidence_interval": [
229
+ mastery.theta - 1.96 * mastery.sem,
230
+ mastery.theta + 1.96 * mastery.sem
231
+ ]
232
+ }
233
+ else:
234
+ return {
235
+ "theta": 0.0,
236
+ "sem": 1.0,
237
+ "mastery": 0.5,
238
+ "confidence_interval": [-1.96, 1.96]
239
+ }
240
+
241
+ def get_research_metrics(self) -> Dict[str, Any]:
242
+ """Get comprehensive research metrics for evaluation."""
243
+
244
+ # Basic session metrics
245
+ session_duration = (datetime.now() - self.session_start).total_seconds()
246
+ total_responses = len(self.session_responses)
247
+ correct_responses = sum(1 for r in self.session_responses if r.correct)
248
+
249
+ # Get detailed metrics from knowledge tracer
250
+ tracer_metrics = self.knowledge_tracer.get_research_metrics()
251
+
252
+ # Calculate additional session-based metrics
253
+ if total_responses > 0:
254
+ session_accuracy = correct_responses / total_responses
255
+ avg_session_time = np.mean([r.response_time for r in self.session_responses])
256
+ hints_per_response = np.mean([r.hints_used for r in self.session_responses])
257
+ else:
258
+ session_accuracy = 0.0
259
+ avg_session_time = 0.0
260
+ hints_per_response = 0.0
261
+
262
+ # Learning gain calculation
263
+ if len(self.session_responses) >= 10:
264
+ early_accuracy = sum(1 for r in self.session_responses[:5] if r.correct) / 5
265
+ late_accuracy = sum(1 for r in self.session_responses[-5:] if r.correct) / 5
266
+ session_learning_gain = late_accuracy - early_accuracy
267
+ else:
268
+ session_learning_gain = 0.0
269
+
270
+ # Combine all metrics
271
+ research_metrics = {
272
+ "session_metrics": {
273
+ "duration_seconds": session_duration,
274
+ "total_responses": total_responses,
275
+ "accuracy": session_accuracy,
276
+ "avg_response_time": avg_session_time,
277
+ "hints_per_response": hints_per_response,
278
+ "learning_gain": session_learning_gain
279
+ },
280
+ "cumulative_metrics": tracer_metrics,
281
+ "knowledge_tracing": {
282
+ "tracked_skills": len(self.knowledge_tracer.skill_masteries),
283
+ "skill_masteries": {
284
+ skill: {
285
+ "theta": mastery.theta,
286
+ "mastery_prob": self.knowledge_tracer.get_mastery_probability(skill),
287
+ "practice_count": mastery.practice_count
288
+ }
289
+ for skill, mastery in self.knowledge_tracer.skill_masteries.items()
290
+ }
291
+ }
292
+ }
293
+
294
+ return research_metrics
295
+
296
+ def _evaluate_answer(self, user_answer: str, correct_answer: str) -> bool:
297
+ """Evaluate if user answer is correct."""
298
+ # Simple string comparison - can be enhanced with semantic matching
299
+ return user_answer.strip().lower() == correct_answer.strip().lower()
300
+
301
+ def _estimate_item_difficulty(self, skill: str, question: str) -> float:
302
+ """Estimate item difficulty based on skill and question complexity."""
303
+ # Base difficulty on skill type
304
+ skill_difficulties = {
305
+ "algebra_simplification": 0.3,
306
+ "linear_equations": 0.5,
307
+ "fraction_operations": 0.6,
308
+ "ratios": 0.5
309
+ }
310
+
311
+ base_difficulty = skill_difficulties.get(skill, 0.5)
312
+
313
+ # Adjust based on question length (proxy for complexity)
314
+ length_factor = min(len(question) / 100.0, 0.3)
315
+
316
+ return np.clip(base_difficulty + length_factor, 0.1, 0.9)
cog_tutor/cache.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import sqlite3
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ _DB = Path(__file__).with_name('cog_cache.sqlite')
7
+
8
+ def _conn():
9
+ con = sqlite3.connect(str(_DB))
10
+ con.execute('CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT)')
11
+ return con
12
+
13
+ def make_key(*parts) -> str:
14
+ raw = '\u241f'.join(str(p) for p in parts)
15
+ return hashlib.sha256(raw.encode('utf-8')).hexdigest()
16
+
17
+ def get(key: str) -> Optional[str]:
18
+ with _conn() as con:
19
+ cur = con.execute('SELECT v FROM kv WHERE k=?', (key,))
20
+ row = cur.fetchone()
21
+ return row[0] if row else None
22
+
23
+ def set(key: str, value: str) -> None:
24
+ with _conn() as con:
25
+ con.execute('REPLACE INTO kv (k, v) VALUES (?, ?)', (key, value))
cog_tutor/inference.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Dict, Any
3
+ from . import prompts
4
+ from .schemas import (
5
+ ItemExplanationInput, ItemExplanationOutput,
6
+ MasteryDiagnosticInput, MasteryDiagnosticOutput,
7
+ NextItemSelectorInput, NextItemSelectorOutput,
8
+ SkillFeedbackInput, SkillFeedbackOutput,
9
+ HintGenerationInput, HintGenerationOutput,
10
+ ReflectionInput, ReflectionOutput,
11
+ InstructorInsightInput, InstructorInsightRow,
12
+ ExplanationCompressionInput, ExplanationCompressionOutput,
13
+ QuestionAuthoringInput, QuestionAuthoringOutput,
14
+ ToneNormalizerInput, ToneNormalizerOutput,
15
+ )
16
+ from .validation import parse_and_validate
17
+ from .cache import make_key, get as cache_get, set as cache_set
18
+ from .adapters.qwen_adapter import QwenAdapter
19
+
20
+ PRESETS = {
21
+ 'item_explanation': dict(temperature=0.2, max_tokens=256),
22
+ 'mastery_diagnostic': dict(temperature=0.2, max_tokens=128),
23
+ 'next_item_selector': dict(temperature=0.2, max_tokens=128),
24
+ 'skill_feedback': dict(temperature=0.3, max_tokens=256),
25
+ 'hint_generation': dict(temperature=0.6, max_tokens=200),
26
+ 'reflection': dict(temperature=0.3, max_tokens=120),
27
+ 'instructor_insight': dict(temperature=0.2, max_tokens=160),
28
+ 'explanation_compression': dict(temperature=0.2, max_tokens=80),
29
+ 'question_authoring': dict(temperature=0.6, max_tokens=400),
30
+ 'tone_normalizer': dict(temperature=0.2, max_tokens=60),
31
+ }
32
+
33
+ SYSTEMS = {
34
+ 'item_explanation': prompts.item_explanation,
35
+ 'mastery_diagnostic': prompts.mastery_diagnostic,
36
+ 'next_item_selector': prompts.next_item_selector,
37
+ 'skill_feedback': prompts.skill_feedback,
38
+ 'hint_generation': prompts.hint_generation,
39
+ 'reflection': prompts.reflection,
40
+ 'instructor_insight': prompts.instructor_insight,
41
+ 'explanation_compression': prompts.explanation_compression,
42
+ 'question_authoring': prompts.question_authoring,
43
+ 'tone_normalizer': prompts.tone_normalizer,
44
+ }
45
+
46
+ INPUT_MODELS = {
47
+ 'item_explanation': ItemExplanationInput,
48
+ 'mastery_diagnostic': MasteryDiagnosticInput,
49
+ 'next_item_selector': NextItemSelectorInput,
50
+ 'skill_feedback': SkillFeedbackInput,
51
+ 'hint_generation': HintGenerationInput,
52
+ 'reflection': ReflectionInput,
53
+ 'instructor_insight': InstructorInsightInput,
54
+ 'explanation_compression': ExplanationCompressionInput,
55
+ 'question_authoring': QuestionAuthoringInput,
56
+ 'tone_normalizer': ToneNormalizerInput,
57
+ }
58
+
59
+ OUTPUT_MODELS = {
60
+ 'item_explanation': ItemExplanationOutput,
61
+ 'mastery_diagnostic': MasteryDiagnosticOutput,
62
+ 'next_item_selector': NextItemSelectorOutput,
63
+ 'skill_feedback': SkillFeedbackOutput,
64
+ 'hint_generation': HintGenerationOutput,
65
+ 'reflection': ReflectionOutput,
66
+ 'instructor_insight': InstructorInsightRow, # list validated separately
67
+ 'explanation_compression': ExplanationCompressionOutput,
68
+ 'question_authoring': QuestionAuthoringOutput,
69
+ 'tone_normalizer': ToneNormalizerOutput,
70
+ }
71
+
72
+ _adapter = None
73
+ SPECIAL_CACHE_KEYS = {'item_explanation', 'hint_generation'}
74
+
75
+
76
+ def _get_adapter(model_id: str) -> QwenAdapter:
77
+ global _adapter
78
+ if _adapter is None:
79
+ _adapter = QwenAdapter(model_name=model_id)
80
+ return _adapter
81
+
82
+ def _cache_key(prompt_name: str, input_data: Dict[str, Any], model_id: str, temperature: float) -> str:
83
+ special = None
84
+ if prompt_name in SPECIAL_CACHE_KEYS:
85
+ if prompt_name == 'item_explanation':
86
+ q = input_data.get('question', '')
87
+ ua = input_data.get('user_answer', '')
88
+ special = f"{q}\u241f{ua}"
89
+ elif prompt_name == 'hint_generation':
90
+ q = input_data.get('question', '')
91
+ special = q
92
+ base = json.dumps(input_data, sort_keys=True)
93
+ parts = [prompt_name, base, model_id, temperature, special or '-']
94
+ return make_key(*parts)
95
+
96
+
97
+ def run_prompt(prompt_name: str, input_payload: Dict[str, Any], *, model_id: str = 'Qwen/Qwen3-7B-Instruct', seed: int = 42) -> Any:
98
+ if prompt_name not in PRESETS:
99
+ raise ValueError(f'Unknown prompt: {prompt_name}')
100
+
101
+ input_model = INPUT_MODELS[prompt_name]
102
+ parsed_input = input_model.parse_obj(input_payload)
103
+
104
+ preset = PRESETS[prompt_name]
105
+ ckey = _cache_key(prompt_name, parsed_input.dict(by_alias=True), model_id, preset['temperature'])
106
+ cached = cache_get(ckey)
107
+ if cached is not None:
108
+ return json.loads(cached)
109
+
110
+ # Get adapter with lazy initialization
111
+ adapter = _get_adapter(model_id)
112
+
113
+ system = SYSTEMS[prompt_name]()
114
+ user = json.dumps(parsed_input.dict(by_alias=True), ensure_ascii=False)
115
+
116
+ text = adapter.generate(
117
+ system=system,
118
+ user=f"Return JSON only. No commentary.\nInput: {user}",
119
+ temperature=preset['temperature'],
120
+ max_tokens=preset['max_tokens'],
121
+ stop=None,
122
+ seed=seed,
123
+ )
124
+
125
+ if prompt_name == 'instructor_insight':
126
+ data = json.loads(text)
127
+ if not isinstance(data, list):
128
+ raise ValueError('Expected a JSON array')
129
+ from .schemas import InstructorInsightRow
130
+ validated = [InstructorInsightRow.parse_obj(x).dict() for x in data]
131
+ out_obj = validated
132
+ else:
133
+ out_model = OUTPUT_MODELS[prompt_name]
134
+ out_obj = parse_and_validate(out_model, text)
135
+ # Handle RootModel (Pydantic v2)
136
+ if hasattr(out_obj, 'root'):
137
+ out_obj = out_obj.root
138
+ elif hasattr(out_obj, 'dict'):
139
+ out_obj = out_obj.dict(by_alias=True)
140
+ elif hasattr(out_obj, '__root__'):
141
+ out_obj = out_obj.__root__
142
+
143
+ cache_set(ckey, json.dumps(out_obj, ensure_ascii=False))
144
+ return out_obj
cog_tutor/knowledge_tracing.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import json
3
+ from typing import Dict, List, Any, Tuple
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timedelta
6
+ import sqlite3
7
+
8
+ @dataclass
9
+ class SkillMastery:
10
+ skill: str
11
+ theta: float # IRT ability parameter (-3 to +3)
12
+ sem: float # Standard error of measurement
13
+ last_practiced: datetime
14
+ practice_count: int
15
+ success_rate: float
16
+
17
+ @dataclass
18
+ class ItemResponse:
19
+ item_id: str
20
+ skill: str
21
+ correct: bool
22
+ response_time: float
23
+ hints_used: int
24
+ difficulty: float
25
+ timestamp: datetime
26
+
27
+ class KnowledgeTracer:
28
+ """Knowledge tracing system using Item Response Theory and Bayesian updating."""
29
+
30
+ def __init__(self, db_path: str = "knowledge_tracing.sqlite"):
31
+ self.db_path = db_path
32
+ self._init_database()
33
+ self.skill_masteries: Dict[str, SkillMastery] = {}
34
+ self.response_history: List[ItemResponse] = []
35
+
36
+ def _init_database(self):
37
+ """Initialize database for storing tracing data."""
38
+ with sqlite3.connect(self.db_path) as conn:
39
+ conn.execute("""
40
+ CREATE TABLE IF NOT EXISTS skill_mastery (
41
+ skill TEXT PRIMARY KEY,
42
+ theta REAL DEFAULT 0.0,
43
+ sem REAL DEFAULT 1.0,
44
+ last_practiced TIMESTAMP,
45
+ practice_count INTEGER DEFAULT 0,
46
+ success_rate REAL DEFAULT 0.0
47
+ )
48
+ """)
49
+ conn.execute("""
50
+ CREATE TABLE IF NOT EXISTS item_responses (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ item_id TEXT,
53
+ skill TEXT,
54
+ correct BOOLEAN,
55
+ response_time REAL,
56
+ hints_used INTEGER,
57
+ difficulty REAL,
58
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
59
+ )
60
+ """)
61
+ conn.execute("""
62
+ CREATE INDEX IF NOT EXISTS idx_skill_responses ON item_responses(skill)
63
+ """)
64
+
65
+ def update_mastery(self, response: ItemResponse) -> float:
66
+ """Update skill mastery using Bayesian updating with IRT."""
67
+ skill = response.skill
68
+
69
+ # Load current mastery if exists
70
+ if skill not in self.skill_masteries:
71
+ self._load_skill_mastery(skill)
72
+
73
+ current = self.skill_masteries.get(skill, SkillMastery(
74
+ skill=skill, theta=0.0, sem=1.0,
75
+ last_practiced=datetime.now(),
76
+ practice_count=0, success_rate=0.0
77
+ ))
78
+
79
+ # IRT 2-parameter model update
80
+ # P(correct) = 1 / (1 + exp(-a*(theta - b)))
81
+ # where a = discrimination (fixed at 1.0), b = difficulty
82
+
83
+ # Calculate likelihood of response given current theta
84
+ logit = current.theta - response.difficulty
85
+ p_correct = 1.0 / (1.0 + np.exp(-logit))
86
+
87
+ # Bayesian update using response as evidence
88
+ # Posterior precision = prior precision + information
89
+ prior_precision = 1.0 / (current.sem ** 2)
90
+
91
+ # Information function for 2PL IRT
92
+ information = p_correct * (1 - p_correct)
93
+
94
+ posterior_precision = prior_precision + information
95
+ posterior_sem = np.sqrt(1.0 / posterior_precision)
96
+
97
+ # Update theta based on response
98
+ if response.correct:
99
+ # Correct response increases theta
100
+ theta_update = (current.theta / (current.sem ** 2) +
101
+ information * response.difficulty) / posterior_precision
102
+ else:
103
+ # Incorrect response decreases theta
104
+ theta_update = (current.theta / (current.sem ** 2) -
105
+ information * (1 - response.difficulty)) / posterior_precision
106
+
107
+ # Apply forgetting factor for time since last practice
108
+ days_since_practice = (response.timestamp - current.last_practiced).days
109
+ forgetting_factor = np.exp(-0.05 * days_since_practice) # 5% decay per day
110
+
111
+ theta_update *= forgetting_factor
112
+
113
+ # Update mastery
114
+ updated = SkillMastery(
115
+ skill=skill,
116
+ theta=np.clip(theta_update, -3.0, 3.0),
117
+ sem=posterior_sem,
118
+ last_practiced=response.timestamp,
119
+ practice_count=current.practice_count + 1,
120
+ success_rate=self._update_success_rate(current.success_rate, current.practice_count, response.correct)
121
+ )
122
+
123
+ self.skill_masteries[skill] = updated
124
+ self.response_history.append(response)
125
+
126
+ # Save to database
127
+ self._save_skill_mastery(updated)
128
+ self._save_response(response)
129
+
130
+ return updated.theta
131
+
132
+ def _update_success_rate(self, current_rate: float, count: int, correct: bool) -> float:
133
+ """Update exponential moving average of success rate."""
134
+ alpha = 0.1 # Learning rate for EMA
135
+ if count == 0:
136
+ return 1.0 if correct else 0.0
137
+ return alpha * (1.0 if correct else 0.0) + (1 - alpha) * current_rate
138
+
139
+ def get_mastery_probability(self, skill: str) -> float:
140
+ """Convert theta to mastery probability (0-1 scale)."""
141
+ if skill not in self.skill_masteries:
142
+ self._load_skill_mastery(skill)
143
+
144
+ # Use default theta if skill not found
145
+ theta = self.skill_masteries.get(skill, SkillMastery(
146
+ skill=skill, theta=0.0, sem=1.0,
147
+ last_practiced=datetime.now(),
148
+ practice_count=0, success_rate=0.0
149
+ )).theta
150
+
151
+ # Logistic transformation: theta=0 -> 0.5, theta=+2 -> 0.88, theta=-2 -> 0.12
152
+ return 1.0 / (1.0 + np.exp(-theta))
153
+
154
+ def calculate_information_gain(self, skill: str, difficulty: float) -> float:
155
+ """Calculate expected information gain for an item."""
156
+ if skill not in self.skill_masteries:
157
+ self._load_skill_mastery(skill)
158
+
159
+ # Use default theta if skill not found
160
+ theta = self.skill_masteries.get(skill, SkillMastery(
161
+ skill=skill, theta=0.0, sem=1.0,
162
+ last_practiced=datetime.now(),
163
+ practice_count=0, success_rate=0.0
164
+ )).theta
165
+
166
+ # Expected information = I(theta) where I is Fisher information
167
+ logit = theta - difficulty
168
+ p_correct = 1.0 / (1.0 + np.exp(-logit))
169
+ information = p_correct * (1 - p_correct)
170
+
171
+ return information
172
+
173
+ def get_next_item_recommendations(self, candidate_items: List[Dict[str, Any]],
174
+ max_items: int = 5) -> List[Dict[str, Any]]:
175
+ """Recommend next items based on information gain and spacing."""
176
+ scored_items = []
177
+
178
+ for item in candidate_items:
179
+ skill = item['skill']
180
+ difficulty = item['difficulty']
181
+
182
+ # Calculate information gain
183
+ info_gain = self.calculate_information_gain(skill, difficulty)
184
+
185
+ # Calculate spacing benefit (higher for items not practiced recently)
186
+ if skill in self.skill_masteries:
187
+ days_since = (datetime.now() - self.skill_masteries[skill].last_practiced).days
188
+ spacing_bonus = min(days_since / 7.0, 1.0) # Max bonus after 1 week
189
+ else:
190
+ spacing_bonus = 1.0 # New skill gets max bonus
191
+
192
+ # Calculate mastery urgency (higher for lower mastery)
193
+ mastery = self.get_mastery_probability(skill)
194
+ urgency = 1.0 - mastery
195
+
196
+ # Combined score
197
+ score = 0.4 * info_gain + 0.3 * spacing_bonus + 0.3 * urgency
198
+
199
+ scored_items.append({
200
+ **item,
201
+ 'score': score,
202
+ 'information_gain': info_gain,
203
+ 'spacing_bonus': spacing_bonus,
204
+ 'urgency': urgency,
205
+ 'current_mastery': mastery
206
+ })
207
+
208
+ # Sort by score and return top items
209
+ scored_items.sort(key=lambda x: x['score'], reverse=True)
210
+ return scored_items[:max_items]
211
+
212
+ def get_research_metrics(self, skill: str = None) -> Dict[str, Any]:
213
+ """Calculate research metrics for evaluation."""
214
+ if skill:
215
+ responses = [r for r in self.response_history if r.skill == skill]
216
+ else:
217
+ responses = self.response_history
218
+
219
+ if not responses:
220
+ return {}
221
+
222
+ # Basic metrics
223
+ total_responses = len(responses)
224
+ correct_responses = sum(1 for r in responses if r.correct)
225
+ accuracy = correct_responses / total_responses
226
+
227
+ # Time metrics
228
+ avg_response_time = np.mean([r.response_time for r in responses])
229
+
230
+ # Hint metrics
231
+ hints_per_response = np.mean([r.hints_used for r in responses])
232
+
233
+ # Learning gain (compare first vs last 10 responses)
234
+ if len(responses) >= 20:
235
+ early_responses = responses[:10]
236
+ late_responses = responses[-10:]
237
+
238
+ early_accuracy = sum(1 for r in early_responses if r.correct) / len(early_responses)
239
+ late_accuracy = sum(1 for r in late_responses if r.correct) / len(late_responses)
240
+ learning_gain = late_accuracy - early_accuracy
241
+ else:
242
+ learning_gain = 0.0
243
+
244
+ # Retention (performance on items practiced > 3 days ago)
245
+ retention_items = [r for r in responses
246
+ if (datetime.now() - r.timestamp).days > 3]
247
+ if retention_items:
248
+ retention_rate = sum(1 for r in retention_items if r.correct) / len(retention_items)
249
+ else:
250
+ retention_rate = None
251
+
252
+ return {
253
+ 'total_responses': total_responses,
254
+ 'accuracy': accuracy,
255
+ 'avg_response_time': avg_response_time,
256
+ 'hints_per_response': hints_per_response,
257
+ 'learning_gain': learning_gain,
258
+ 'retention_rate': retention_rate,
259
+ 'skill_masteries': len(self.skill_masteries)
260
+ }
261
+
262
+ def _load_skill_mastery(self, skill: str):
263
+ """Load skill mastery from database."""
264
+ with sqlite3.connect(self.db_path) as conn:
265
+ conn.row_factory = sqlite3.Row
266
+ cursor = conn.execute(
267
+ "SELECT * FROM skill_mastery WHERE skill = ?", (skill,)
268
+ )
269
+ row = cursor.fetchone()
270
+ if row:
271
+ self.skill_masteries[skill] = SkillMastery(
272
+ skill=row['skill'],
273
+ theta=row['theta'],
274
+ sem=row['sem'],
275
+ last_practiced=datetime.fromisoformat(row['last_practiced']),
276
+ practice_count=row['practice_count'],
277
+ success_rate=row['success_rate']
278
+ )
279
+
280
+ def _save_skill_mastery(self, mastery: SkillMastery):
281
+ """Save skill mastery to database."""
282
+ with sqlite3.connect(self.db_path) as conn:
283
+ conn.execute("""
284
+ INSERT OR REPLACE INTO skill_mastery
285
+ (skill, theta, sem, last_practiced, practice_count, success_rate)
286
+ VALUES (?, ?, ?, ?, ?, ?)
287
+ """, (
288
+ mastery.skill,
289
+ mastery.theta,
290
+ mastery.sem,
291
+ mastery.last_practiced.isoformat(),
292
+ mastery.practice_count,
293
+ mastery.success_rate
294
+ ))
295
+
296
+ def _save_response(self, response: ItemResponse):
297
+ """Save item response to database."""
298
+ with sqlite3.connect(self.db_path) as conn:
299
+ conn.execute("""
300
+ INSERT INTO item_responses
301
+ (item_id, skill, correct, response_time, hints_used, difficulty, timestamp)
302
+ VALUES (?, ?, ?, ?, ?, ?, ?)
303
+ """, (
304
+ response.item_id,
305
+ response.skill,
306
+ response.correct,
307
+ response.response_time,
308
+ response.hints_used,
309
+ response.difficulty,
310
+ response.timestamp.isoformat()
311
+ ))
cog_tutor/prompts.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def item_explanation() -> str:
2
+ return (
3
+ "You are a tutoring engine for short-form questions. Given a question, user answer, and correct solution, "
4
+ "explain the reasoning step-by-step in plain language. Output three tiers: Hint, Guided reasoning, Full explanation. "
5
+ "Never invent new facts beyond the item’s text. Return JSON with keys hint, guided, full."
6
+ )
7
+
8
+ def mastery_diagnostic() -> str:
9
+ return (
10
+ "You estimate mastery of a single skill from the last 10 responses. "
11
+ "Consider correctness, response time, and hint usage. Output a float 0–1 and one-sentence rationale. "
12
+ "Return JSON with keys mastery, comment."
13
+ )
14
+
15
+ def next_item_selector() -> str:
16
+ return (
17
+ "You are a learning planner. Choose the next item that maximizes expected learning gain. "
18
+ "Prioritize skills with low mastery and overdue reviews. Return item_id and reason as JSON."
19
+ )
20
+
21
+ def skill_feedback() -> str:
22
+ return (
23
+ "Summarize a learner’s progress across all skills. Highlight top 3 strengths and 3 weaknesses. "
24
+ "Give one actionable tip per weakness. Output concise JSON with strengths and weaknesses."
25
+ )
26
+
27
+ def hint_generation() -> str:
28
+ return (
29
+ "Provide a tiered hint sequence for a given question. Level 1: conceptual nudge. Level 2: procedural cue. "
30
+ "Level 3: near-solution scaffold. Do not reveal the final answer. Return JSON keys '1','2','3'."
31
+ )
32
+
33
+ def reflection() -> str:
34
+ return (
35
+ "After each session, guide the learner to reflect. Ask one self-evaluation question and one improvement question. "
36
+ "Keep tone neutral and constructive. Return JSON with reflection and improvement."
37
+ )
38
+
39
+ def instructor_insight() -> str:
40
+ return (
41
+ "You analyze cohort data and surface anomalies for teachers. Detect items with low discrimination or poor fit. "
42
+ "Suggest review actions. Return JSON array of objects with item_id and flag."
43
+ )
44
+
45
+ def explanation_compression() -> str:
46
+ return (
47
+ "Convert a long explanation into a single 2-line recap focused on rule application. "
48
+ "Keep syntax minimal, grade-appropriate. Return JSON with recap."
49
+ )
50
+
51
+ def question_authoring() -> str:
52
+ return (
53
+ "Generate 5 original practice items for a given skill. Each must include the correct answer and a short rationale. "
54
+ "Output JSON array with objects having q, a, why."
55
+ )
56
+
57
+ def tone_normalizer() -> str:
58
+ return (
59
+ "Rewrite AI feedback to be neutral, factual, and non-emotional. Keep it under 20 words. Return JSON with normalized."
60
+ )
cog_tutor/rag/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .retriever import KnowledgeRetriever
2
+ from .knowledge_base import KnowledgeBase
3
+ from .rag_prompts import RAGEnhancedPrompts
4
+
5
+ __all__ = ["KnowledgeRetriever", "KnowledgeBase", "RAGEnhancedPrompts"]
cog_tutor/rag/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (374 Bytes). View file
 
cog_tutor/rag/__pycache__/knowledge_base.cpython-313.pyc ADDED
Binary file (8.42 kB). View file
 
cog_tutor/rag/__pycache__/rag_prompts.cpython-313.pyc ADDED
Binary file (4.42 kB). View file
 
cog_tutor/rag/__pycache__/retriever.cpython-313.pyc ADDED
Binary file (6.25 kB). View file
 
cog_tutor/rag/knowledge_base.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import hashlib
3
+ from typing import List, Dict, Any, Optional
4
+ from pathlib import Path
5
+ import sqlite3
6
+
7
+ class KnowledgeBase:
8
+ """Knowledge base for educational content with fact-grounded explanations."""
9
+
10
+ def __init__(self, db_path: str = "knowledge_base.sqlite"):
11
+ self.db_path = db_path
12
+ self._init_database()
13
+ self._load_sample_content()
14
+
15
+ def _init_database(self):
16
+ """Initialize SQLite database for knowledge storage."""
17
+ with sqlite3.connect(self.db_path) as conn:
18
+ conn.execute("""
19
+ CREATE TABLE IF NOT EXISTS knowledge_items (
20
+ id TEXT PRIMARY KEY,
21
+ skill TEXT NOT NULL,
22
+ content TEXT NOT NULL,
23
+ facts TEXT NOT NULL,
24
+ difficulty REAL DEFAULT 0.5,
25
+ prerequisite_skills TEXT,
26
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
27
+ )
28
+ """)
29
+ conn.execute("""
30
+ CREATE INDEX IF NOT EXISTS idx_skill ON knowledge_items(skill)
31
+ """)
32
+
33
+ def _load_sample_content(self):
34
+ """Load sample educational content for testing."""
35
+ sample_items = [
36
+ {
37
+ "id": "algebra_simplify_001",
38
+ "skill": "algebra_simplification",
39
+ "content": "To simplify algebraic expressions, combine like terms by adding or subtracting coefficients of the same variable. For example, 3x + 2x = 5x.",
40
+ "facts": [
41
+ "Like terms have the same variable raised to the same power",
42
+ "Coefficients of like terms can be combined through addition",
43
+ "The variable part remains unchanged when combining like terms"
44
+ ],
45
+ "difficulty": 0.3,
46
+ "prerequisite_skills": ["basic_arithmetic", "variables"]
47
+ },
48
+ {
49
+ "id": "algebra_simplify_002",
50
+ "skill": "algebra_simplification",
51
+ "content": "When simplifying expressions with division, first combine like terms in the numerator, then divide by the denominator. Example: (3x + 2x) / 5 = 5x / 5 = x.",
52
+ "facts": [
53
+ "Division applies to the entire expression",
54
+ "Simplify numerator before dividing",
55
+ "A term divided by itself equals 1"
56
+ ],
57
+ "difficulty": 0.5,
58
+ "prerequisite_skills": ["algebra_simplification", "division"]
59
+ },
60
+ {
61
+ "id": "linear_eq_001",
62
+ "skill": "linear_equations",
63
+ "content": "To solve linear equations, isolate the variable by performing inverse operations. Add/subtract to isolate the variable term, then multiply/divide to solve for the variable.",
64
+ "facts": [
65
+ "Inverse operations undo each other (addition ↔ subtraction, multiplication ↔ division)",
66
+ "Apply the same operation to both sides to maintain equality",
67
+ "Goal is to isolate the variable on one side"
68
+ ],
69
+ "difficulty": 0.4,
70
+ "prerequisite_skills": ["algebra_simplification"]
71
+ },
72
+ {
73
+ "id": "fraction_div_001",
74
+ "skill": "fraction_operations",
75
+ "content": "To divide fractions, multiply by the reciprocal of the second fraction. The reciprocal of a/b is b/a.",
76
+ "facts": [
77
+ "Division is equivalent to multiplication by the reciprocal",
78
+ "Reciprocal flips numerator and denominator",
79
+ "Multiply numerators together and denominators together"
80
+ ],
81
+ "difficulty": 0.6,
82
+ "prerequisite_skills": ["fraction_multiplication"]
83
+ },
84
+ {
85
+ "id": "ratio_001",
86
+ "skill": "ratios",
87
+ "content": "Ratios compare quantities. To solve ratio problems, set up proportions and cross-multiply. a:b = c:d means a×d = b×c.",
88
+ "facts": [
89
+ "Ratios show relative sizes of quantities",
90
+ "Equivalent ratios have the same value when simplified",
91
+ "Cross-multiplication solves proportion equations"
92
+ ],
93
+ "difficulty": 0.5,
94
+ "prerequisite_skills": ["proportions"]
95
+ }
96
+ ]
97
+
98
+ with sqlite3.connect(self.db_path) as conn:
99
+ for item in sample_items:
100
+ conn.execute("""
101
+ INSERT OR REPLACE INTO knowledge_items
102
+ (id, skill, content, facts, difficulty, prerequisite_skills)
103
+ VALUES (?, ?, ?, ?, ?, ?)
104
+ """, (
105
+ item["id"],
106
+ item["skill"],
107
+ item["content"],
108
+ json.dumps(item["facts"]),
109
+ item["difficulty"],
110
+ json.dumps(item["prerequisite_skills"])
111
+ ))
112
+
113
+ def retrieve_by_skill(self, skill: str, limit: int = 3) -> List[Dict[str, Any]]:
114
+ """Retrieve knowledge items for a specific skill."""
115
+ with sqlite3.connect(self.db_path) as conn:
116
+ conn.row_factory = sqlite3.Row
117
+ cursor = conn.execute("""
118
+ SELECT * FROM knowledge_items
119
+ WHERE skill = ? OR skill LIKE ?
120
+ ORDER BY difficulty ASC
121
+ LIMIT ?
122
+ """, (skill, f"%{skill}%", limit))
123
+
124
+ results = []
125
+ for row in cursor.fetchall():
126
+ results.append({
127
+ "id": row["id"],
128
+ "skill": row["skill"],
129
+ "content": row["content"],
130
+ "facts": json.loads(row["facts"]),
131
+ "difficulty": row["difficulty"],
132
+ "prerequisite_skills": json.loads(row["prerequisite_skills"])
133
+ })
134
+ return results
135
+
136
+ def retrieve_by_query(self, query: str, limit: int = 3) -> List[Dict[str, Any]]:
137
+ """Retrieve knowledge items based on text search."""
138
+ with sqlite3.connect(self.db_path) as conn:
139
+ conn.row_factory = sqlite3.Row
140
+ cursor = conn.execute("""
141
+ SELECT * FROM knowledge_items
142
+ WHERE content LIKE ? OR skill LIKE ?
143
+ ORDER BY difficulty ASC
144
+ LIMIT ?
145
+ """, (f"%{query}%", f"%{query}%", limit))
146
+
147
+ results = []
148
+ for row in cursor.fetchall():
149
+ results.append({
150
+ "id": row["id"],
151
+ "skill": row["skill"],
152
+ "content": row["content"],
153
+ "facts": json.loads(row["facts"]),
154
+ "difficulty": row["difficulty"],
155
+ "prerequisite_skills": json.loads(row["prerequisite_skills"])
156
+ })
157
+ return results
158
+
159
+ def add_knowledge_item(self, item: Dict[str, Any]):
160
+ """Add a new knowledge item to the database."""
161
+ with sqlite3.connect(self.db_path) as conn:
162
+ conn.execute("""
163
+ INSERT OR REPLACE INTO knowledge_items
164
+ (id, skill, content, facts, difficulty, prerequisite_skills)
165
+ VALUES (?, ?, ?, ?, ?, ?)
166
+ """, (
167
+ item["id"],
168
+ item["skill"],
169
+ item["content"],
170
+ json.dumps(item["facts"]),
171
+ item["difficulty"],
172
+ json.dumps(item.get("prerequisite_skills", []))
173
+ ))
cog_tutor/rag/rag_prompts.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, List
2
+
3
+ class RAGEnhancedPrompts:
4
+ """RAG-enhanced prompts with knowledge grounding and citations."""
5
+
6
+ @staticmethod
7
+ def item_explanation_with_rag() -> str:
8
+ return """You are a tutoring engine for short-form questions with access to educational knowledge.
9
+
10
+ Given a question, user answer, correct solution, and relevant facts from the knowledge base, explain the reasoning step-by-step in plain language.
11
+
12
+ IMPORTANT:
13
+ - Use ONLY the provided facts to build your explanation
14
+ - Cite the knowledge sources using [Source X] notation
15
+ - Output three tiers: Hint, Guided reasoning, Full explanation
16
+ - Never invent new facts beyond the provided knowledge
17
+
18
+ Output JSON with keys: hint, guided, full, citations
19
+
20
+ Example citation format: "Combine like terms by adding coefficients [Source 1]." """
21
+
22
+ @staticmethod
23
+ def hint_generation_with_rag() -> str:
24
+ return """You are generating hints using educational knowledge.
25
+
26
+ Given a question and relevant facts, provide a tiered hint sequence:
27
+ - Level 1: conceptual nudge using the facts
28
+ - Level 2: procedural cue based on the knowledge
29
+ - Level 3: near-solution scaffold
30
+
31
+ IMPORTANT:
32
+ - Use ONLY the provided facts
33
+ - Do not reveal the final answer
34
+ - Cite sources using [Source X]
35
+
36
+ Return JSON with keys '1','2','3' and include citations in each hint."""
37
+
38
+ @staticmethod
39
+ def adaptive_question_generation() -> str:
40
+ return """You are generating adaptive practice questions based on student performance.
41
+
42
+ Given a skill, mastery level, and knowledge content, create a question that:
43
+ - Matches the student's current mastery (difficulty = 1 - mastery)
44
+ - Uses concepts from the provided knowledge
45
+ - Includes the correct answer and explanation
46
+
47
+ Output JSON with keys: question, answer, explanation, difficulty, skill"""
48
+
49
+ @staticmethod
50
+ def next_item_selector_with_entropy() -> str:
51
+ return """You are a learning planner using entropy-based scheduling.
52
+
53
+ Given candidate items, student mastery, and recent performance, select the next item that:
54
+ - Maximizes expected learning gain (high information gain for uncertain skills)
55
+ - Balances review and new content
56
+ - Considers prerequisite relationships
57
+
58
+ Return JSON with keys: item_id, reason, expected_gain, information_gain"""
59
+
60
+ @staticmethod
61
+ def mastery_diagnostic_with_irt() -> str:
62
+ return """You are estimating mastery using Item Response Theory (IRT).
63
+
64
+ Given skill performance data including:
65
+ - Response accuracy
66
+ - Item difficulty
67
+ - Response time
68
+ - Hint usage
69
+
70
+ Estimate:
71
+ - Theta (ability parameter): -3 to +3 scale
72
+ - Standard error of measurement
73
+ - Mastery probability (0-1)
74
+
75
+ Return JSON with keys: theta, sem, mastery, confidence_interval"""
76
+
77
+ @staticmethod
78
+ def research_metrics() -> str:
79
+ return """You are calculating research metrics for learning analytics.
80
+
81
+ Given session data, compute:
82
+ - Learning gain (pre/post mastery difference)
83
+ - Retention rate (accuracy on review items)
84
+ - Hint efficiency (hints per correct answer)
85
+ - Time on task
86
+ - Knowledge transfer (cross-skill performance)
87
+
88
+ Return JSON with all metrics and statistical significance where applicable."""
cog_tutor/rag/retriever.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import sqlite3
3
+ from typing import List, Dict, Any, Tuple
4
+ from .knowledge_base import KnowledgeBase
5
+ import numpy as np
6
+ from sklearn.feature_extraction.text import TfidfVectorizer
7
+ from sklearn.metrics.pairwise import cosine_similarity
8
+
9
+ class KnowledgeRetriever:
10
+ """Retrieval-augmented generation system for educational content."""
11
+
12
+ def __init__(self, knowledge_base: KnowledgeBase):
13
+ self.kb = knowledge_base
14
+ self.vectorizer = TfidfVectorizer(
15
+ stop_words='english',
16
+ ngram_range=(1, 2),
17
+ max_features=1000
18
+ )
19
+ self._build_index()
20
+
21
+ def _build_index(self):
22
+ """Build TF-IDF index for semantic search."""
23
+ # Get all knowledge items
24
+ all_items = []
25
+ with sqlite3.connect(self.kb.db_path) as conn:
26
+ conn.row_factory = sqlite3.Row
27
+ cursor = conn.execute("SELECT * FROM knowledge_items")
28
+ for row in cursor.fetchall():
29
+ all_items.append({
30
+ "id": row["id"],
31
+ "skill": row["skill"],
32
+ "content": row["content"],
33
+ "facts": eval(row["facts"]),
34
+ "difficulty": row["difficulty"]
35
+ })
36
+
37
+ self.all_items = all_items
38
+
39
+ # Build corpus for vectorization
40
+ corpus = []
41
+ for item in self.all_items:
42
+ text = f"{item['skill']} {item['content']} {' '.join(item['facts'])}"
43
+ corpus.append(text)
44
+
45
+ # Fit vectorizer
46
+ self.tfidf_matrix = self.vectorizer.fit_transform(corpus)
47
+
48
+ def retrieve_relevant_knowledge(self, query: str, skill: str = None, top_k: int = 3) -> List[Dict[str, Any]]:
49
+ """Retrieve relevant knowledge items for a query."""
50
+ # If skill is specified, prioritize skill-specific items
51
+ if skill:
52
+ skill_items = self.kb.retrieve_by_skill(skill, limit=top_k)
53
+ if len(skill_items) >= top_k:
54
+ return skill_items[:top_k]
55
+
56
+ # Use semantic search
57
+ query_vec = self.vectorizer.transform([query])
58
+ similarities = cosine_similarity(query_vec, self.tfidf_matrix).flatten()
59
+
60
+ # Get top-k most similar items
61
+ top_indices = np.argsort(similarities)[-top_k:][::-1]
62
+
63
+ results = []
64
+ for idx in top_indices:
65
+ if similarities[idx] > 0.1: # Threshold for relevance
66
+ item = self.all_items[idx].copy()
67
+ item["relevance_score"] = float(similarities[idx])
68
+ results.append(item)
69
+
70
+ return results
71
+
72
+ def get_facts_for_explanation(self, question: str, user_answer: str, solution: str) -> List[str]:
73
+ """Extract relevant facts for explaining a problem."""
74
+ query = f"{question} {solution}"
75
+ relevant_items = self.retrieve_relevant_knowledge(query, top_k=5)
76
+
77
+ # Collect and deduplicate facts
78
+ all_facts = []
79
+ seen_facts = set()
80
+
81
+ for item in relevant_items:
82
+ for fact in item["facts"]:
83
+ if fact not in seen_facts:
84
+ all_facts.append(fact)
85
+ seen_facts.add(fact)
86
+
87
+ return all_facts[:5] # Return top 5 most relevant facts
88
+
89
+ def get_contextual_hints(self, question: str, hint_level: int = 1) -> List[str]:
90
+ """Generate contextual hints based on retrieved knowledge."""
91
+ relevant_items = self.retrieve_relevant_knowledge(question, top_k=3)
92
+
93
+ if hint_level == 1:
94
+ # Conceptual nudge
95
+ hints = [item["content"].split('.')[0] + "." for item in relevant_items]
96
+ elif hint_level == 2:
97
+ # Procedural cue
98
+ hints = [item["content"] for item in relevant_items]
99
+ else:
100
+ # Near-solution scaffold
101
+ hints = []
102
+ for item in relevant_items:
103
+ for fact in item["facts"]:
104
+ if "step" in fact.lower() or "method" in fact.lower():
105
+ hints.append(fact)
106
+
107
+ return hints[:3]
108
+
109
+ def get_explanation_with_citations(self, question: str, user_answer: str, solution: str) -> Dict[str, Any]:
110
+ """Generate explanation with knowledge citations."""
111
+ facts = self.get_facts_for_explanation(question, user_answer, solution)
112
+ relevant_items = self.retrieve_relevant_knowledge(f"{question} {solution}", top_k=3)
113
+
114
+ return {
115
+ "facts": facts,
116
+ "citations": [{"id": item["id"], "skill": item["skill"]} for item in relevant_items],
117
+ "sources": [item["content"] for item in relevant_items]
118
+ }
cog_tutor/schemas.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any
2
+ from pydantic import BaseModel, Field, confloat, conlist, RootModel
3
+
4
+ class ItemExplanationInput(BaseModel):
5
+ question: str
6
+ user_answer: str
7
+ solution: str
8
+
9
+ class ItemExplanationOutput(BaseModel):
10
+ hint: str
11
+ guided: str
12
+ full: str
13
+
14
+ class MasteryDiagEvent(BaseModel):
15
+ correct: bool
16
+ rt: int
17
+ hints: int
18
+
19
+ class MasteryDiagnosticInput(BaseModel):
20
+ skill: str
21
+ history: conlist(MasteryDiagEvent, min_length=1, max_length=50)
22
+
23
+ class MasteryDiagnosticOutput(BaseModel):
24
+ mastery: confloat(ge=0.0, le=1.0)
25
+ comment: str
26
+
27
+ class NextItemCandidate(BaseModel):
28
+ item_id: str
29
+ skill: str
30
+ p_correct: confloat(ge=0.0, le=1.0)
31
+ due: bool
32
+
33
+ class NextItemSelectorInput(BaseModel):
34
+ user_id: str
35
+ candidates: conlist(NextItemCandidate, min_length=1)
36
+
37
+ class NextItemSelectorOutput(BaseModel):
38
+ item_id: str
39
+ reason: str
40
+
41
+ class SkillMastery(BaseModel):
42
+ name: str
43
+ mastery: confloat(ge=0.0, le=1.0)
44
+
45
+ class SkillFeedbackInput(BaseModel):
46
+ skills: conlist(SkillMastery, min_length=1)
47
+
48
+ class SkillWeakness(BaseModel):
49
+ skill: str
50
+ tip: str
51
+
52
+ class SkillFeedbackOutput(BaseModel):
53
+ strengths: List[str]
54
+ weaknesses: List[SkillWeakness]
55
+
56
+ class HintGenerationInput(BaseModel):
57
+ question: str
58
+
59
+ class HintGenerationOutput(BaseModel):
60
+ field_1: str = Field(alias='1')
61
+ field_2: str = Field(alias='2')
62
+ field_3: str = Field(alias='3')
63
+ class Config:
64
+ populate_by_name = True
65
+
66
+ class ReflectionInput(BaseModel):
67
+ session: Dict[str, Any]
68
+
69
+ class ReflectionOutput(BaseModel):
70
+ reflection: str
71
+ improvement: str
72
+
73
+ class InstructorItem(BaseModel):
74
+ id: str
75
+ discrimination: float
76
+ accuracy: confloat(ge=0.0, le=1.0)
77
+
78
+ class InstructorInsightInput(BaseModel):
79
+ items: conlist(InstructorItem, min_length=1)
80
+
81
+ class InstructorInsightRow(BaseModel):
82
+ item_id: str
83
+ flag: str
84
+
85
+ class ExplanationCompressionInput(BaseModel):
86
+ explanation: str
87
+
88
+ class ExplanationCompressionOutput(BaseModel):
89
+ recap: str
90
+
91
+ class QuestionAuthoringInput(BaseModel):
92
+ skill: str
93
+ difficulty: str
94
+
95
+ class QAItem(BaseModel):
96
+ q: str
97
+ a: str
98
+ why: str
99
+
100
+ class QuestionAuthoringOutput(RootModel[List[QAItem]]):
101
+ pass
102
+
103
+ class ToneNormalizerInput(BaseModel):
104
+ raw: str
105
+
106
+ class ToneNormalizerOutput(BaseModel):
107
+ normalized: str
cog_tutor/validation.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from typing import Type, Any
3
+ from pydantic import BaseModel
4
+
5
+ def parse_and_validate(model: Type[BaseModel], text: str) -> Any:
6
+ data = json.loads(text)
7
+ # Support pydantic v1 and v2
8
+ if hasattr(model, 'model_validate'):
9
+ return model.model_validate(data)
10
+ return model.parse_obj(data)
cognitive_llm.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
3
+ from typing import Optional, Dict, Any
4
+
5
+ class CognitiveLLM:
6
+ def __init__(self, model_name: str = "Qwen/Qwen3-7B-Instruct", device: str = None):
7
+ """
8
+ Initialize the Cognitive LLM with the specified model.
9
+
10
+ Args:
11
+ model_name: Name of the model to use (default: Qwen/Qwen3-7B-Instruct)
12
+ device: Device to run the model on ('cuda', 'mps', or 'cpu'). Auto-detects if None.
13
+ """
14
+ self.model_name = model_name
15
+ self.device = device if device else 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'
16
+
17
+ print(f"Loading {model_name} on {self.device}...")
18
+
19
+ # Load tokenizer and model
20
+ self.tokenizer = AutoTokenizer.from_pretrained(
21
+ model_name,
22
+ trust_remote_code=True
23
+ )
24
+
25
+ # Load model with 4-bit quantization for efficiency
26
+ self.model = AutoModelForCausalLM.from_pretrained(
27
+ model_name,
28
+ device_map="auto",
29
+ trust_remote_code=True,
30
+ torch_dtype=torch.bfloat16,
31
+ attn_implementation="flash_attention_2" if self.device.startswith('cuda') else None,
32
+ load_in_4bit=True
33
+ )
34
+
35
+ # Create text generation pipeline
36
+ self.pipe = pipeline(
37
+ "text-generation",
38
+ model=self.model,
39
+ tokenizer=self.tokenizer,
40
+ device_map="auto"
41
+ )
42
+
43
+ print(f"Model {model_name} loaded successfully on {self.device}")
44
+
45
+ def generate(
46
+ self,
47
+ prompt: str,
48
+ max_new_tokens: int = 512,
49
+ temperature: float = 0.7,
50
+ top_p: float = 0.9,
51
+ **generation_kwargs
52
+ ) -> str:
53
+ """
54
+ Generate text from a prompt using the loaded model.
55
+
56
+ Args:
57
+ prompt: Input text prompt
58
+ max_new_tokens: Maximum number of tokens to generate
59
+ temperature: Sampling temperature (lower = more focused, higher = more creative)
60
+ top_p: Nucleus sampling parameter
61
+ **generation_kwargs: Additional generation parameters
62
+
63
+ Returns:
64
+ Generated text
65
+ """
66
+ # Format the prompt for Qwen3 chat
67
+ messages = [
68
+ {"role": "user", "content": prompt}
69
+ ]
70
+
71
+ # Generate response
72
+ response = self.pipe(
73
+ messages,
74
+ max_new_tokens=max_new_tokens,
75
+ temperature=temperature,
76
+ top_p=top_p,
77
+ do_sample=True,
78
+ **generation_kwargs
79
+ )
80
+
81
+ # Extract and return the generated text
82
+ return response[0]["generated_text"][-1]["content"]
83
+
84
+
85
+ def main():
86
+ # Initialize the cognitive LLM
87
+ llm = CognitiveLLM()
88
+
89
+ print("\nCognitive LLM initialized. Type 'quit' to exit.")
90
+ print("Enter your prompt:")
91
+
92
+ # Interactive loop
93
+ while True:
94
+ try:
95
+ user_input = input(">> ")
96
+ if user_input.lower() in ['quit', 'exit', 'q']:
97
+ break
98
+
99
+ if user_input.strip() == '':
100
+ continue
101
+
102
+ # Generate response
103
+ response = llm.generate(user_input)
104
+ print("\nResponse:")
105
+ print(response)
106
+ print("\n---\nEnter another prompt or 'quit' to exit:")
107
+
108
+ except KeyboardInterrupt:
109
+ print("\nExiting...")
110
+ break
111
+ except Exception as e:
112
+ print(f"\nError: {str(e)}")
113
+ continue
114
+
115
+
116
+ if __name__ == "__main__":
117
+ main()
knowledge_base.sqlite ADDED
Binary file (16.4 kB). View file
 
knowledge_tracing.sqlite ADDED
Binary file (24.6 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ transformers>=4.36.0
2
+ torch>=2.0.0
3
+ sentencepiece
4
+ accelerate
5
+ bitsandbytes
6
+ pydantic>=2.5.0
research_output.json ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "user_id": "sunny_test",
3
+ "session_start": "2025-11-01T22:59:20.445840",
4
+ "metrics": {
5
+ "session_metrics": {
6
+ "duration_seconds": 2.298408,
7
+ "total_responses": 6,
8
+ "accuracy": 0.5,
9
+ "avg_response_time": 2.3333723147710166,
10
+ "hints_per_response": 0.6666666666666666,
11
+ "learning_gain": 0.0
12
+ },
13
+ "cumulative_metrics": {
14
+ "total_responses": 6,
15
+ "accuracy": 0.5,
16
+ "avg_response_time": 2.3333723147710166,
17
+ "hints_per_response": 0.6666666666666666,
18
+ "learning_gain": 0.0,
19
+ "retention_rate": null,
20
+ "skill_masteries": 1
21
+ },
22
+ "knowledge_tracing": {
23
+ "tracked_skills": 1,
24
+ "skill_masteries": {
25
+ "algebra_simplification": {
26
+ "theta": 0.014428777179973144,
27
+ "mastery_prob": 0.503607131714598,
28
+ "practice_count": 7
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "session_responses": [
34
+ {
35
+ "item_id": "test_001",
36
+ "skill": "algebra_simplification",
37
+ "correct": false,
38
+ "response_time": 2.0002338886260986,
39
+ "hints_used": 1,
40
+ "difficulty": 0.6,
41
+ "timestamp": "2025-11-01T22:59:22.449109"
42
+ },
43
+ {
44
+ "item_id": "test_002",
45
+ "skill": "algebra_simplification",
46
+ "correct": false,
47
+ "response_time": 3.0,
48
+ "hints_used": 2,
49
+ "difficulty": 0.6,
50
+ "timestamp": "2025-11-01T22:59:22.518361"
51
+ },
52
+ {
53
+ "item_id": "test_003",
54
+ "skill": "algebra_simplification",
55
+ "correct": false,
56
+ "response_time": 2.7,
57
+ "hints_used": 1,
58
+ "difficulty": 0.6,
59
+ "timestamp": "2025-11-01T22:59:22.562224"
60
+ },
61
+ {
62
+ "item_id": "test_004",
63
+ "skill": "algebra_simplification",
64
+ "correct": true,
65
+ "response_time": 2.4,
66
+ "hints_used": 0,
67
+ "difficulty": 0.6,
68
+ "timestamp": "2025-11-01T22:59:22.603972"
69
+ },
70
+ {
71
+ "item_id": "test_005",
72
+ "skill": "algebra_simplification",
73
+ "correct": true,
74
+ "response_time": 2.1,
75
+ "hints_used": 0,
76
+ "difficulty": 0.6,
77
+ "timestamp": "2025-11-01T22:59:22.648160"
78
+ },
79
+ {
80
+ "item_id": "test_006",
81
+ "skill": "algebra_simplification",
82
+ "correct": true,
83
+ "response_time": 1.8,
84
+ "hints_used": 0,
85
+ "difficulty": 0.6,
86
+ "timestamp": "2025-11-01T22:59:22.693063"
87
+ }
88
+ ]
89
+ }
test_cog_tutor.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Test the cog_tutor package imports correctly
2
+ try:
3
+ from cog_tutor import run_prompt
4
+ print("SUCCESS: cog_tutor package imported correctly")
5
+ print("The package is ready to use with your Qwen model.")
6
+ print("\nTo test with a specific model, run:")
7
+ print(" from cog_tutor import run_prompt")
8
+ print(" output = run_prompt('item_explanation', {...}, model_id='your-model-id')")
9
+ print("\nMake sure you have proper Hugging Face authentication if using gated models.")
10
+ except Exception as e:
11
+ print(f"ERROR: Failed to import cog_tutor: {e}")
test_rag_tutor.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for RAG-enhanced Cognitive Tutor system.
4
+ Demonstrates adaptive questioning, knowledge tracing, and research metrics.
5
+ """
6
+
7
+ from cog_tutor.adaptive_tutor import AdaptiveTutor
8
+ import json
9
+ import time
10
+
11
+ def test_rag_enhanced_tutor():
12
+ """Test the complete RAG-enhanced tutoring system."""
13
+
14
+ print("=== RAG-Enhanced Cognitive Tutor Test ===\n")
15
+
16
+ # Initialize tutor
17
+ tutor = AdaptiveTutor(user_id="sunny_test")
18
+
19
+ # Test 1: Generate adaptive question
20
+ print("1. Testing Adaptive Question Generation")
21
+ print("-" * 40)
22
+
23
+ question = tutor.generate_adaptive_question("algebra_simplification")
24
+ print(f"Generated Question: {question['question']}")
25
+ print(f"Expected Difficulty: {question['difficulty']:.2f}")
26
+ print(f"Knowledge Sources: {question['knowledge_sources']}")
27
+ print()
28
+
29
+ # Test 2: Process student response
30
+ print("2. Testing Student Response Processing")
31
+ print("-" * 40)
32
+
33
+ # Simulate student response
34
+ start_time = time.time()
35
+ time.sleep(2) # Simulate thinking time
36
+ response_time = time.time() - start_time
37
+
38
+ result = tutor.process_student_response(
39
+ item_id="test_001",
40
+ skill="algebra_simplification",
41
+ question=question['question'],
42
+ user_answer="5x", # Incorrect answer
43
+ correct_answer="x",
44
+ response_time=response_time,
45
+ hints_used=1
46
+ )
47
+
48
+ print(f"Response Correct: {result['correct']}")
49
+ print(f"Mastery Theta: {result['mastery_theta']:.3f}")
50
+ print(f"Mastery Probability: {result['mastery_probability']:.3f}")
51
+ print(f"Explanation Hint: {result['explanation']['hint']}")
52
+ print()
53
+
54
+ # Test 3: Generate contextual hints
55
+ print("3. Testing Contextual Hint Generation")
56
+ print("-" * 40)
57
+
58
+ hints = tutor.generate_adaptive_hints(question['question'], hint_level=2)
59
+ for i, hint in enumerate(hints, 1):
60
+ print(f"Hint {i}: {hint}")
61
+ print()
62
+
63
+ # Test 4: Get next item recommendations
64
+ print("4. Testing Next Item Recommendations")
65
+ print("-" * 40)
66
+
67
+ recommendations = tutor.get_next_items("algebra_simplification", max_items=3)
68
+ for i, rec in enumerate(recommendations, 1):
69
+ print(f"Recommendation {i}:")
70
+ print(f" Item: {rec['item_id']}")
71
+ print(f" Skill: {rec['skill']}")
72
+ print(f" Score: {rec['score']:.3f}")
73
+ print(f" Information Gain: {rec['information_gain']:.3f}")
74
+ print(f" Current Mastery: {rec['current_mastery']:.3f}")
75
+ print()
76
+
77
+ # Test 5: Evaluate mastery with IRT
78
+ print("5. Testing IRT Mastery Evaluation")
79
+ print("-" * 40)
80
+
81
+ irt_evaluation = tutor.evaluate_mastery_with_irt("algebra_simplification")
82
+ print(f"Theta (Ability): {irt_evaluation['theta']:.3f}")
83
+ print(f"Standard Error: {irt_evaluation['sem']:.3f}")
84
+ print(f"Mastery Probability: {irt_evaluation['mastery']:.3f}")
85
+ print(f"95% CI: [{irt_evaluation['confidence_interval'][0]:.2f}, {irt_evaluation['confidence_interval'][1]:.2f}]")
86
+ print()
87
+
88
+ # Test 6: Simulate multiple responses for learning metrics
89
+ print("6. Testing Learning Progress Simulation")
90
+ print("-" * 40)
91
+
92
+ # Simulate 5 more responses
93
+ for i in range(5):
94
+ # Generate question
95
+ q = tutor.generate_adaptive_question("algebra_simplification")
96
+
97
+ # Simulate improving performance
98
+ correct = i >= 2 # Get correct after 3 attempts
99
+
100
+ result = tutor.process_student_response(
101
+ item_id=f"test_{i+2:03d}",
102
+ skill="algebra_simplification",
103
+ question=q['question'],
104
+ user_answer="x" if correct else "5x",
105
+ correct_answer="x",
106
+ response_time=3.0 - i * 0.3, # Get faster
107
+ hints_used=max(0, 2 - i) # Use fewer hints
108
+ )
109
+
110
+ print(f"Response {i+1}: Correct={result['correct']}, Mastery={result['mastery_probability']:.3f}")
111
+
112
+ print()
113
+
114
+ # Test 7: Get comprehensive research metrics
115
+ print("7. Testing Research Metrics")
116
+ print("-" * 40)
117
+
118
+ metrics = tutor.get_research_metrics()
119
+
120
+ print("Session Metrics:")
121
+ session = metrics['session_metrics']
122
+ print(f" Duration: {session['duration_seconds']:.1f}s")
123
+ print(f" Total Responses: {session['total_responses']}")
124
+ print(f" Accuracy: {session['accuracy']:.3f}")
125
+ print(f" Learning Gain: {session['learning_gain']:.3f}")
126
+
127
+ print("\nCumulative Metrics:")
128
+ cumulative = metrics['cumulative_metrics']
129
+ if cumulative:
130
+ print(f" Total Responses: {cumulative.get('total_responses', 0)}")
131
+ print(f" Accuracy: {cumulative.get('accuracy', 0):.3f}")
132
+ print(f" Retention Rate: {cumulative.get('retention_rate', 'N/A')}")
133
+
134
+ print("\nKnowledge Tracing:")
135
+ kt = metrics['knowledge_tracing']
136
+ print(f" Tracked Skills: {kt['tracked_skills']}")
137
+ for skill, data in kt['skill_masteries'].items():
138
+ print(f" {skill}: θ={data['theta']:.2f}, mastery={data['mastery_prob']:.3f}")
139
+
140
+ print()
141
+
142
+ # Test 8: Test RAG knowledge retrieval
143
+ print("8. Testing Knowledge Retrieval")
144
+ print("-" * 40)
145
+
146
+ # Test fact retrieval
147
+ facts = tutor.retriever.get_facts_for_explanation(
148
+ "Simplify (3x + 2x) / 5",
149
+ "5x",
150
+ "x"
151
+ )
152
+
153
+ print("Retrieved Facts for Explanation:")
154
+ for i, fact in enumerate(facts, 1):
155
+ print(f" {i}. {fact}")
156
+
157
+ print()
158
+
159
+ # Test 9: Save metrics to file for research analysis
160
+ print("9. Saving Research Data")
161
+ print("-" * 40)
162
+
163
+ research_data = {
164
+ "user_id": tutor.user_id,
165
+ "session_start": tutor.session_start.isoformat(),
166
+ "metrics": metrics,
167
+ "session_responses": [
168
+ {
169
+ "item_id": r.item_id,
170
+ "skill": r.skill,
171
+ "correct": r.correct,
172
+ "response_time": r.response_time,
173
+ "hints_used": r.hints_used,
174
+ "difficulty": r.difficulty,
175
+ "timestamp": r.timestamp.isoformat()
176
+ }
177
+ for r in tutor.session_responses
178
+ ]
179
+ }
180
+
181
+ with open("research_output.json", "w") as f:
182
+ json.dump(research_data, f, indent=2)
183
+
184
+ print("Research data saved to 'research_output.json'")
185
+ print()
186
+
187
+ print("=== Test Complete ===")
188
+ print("RAG-enhanced Cognitive Tutor is ready for deployment!")
189
+
190
+ if __name__ == "__main__":
191
+ test_rag_enhanced_tutor()