Spaces:
Sleeping
Sleeping
Commit
·
cd8c2bb
1
Parent(s):
f6c853c
Push existing cognitive tutor project
Browse files- README.md +79 -12
- __pycache__/cognitive_llm.cpython-313.pyc +0 -0
- cog_tutor/__init__.py +3 -0
- cog_tutor/__pycache__/__init__.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/adaptive_tutor.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/cache.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/inference.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/knowledge_tracing.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/prompts.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/schemas.cpython-313.pyc +0 -0
- cog_tutor/__pycache__/validation.cpython-313.pyc +0 -0
- cog_tutor/adapters/__init__.py +2 -0
- cog_tutor/adapters/__pycache__/__init__.cpython-313.pyc +0 -0
- cog_tutor/adapters/__pycache__/qwen_adapter.cpython-313.pyc +0 -0
- cog_tutor/adapters/qwen_adapter.py +46 -0
- cog_tutor/adaptive_tutor.py +316 -0
- cog_tutor/cache.py +25 -0
- cog_tutor/inference.py +144 -0
- cog_tutor/knowledge_tracing.py +311 -0
- cog_tutor/prompts.py +60 -0
- cog_tutor/rag/__init__.py +5 -0
- cog_tutor/rag/__pycache__/__init__.cpython-313.pyc +0 -0
- cog_tutor/rag/__pycache__/knowledge_base.cpython-313.pyc +0 -0
- cog_tutor/rag/__pycache__/rag_prompts.cpython-313.pyc +0 -0
- cog_tutor/rag/__pycache__/retriever.cpython-313.pyc +0 -0
- cog_tutor/rag/knowledge_base.py +173 -0
- cog_tutor/rag/rag_prompts.py +88 -0
- cog_tutor/rag/retriever.py +118 -0
- cog_tutor/schemas.py +107 -0
- cog_tutor/validation.py +10 -0
- cognitive_llm.py +117 -0
- knowledge_base.sqlite +0 -0
- knowledge_tracing.sqlite +0 -0
- requirements.txt +6 -0
- research_output.json +89 -0
- test_cog_tutor.py +11 -0
- test_rag_tutor.py +191 -0
README.md
CHANGED
|
@@ -1,12 +1,79 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|