Spaces:
Sleeping
Sleeping
Upload 26 files
Browse files- .env +1 -0
- .gradio/certificate.pem +31 -0
- README.md +36 -12
- Untitled-2025-07-21-1300.svg +4 -0
- __pycache__/curriculum.cpython-312.pyc +0 -0
- __pycache__/main.cpython-312.pyc +0 -0
- __pycache__/oai.cpython-312.pyc +0 -0
- __pycache__/streamlit_app.cpython-312.pyc +0 -0
- __pycache__/user_state.cpython-312.pyc +0 -0
- agents/__pycache__/base_agent.cpython-312.pyc +0 -0
- agents/__pycache__/bloom_assess.cpython-312.pyc +0 -0
- agents/__pycache__/coordinator.cpython-312.pyc +0 -0
- agents/__pycache__/curriculum_planner.cpython-312.pyc +0 -0
- agents/__pycache__/level_assess.cpython-312.pyc +0 -0
- agents/__pycache__/teacher.cpython-312.pyc +0 -0
- agents/base_agent.py +6 -0
- agents/bloom_assess.py +218 -0
- agents/coordinator.py +106 -0
- agents/curriculum_planner.py +88 -0
- agents/level_assess.py +247 -0
- agents/teacher.py +238 -0
- app.py +951 -0
- curriculum.py +41 -0
- main.py +162 -0
- requirements.txt +4 -0
- user_state.py +119 -0
.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY=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
|
.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
| 3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
| 4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
| 5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
| 6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
| 7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
| 8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
| 9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
| 10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
| 11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
| 12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
| 13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
| 14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
| 15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
| 16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
| 17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
| 18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
| 19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
| 20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
| 21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
| 22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
| 23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
| 24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
| 25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
| 26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
| 27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
| 28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
| 29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
| 30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
| 31 |
+
-----END CERTIFICATE-----
|
README.md
CHANGED
|
@@ -1,12 +1,36 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned:
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AI Teacher Bot
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "3.44"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: true
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# AI Teacher Bot
|
| 13 |
+
|
| 14 |
+
**Interactive Personalized Learning Platform**
|
| 15 |
+
|
| 16 |
+
AI Teacher Bot is an interactive learning platform that:
|
| 17 |
+
- Teaches modules with detailed explanations adapted to the user's learning level (Novice, Intermediate, Advanced)
|
| 18 |
+
- Evaluates student examples and provides constructive feedback
|
| 19 |
+
- Performs Bloom’s Taxonomy assessments per chapter to measure understanding
|
| 20 |
+
- Generates structured, level-appropriate curriculum automatically
|
| 21 |
+
|
| 22 |
+
**Usage**
|
| 23 |
+
1. Enter a topic and select your claimed level.
|
| 24 |
+
2. Click “Start Session” to begin learning.
|
| 25 |
+
3. Follow module explanations and submit examples.
|
| 26 |
+
4. Complete Bloom assessments to evaluate mastery.
|
| 27 |
+
|
| 28 |
+
**Tech Stack**
|
| 29 |
+
- Python 3.12
|
| 30 |
+
- Gradio (for UI)
|
| 31 |
+
- OpenAI API (GPT-4o-mini)
|
| 32 |
+
|
| 33 |
+
**Notes**
|
| 34 |
+
- Ensure you have a valid OpenAI API key set in `.env`.
|
| 35 |
+
- For testing and temporary deployment, Gradio provides shareable links.
|
| 36 |
+
- For permanent hosting, this app runs on Hugging Face Spaces.
|
Untitled-2025-07-21-1300.svg
ADDED
|
|
__pycache__/curriculum.cpython-312.pyc
ADDED
|
Binary file (3.45 kB). View file
|
|
|
__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (7.17 kB). View file
|
|
|
__pycache__/oai.cpython-312.pyc
ADDED
|
Binary file (803 Bytes). View file
|
|
|
__pycache__/streamlit_app.cpython-312.pyc
ADDED
|
Binary file (56 kB). View file
|
|
|
__pycache__/user_state.cpython-312.pyc
ADDED
|
Binary file (6.85 kB). View file
|
|
|
agents/__pycache__/base_agent.cpython-312.pyc
ADDED
|
Binary file (699 Bytes). View file
|
|
|
agents/__pycache__/bloom_assess.cpython-312.pyc
ADDED
|
Binary file (9.94 kB). View file
|
|
|
agents/__pycache__/coordinator.cpython-312.pyc
ADDED
|
Binary file (6.16 kB). View file
|
|
|
agents/__pycache__/curriculum_planner.cpython-312.pyc
ADDED
|
Binary file (4.65 kB). View file
|
|
|
agents/__pycache__/level_assess.cpython-312.pyc
ADDED
|
Binary file (13.8 kB). View file
|
|
|
agents/__pycache__/teacher.cpython-312.pyc
ADDED
|
Binary file (11.7 kB). View file
|
|
|
agents/base_agent.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class BaseAgent:
|
| 2 |
+
def __init__(self, name):
|
| 3 |
+
self.name = name
|
| 4 |
+
|
| 5 |
+
def process(self, *args, **kwargs):
|
| 6 |
+
raise NotImplementedError("Each agent must implement the process method.")
|
agents/bloom_assess.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import openai
|
| 4 |
+
import json
|
| 5 |
+
from .base_agent import BaseAgent
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
class BloomsAssessmentAgent(BaseAgent):
|
| 10 |
+
def __init__(self):
|
| 11 |
+
super().__init__("BloomsAssessmentAgent")
|
| 12 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 13 |
+
self.client = openai.OpenAI(api_key=api_key)
|
| 14 |
+
self.bloom_levels = [
|
| 15 |
+
"Remembering", "Understanding", "Applying",
|
| 16 |
+
"Analyzing", "Evaluating", "Creating"
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
def generate_bloom_question(self, chapter, bloom_level):
|
| 20 |
+
system_prompt = (
|
| 21 |
+
f"You are an expert educator creating a {bloom_level} level question according to Bloom's Taxonomy. "
|
| 22 |
+
f"Generate ONE question that tests the student's ability at the {bloom_level} level for the given chapter. "
|
| 23 |
+
"The question should be clear, concise, and appropriate for the chapter content. "
|
| 24 |
+
"Return ONLY the question text, no additional formatting or explanation."
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
user_prompt = (
|
| 28 |
+
f"Chapter: {chapter.name}\n"
|
| 29 |
+
f"Modules in this chapter: {[m.name for m in chapter.modules]}\n"
|
| 30 |
+
f"Bloom's Level: {bloom_level}\n"
|
| 31 |
+
f"Generate a {bloom_level} level question."
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
response = self.client.chat.completions.create(
|
| 35 |
+
model="gpt-4o-mini",
|
| 36 |
+
messages=[
|
| 37 |
+
{"role": "system", "content": system_prompt},
|
| 38 |
+
{"role": "user", "content": user_prompt}
|
| 39 |
+
]
|
| 40 |
+
)
|
| 41 |
+
return response.choices[0].message.content.strip()
|
| 42 |
+
|
| 43 |
+
def evaluate_bloom_answer(self, question, user_answer, bloom_level, chapter):
|
| 44 |
+
system_prompt = (
|
| 45 |
+
f"You are an expert educator evaluating a student's answer for a {bloom_level} level question. "
|
| 46 |
+
"Evaluate the answer based on the specific cognitive skills required for this Bloom's level. "
|
| 47 |
+
"Be STRICT about answer quality - vague, incomplete, or overly brief answers should receive low scores.\n\n"
|
| 48 |
+
"**Evaluation Criteria:**\n"
|
| 49 |
+
"- **Precision:** Is the answer specific and detailed enough for the Bloom's level?\n"
|
| 50 |
+
"- **Relevance:** Does it directly address the question asked?\n"
|
| 51 |
+
"- **Depth:** Does it demonstrate the expected cognitive complexity?\n"
|
| 52 |
+
"- **Completeness:** Are all parts of the question addressed?\n\n"
|
| 53 |
+
"**Automatic Penalties:**\n"
|
| 54 |
+
"- Answers under 15 words: Maximum score of 3\n"
|
| 55 |
+
"- Vague responses (yes/no, maybe, I think): Maximum score of 2\n"
|
| 56 |
+
"- Off-topic or irrelevant answers: Score of 0-1\n\n"
|
| 57 |
+
"You must respond with a JSON object containing:\n"
|
| 58 |
+
"1. 'score': A number between 0 and 10\n"
|
| 59 |
+
"2. 'feedback': Detailed explanation of the score and what was missing\n"
|
| 60 |
+
"3. 'level_achieved': The Bloom's level the answer demonstrates\n"
|
| 61 |
+
"4. 'hint': A subtle hint to guide toward better understanding (if score < 7)"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
user_prompt = (
|
| 65 |
+
f"Question (Bloom's Level: {bloom_level}): {question}\n"
|
| 66 |
+
f"Student's Answer: {user_answer}\n"
|
| 67 |
+
f"Chapter Context: {chapter.name}\n"
|
| 68 |
+
"Evaluate this answer and provide the JSON response."
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
response = self.client.chat.completions.create(
|
| 72 |
+
model="gpt-4o-mini",
|
| 73 |
+
messages=[
|
| 74 |
+
{"role": "system", "content": system_prompt},
|
| 75 |
+
{"role": "user", "content": user_prompt}
|
| 76 |
+
]
|
| 77 |
+
)
|
| 78 |
+
return response.choices[0].message.content.strip()
|
| 79 |
+
|
| 80 |
+
def process(self, chapter):
|
| 81 |
+
print(f"\n{'='*60}")
|
| 82 |
+
print(f"BLOOM'S TAXONOMY ASSESSMENT")
|
| 83 |
+
print(f"Chapter: {chapter.name}")
|
| 84 |
+
print(f"{'='*60}")
|
| 85 |
+
print("You will be asked 2 questions for each of the 6 Bloom's levels.")
|
| 86 |
+
print("This comprehensive assessment will evaluate your mastery of the chapter.")
|
| 87 |
+
|
| 88 |
+
results = {}
|
| 89 |
+
total_score = 0
|
| 90 |
+
questions_asked = 0
|
| 91 |
+
|
| 92 |
+
for bloom_level in self.bloom_levels:
|
| 93 |
+
level_scores = []
|
| 94 |
+
print(f"\n--- {bloom_level.upper()} LEVEL (2 Questions) ---")
|
| 95 |
+
|
| 96 |
+
for question_num in range(1, 3): # 2 questions per level
|
| 97 |
+
print(f"\n[Question {question_num}/2 for {bloom_level}]")
|
| 98 |
+
|
| 99 |
+
question = self.generate_bloom_question(chapter, bloom_level)
|
| 100 |
+
print(f"Question: {question}")
|
| 101 |
+
|
| 102 |
+
user_answer = input("Your answer: ")
|
| 103 |
+
|
| 104 |
+
evaluation_json = self.evaluate_bloom_answer(question, user_answer, bloom_level, chapter)
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
evaluation = json.loads(evaluation_json)
|
| 108 |
+
score = evaluation.get('score', 0)
|
| 109 |
+
feedback = evaluation.get('feedback', 'No feedback provided')
|
| 110 |
+
level_achieved = evaluation.get('level_achieved', bloom_level)
|
| 111 |
+
hint = evaluation.get('hint', '')
|
| 112 |
+
|
| 113 |
+
print(f"Score: {score}/10")
|
| 114 |
+
print(f"Feedback: {feedback}")
|
| 115 |
+
print(f"Level Demonstrated: {level_achieved}")
|
| 116 |
+
|
| 117 |
+
# If score is low, provide hint and offer retry
|
| 118 |
+
if score < 7 and hint:
|
| 119 |
+
print(f"💡 Hint: {hint}")
|
| 120 |
+
|
| 121 |
+
retry = input("\nYour answer needs improvement. Would you like to try again? (y/n): ").lower().strip()
|
| 122 |
+
if retry == 'y':
|
| 123 |
+
print("\n🔄 Please provide a more detailed and specific answer.")
|
| 124 |
+
retry_answer = input("Your revised answer: ")
|
| 125 |
+
|
| 126 |
+
# Re-evaluate the retry answer
|
| 127 |
+
retry_evaluation_json = self.evaluate_bloom_answer(question, retry_answer, bloom_level, chapter)
|
| 128 |
+
try:
|
| 129 |
+
retry_evaluation = json.loads(retry_evaluation_json)
|
| 130 |
+
retry_score = retry_evaluation.get('score', 0)
|
| 131 |
+
retry_feedback = retry_evaluation.get('feedback', 'No feedback provided')
|
| 132 |
+
retry_level = retry_evaluation.get('level_achieved', bloom_level)
|
| 133 |
+
|
| 134 |
+
print(f"\nRetry Score: {retry_score}/10")
|
| 135 |
+
print(f"Retry Feedback: {retry_feedback}")
|
| 136 |
+
print(f"Retry Level Demonstrated: {retry_level}")
|
| 137 |
+
|
| 138 |
+
# Use the better of the two scores
|
| 139 |
+
final_score = max(score, retry_score)
|
| 140 |
+
level_scores.append(final_score)
|
| 141 |
+
total_score += final_score
|
| 142 |
+
questions_asked += 1
|
| 143 |
+
|
| 144 |
+
except json.JSONDecodeError:
|
| 145 |
+
print("Error evaluating retry answer. Using original score.")
|
| 146 |
+
level_scores.append(score)
|
| 147 |
+
total_score += score
|
| 148 |
+
questions_asked += 1
|
| 149 |
+
else:
|
| 150 |
+
level_scores.append(score)
|
| 151 |
+
total_score += score
|
| 152 |
+
questions_asked += 1
|
| 153 |
+
else:
|
| 154 |
+
level_scores.append(score)
|
| 155 |
+
total_score += score
|
| 156 |
+
questions_asked += 1
|
| 157 |
+
|
| 158 |
+
except json.JSONDecodeError:
|
| 159 |
+
print("Error evaluating answer. Defaulting to score 5.")
|
| 160 |
+
level_scores.append(5)
|
| 161 |
+
total_score += 5
|
| 162 |
+
questions_asked += 1
|
| 163 |
+
|
| 164 |
+
# Store results for this Bloom's level
|
| 165 |
+
results[bloom_level] = {
|
| 166 |
+
'scores': level_scores,
|
| 167 |
+
'average_score': sum(level_scores) / len(level_scores),
|
| 168 |
+
'total_score': sum(level_scores)
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
# Calculate overall results
|
| 172 |
+
max_possible = questions_asked * 10
|
| 173 |
+
average_score = total_score / questions_asked
|
| 174 |
+
percentage = (total_score / max_possible) * 100
|
| 175 |
+
|
| 176 |
+
print(f"\n{'='*60}")
|
| 177 |
+
print("COMPREHENSIVE ASSESSMENT RESULTS")
|
| 178 |
+
print(f"{'='*60}")
|
| 179 |
+
print(f"Total Score: {total_score}/{max_possible}")
|
| 180 |
+
print(f"Average Score: {average_score:.1f}/10")
|
| 181 |
+
print(f"Percentage: {percentage:.1f}%")
|
| 182 |
+
|
| 183 |
+
# Enhanced mastery determination
|
| 184 |
+
if percentage >= 85:
|
| 185 |
+
mastery = "Excellent Mastery"
|
| 186 |
+
recommendation = "Ready to advance to next level"
|
| 187 |
+
elif percentage >= 70:
|
| 188 |
+
mastery = "Good Understanding"
|
| 189 |
+
recommendation = "Ready to advance with some review"
|
| 190 |
+
elif percentage >= 55:
|
| 191 |
+
mastery = "Basic Understanding"
|
| 192 |
+
recommendation = "Review weak areas before advancing"
|
| 193 |
+
else:
|
| 194 |
+
mastery = "Needs Significant Review"
|
| 195 |
+
recommendation = "Revisit chapter content and retake assessment"
|
| 196 |
+
|
| 197 |
+
print(f"Mastery Level: {mastery}")
|
| 198 |
+
print(f"Recommendation: {recommendation}")
|
| 199 |
+
|
| 200 |
+
print(f"\nDetailed Breakdown by Bloom's Level:")
|
| 201 |
+
for level, result in results.items():
|
| 202 |
+
avg = result['average_score']
|
| 203 |
+
print(f" {level}: {avg:.1f}/10 (Scores: {result['scores']})")
|
| 204 |
+
|
| 205 |
+
# Determine if student should advance or review
|
| 206 |
+
should_advance = percentage >= 70
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
'chapter': chapter.name,
|
| 210 |
+
'total_score': total_score,
|
| 211 |
+
'max_possible': max_possible,
|
| 212 |
+
'average_score': average_score,
|
| 213 |
+
'percentage': percentage,
|
| 214 |
+
'mastery_level': mastery,
|
| 215 |
+
'recommendation': recommendation,
|
| 216 |
+
'should_advance': should_advance,
|
| 217 |
+
'detailed_results': results
|
| 218 |
+
}
|
agents/coordinator.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base_agent import BaseAgent
|
| 2 |
+
from .teacher import NoviceTeacherAgent, IntermediateTeacherAgent, AdvancedTeacherAgent
|
| 3 |
+
from .bloom_assess import BloomsAssessmentAgent
|
| 4 |
+
from curriculum import Chapter, Module
|
| 5 |
+
|
| 6 |
+
class CoordinatorAgent(BaseAgent):
|
| 7 |
+
def __init__(self, user_level, user_state):
|
| 8 |
+
super().__init__("CoordinatorAgent")
|
| 9 |
+
if user_level == "novice":
|
| 10 |
+
self.teacher = NoviceTeacherAgent()
|
| 11 |
+
elif user_level == "intermediate":
|
| 12 |
+
self.teacher = IntermediateTeacherAgent()
|
| 13 |
+
else:
|
| 14 |
+
self.teacher = AdvancedTeacherAgent()
|
| 15 |
+
|
| 16 |
+
self.bloom_assessor = BloomsAssessmentAgent()
|
| 17 |
+
self.user_state = user_state
|
| 18 |
+
|
| 19 |
+
def teach_curriculum(self, curriculum):
|
| 20 |
+
print("\n[System] Starting the teaching session!")
|
| 21 |
+
print(self.user_state.get_progress_summary(curriculum))
|
| 22 |
+
|
| 23 |
+
for chapter_idx, chapter in enumerate(curriculum.chapters, 1):
|
| 24 |
+
print(f"\n{'='*60}")
|
| 25 |
+
print(f"CHAPTER {chapter_idx}: {chapter.name}")
|
| 26 |
+
print(f"{'='*60}")
|
| 27 |
+
|
| 28 |
+
# Teach all modules in the chapter
|
| 29 |
+
for module_idx, module in enumerate(chapter.modules, 1):
|
| 30 |
+
while True:
|
| 31 |
+
print(f"\n--- Module {chapter_idx}.{module_idx}: {module.name} ---")
|
| 32 |
+
# Display the teaching content
|
| 33 |
+
explanation = self.teacher.teach_module(module)
|
| 34 |
+
print(f"\n📚 {explanation}")
|
| 35 |
+
if self.teacher.check_example(module):
|
| 36 |
+
print("[System] Good job! Moving to next module.")
|
| 37 |
+
# Update progress
|
| 38 |
+
self.user_state.update_module_progress(chapter.name, module.name, "completed")
|
| 39 |
+
break
|
| 40 |
+
else:
|
| 41 |
+
print("[System] Let's review this module again.")
|
| 42 |
+
self.user_state.update_module_progress(chapter.name, module.name, "reviewed")
|
| 43 |
+
|
| 44 |
+
# Chapter completed - run Bloom's assessment
|
| 45 |
+
print(f"\n[System] Chapter '{chapter.name}' completed! Time for a comprehensive assessment.")
|
| 46 |
+
assessment_results = self.bloom_assessor.process(chapter)
|
| 47 |
+
|
| 48 |
+
# Check if student should advance or review based on Bloom's assessment
|
| 49 |
+
should_advance = assessment_results.get('should_advance', False)
|
| 50 |
+
percentage = assessment_results.get('percentage', 0)
|
| 51 |
+
|
| 52 |
+
if should_advance:
|
| 53 |
+
print(f"\n🎉 Excellent work! You scored {percentage:.1f}% and demonstrated mastery.")
|
| 54 |
+
print("✅ You're ready to advance to the next chapter!")
|
| 55 |
+
|
| 56 |
+
# Update progress with assessment results
|
| 57 |
+
self.user_state.update_bloom_assessment(chapter.name, assessment_results)
|
| 58 |
+
self.user_state.update_chapter_progress(chapter.name, "completed")
|
| 59 |
+
|
| 60 |
+
# Show updated progress
|
| 61 |
+
print("\n" + self.user_state.get_progress_summary(curriculum))
|
| 62 |
+
|
| 63 |
+
# Ask if user wants to continue
|
| 64 |
+
if chapter_idx < len(curriculum.chapters):
|
| 65 |
+
continue_choice = input("\nContinue to next chapter? (y/n): ").lower().strip()
|
| 66 |
+
if continue_choice != 'y':
|
| 67 |
+
print("[System] Progress updated. You can resume later.")
|
| 68 |
+
return
|
| 69 |
+
else:
|
| 70 |
+
print(f"\n{'='*60}")
|
| 71 |
+
print("CONGRATULATIONS!")
|
| 72 |
+
print(f"{'='*60}")
|
| 73 |
+
print("You have completed the entire curriculum!")
|
| 74 |
+
print(self.user_state.get_progress_summary(curriculum))
|
| 75 |
+
else:
|
| 76 |
+
print(f"\n📚 You scored {percentage:.1f}% on the assessment.")
|
| 77 |
+
print("🔄 Based on your performance, let's review this chapter to strengthen your understanding.")
|
| 78 |
+
|
| 79 |
+
# Update progress but mark chapter as needing review
|
| 80 |
+
self.user_state.update_bloom_assessment(chapter.name, assessment_results)
|
| 81 |
+
self.user_state.update_chapter_progress(chapter.name, "needs_review")
|
| 82 |
+
|
| 83 |
+
# Ask if user wants to retake the chapter or continue anyway
|
| 84 |
+
review_choice = input("\nWould you like to:\n1. Review this chapter again (r)\n2. Continue to next chapter anyway (c)\n3. Save progress and exit (s)\nChoice: ").lower().strip()
|
| 85 |
+
|
| 86 |
+
if review_choice == 'r':
|
| 87 |
+
print(f"\n🔄 Let's review Chapter {chapter_idx}: {chapter.name}")
|
| 88 |
+
# Restart the chapter
|
| 89 |
+
chapter_idx -= 1 # Will be incremented at the end of the loop
|
| 90 |
+
elif review_choice == 'c':
|
| 91 |
+
print("⚠️ Continuing to next chapter. Consider reviewing weak areas later.")
|
| 92 |
+
if chapter_idx < len(curriculum.chapters):
|
| 93 |
+
continue_choice = input("\nContinue to next chapter? (y/n): ").lower().strip()
|
| 94 |
+
if continue_choice != 'y':
|
| 95 |
+
print("[System] Progress updated. You can resume later.")
|
| 96 |
+
return
|
| 97 |
+
else:
|
| 98 |
+
print(f"\n{'='*60}")
|
| 99 |
+
print("CURRICULUM COMPLETED!")
|
| 100 |
+
print(f"{'='*60}")
|
| 101 |
+
print("You have completed the entire curriculum!")
|
| 102 |
+
print("Note: Some chapters may need review for better mastery.")
|
| 103 |
+
print(self.user_state.get_progress_summary(curriculum))
|
| 104 |
+
else: # exit
|
| 105 |
+
print("[System] Progress updated. You can resume later.")
|
| 106 |
+
return
|
agents/curriculum_planner.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import openai
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
from .base_agent import BaseAgent
|
| 7 |
+
from curriculum import Curriculum, Chapter, Module
|
| 8 |
+
|
| 9 |
+
class CurriculumPlannerAgent(BaseAgent):
|
| 10 |
+
def __init__(self):
|
| 11 |
+
super().__init__("CurriculumPlannerAgent")
|
| 12 |
+
load_dotenv()
|
| 13 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 14 |
+
self.client = openai.OpenAI(api_key=api_key)
|
| 15 |
+
|
| 16 |
+
def process(self, topic, level):
|
| 17 |
+
system_prompt = (
|
| 18 |
+
"You are an Expert Instructional Architect. Design a detailed, academically rigorous curriculum.\n\n"
|
| 19 |
+
|
| 20 |
+
"### Rules:\n"
|
| 21 |
+
"1. Ensure academic depth and avoid trivial content.\n"
|
| 22 |
+
"2. Respect the level:\n"
|
| 23 |
+
"- Novice: fundamentals, 4–6 modules per chapter.\n"
|
| 24 |
+
"- Intermediate: problem-solving & applications, 5–7 modules per chapter.\n"
|
| 25 |
+
"- Advanced: theory, research, edge cases, 6–8 modules per chapter.\n"
|
| 26 |
+
"3. Each module must have:\n"
|
| 27 |
+
"- `module_name`\n"
|
| 28 |
+
"- `learning_objective` (with knowledge, skills, applications, examples)\n"
|
| 29 |
+
"4. Use progressive complexity.\n\n"
|
| 30 |
+
|
| 31 |
+
"### Example Curriculum (Novice, Topic: Python Programming)\n"
|
| 32 |
+
"{\n"
|
| 33 |
+
" \"chapters\": [\n"
|
| 34 |
+
" {\n"
|
| 35 |
+
" \"chapter_name\": \"Introduction to Python\",\n"
|
| 36 |
+
" \"modules\": [\n"
|
| 37 |
+
" {\"module_name\": \"What is Python?\", \"learning_objective\": \"Understand Python’s role as a programming language, its history, and its everyday applications.\"},\n"
|
| 38 |
+
" {\"module_name\": \"Setting Up Python\", \"learning_objective\": \"Learn how to install Python and write your first basic script.\"},\n"
|
| 39 |
+
" {\"module_name\": \"Variables and Data Types\", \"learning_objective\": \"Understand how to store data in variables and use types such as strings, numbers, and booleans.\"}\n"
|
| 40 |
+
" ]\n"
|
| 41 |
+
" },\n"
|
| 42 |
+
" {\n"
|
| 43 |
+
" \"chapter_name\": \"Control Structures\",\n"
|
| 44 |
+
" \"modules\": [\n"
|
| 45 |
+
" {\"module_name\": \"If Statements\", \"learning_objective\": \"Learn decision-making in programs with if/else statements and simple examples.\"},\n"
|
| 46 |
+
" {\"module_name\": \"Loops\", \"learning_objective\": \"Understand repetition using for and while loops with practical use cases.\"}\n"
|
| 47 |
+
" ]\n"
|
| 48 |
+
" }\n"
|
| 49 |
+
" ]\n"
|
| 50 |
+
"}\n\n"
|
| 51 |
+
|
| 52 |
+
"### Instructions:\n"
|
| 53 |
+
"- Follow the same style for the requested topic.\n"
|
| 54 |
+
"- Do NOT output explanations outside JSON.\n"
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
user_prompt = f"Generate a curriculum for:\nTopic: {topic}\nLevel: {level}"
|
| 58 |
+
|
| 59 |
+
response = self.client.chat.completions.create(
|
| 60 |
+
model="gpt-4o-mini",
|
| 61 |
+
response_format={"type": "json_object"},
|
| 62 |
+
messages=[
|
| 63 |
+
{"role": "system", "content": system_prompt},
|
| 64 |
+
{"role": "user", "content": user_prompt}
|
| 65 |
+
]
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
curriculum_json = response.choices[0].message.content.strip()
|
| 70 |
+
match = re.search(r'\{.*\}', curriculum_json, re.DOTALL)
|
| 71 |
+
if match:
|
| 72 |
+
json_str = match.group(0)
|
| 73 |
+
else:
|
| 74 |
+
raise ValueError("No JSON found in the response")
|
| 75 |
+
|
| 76 |
+
chapters_obj = json.loads(json_str)
|
| 77 |
+
chapters_data = chapters_obj["chapters"]
|
| 78 |
+
|
| 79 |
+
chapters = []
|
| 80 |
+
for ch in chapters_data:
|
| 81 |
+
modules = [Module(m["module_name"], m["learning_objective"]) for m in ch["modules"]]
|
| 82 |
+
chapters.append(Chapter(ch["chapter_name"], modules))
|
| 83 |
+
|
| 84 |
+
return Curriculum(topic, chapters)
|
| 85 |
+
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print("[CurriculumPlannerAgent] Error parsing curriculum:", e)
|
| 88 |
+
return Curriculum(topic, [Chapter("General Introduction", [Module("Overview")])])
|
agents/level_assess.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .base_agent import BaseAgent
|
| 2 |
+
import os
|
| 3 |
+
import openai
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
class LevelAssessmentAgent(BaseAgent):
|
| 8 |
+
def __init__(self):
|
| 9 |
+
super().__init__("LevelAssessmentAgent")
|
| 10 |
+
load_dotenv()
|
| 11 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 12 |
+
self.client = openai.OpenAI(api_key=api_key)
|
| 13 |
+
|
| 14 |
+
def generate_questions(self, topic, level):
|
| 15 |
+
system_prompt = (
|
| 16 |
+
"You are a Master Educator and Curriculum Designer with deep expertise in pedagogical theory and assessment creation. "
|
| 17 |
+
"Your primary goal is to design a short, insightful assessment to precisely gauge a student's mastery of a topic at a specific proficiency level.\n\n"
|
| 18 |
+
"**Your Guiding Principles:**\n\n"
|
| 19 |
+
"1. **Cognitive Depth:** The questions must go beyond rote memorization. They should probe the student's reasoning, problem-solving abilities, and their ability to connect concepts.\n"
|
| 20 |
+
"2. **Level-Specific Targeting:** You must strictly adhere to the specified 'level':\n"
|
| 21 |
+
" - **Beginner:** Focus on core concepts, definitions, and simple \"how\" or \"why\" explanations. (Testing Comprehension & Application)\n"
|
| 22 |
+
" - **Intermediate:** Focus on comparing/contrasting concepts, applying knowledge to simple scenarios, and analyzing processes. (Testing Application & Analysis)\n"
|
| 23 |
+
" - **Advanced:** Demand synthesis of multiple concepts, evaluation of complex scenarios, and creation of novel solutions or arguments. (Testing Synthesis & Evaluation)\n"
|
| 24 |
+
"3. **Clarity and Precision:** Each question must be unambiguous, concise, and clearly worded.\n\n"
|
| 25 |
+
"**Output Format:**\n"
|
| 26 |
+
"You must generate exactly 5 to 6 questions. Your response should contain ONLY the numbered list of questions. Do not include a title, introduction, conclusion, or any other text."
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
user_prompt = (
|
| 30 |
+
f"Generate a assessment for 5 questions to test if a student is truly at the '{level}' level in the topic '{topic}'. "
|
| 31 |
+
"The question should be open-ended and require a thoughtful answer."
|
| 32 |
+
)
|
| 33 |
+
response = self.client.chat.completions.create(model = "gpt-4o-mini",
|
| 34 |
+
messages=[
|
| 35 |
+
{"role": "system", "content": system_prompt},
|
| 36 |
+
{"role": "user", "content": user_prompt}
|
| 37 |
+
]
|
| 38 |
+
)
|
| 39 |
+
return response.choices[0].message.content.strip()
|
| 40 |
+
|
| 41 |
+
def evaluate_answer(self, topic, level, question, user_answer):
|
| 42 |
+
system_prompt = (
|
| 43 |
+
"You are a meticulous and impartial Grader AI. Your task is to evaluate a student's answer based on a provided question and the student's claimed proficiency level. You must adhere to the following STRICT rubric:\n\n"
|
| 44 |
+
"1. **Precision Requirement:** The answer must be specific, detailed, and demonstrate clear understanding. Vague, generic, or one-word answers are automatically INCORRECT.\n"
|
| 45 |
+
"2. **Correctness:** Is the answer factually accurate and relevant to the question?\n"
|
| 46 |
+
"3. **Depth:** Does the answer's depth match the expected level?\n"
|
| 47 |
+
" - **Novice/Beginner:** The answer should demonstrate basic comprehension and recall of key concepts with specific examples.\n"
|
| 48 |
+
" - **Intermediate:** The answer should show an ability to apply concepts, compare, and analyze with clear reasoning.\n"
|
| 49 |
+
" - **Advanced:** The answer must demonstrate synthesis, evaluation, and nuanced understanding with sophisticated insights.\n"
|
| 50 |
+
"4. **Completeness:** The answer must address all parts of the question comprehensively.\n\n"
|
| 51 |
+
"**AUTOMATIC FAILURES:**\n"
|
| 52 |
+
"- Answers under 20 words are automatically incorrect\n"
|
| 53 |
+
"- Vague responses like 'yes', 'no', 'it depends', 'I think so', 'maybe', 'probably'\n"
|
| 54 |
+
"- Responses that don't directly address the specific question asked\n"
|
| 55 |
+
"- Copy-paste definitions without personal understanding or application\n\n"
|
| 56 |
+
"You must respond ONLY with a JSON object. Do not include any other text or markdown formatting. The JSON object must have three keys:\n"
|
| 57 |
+
"1. `\"evaluation\"`: a string with a value of either `\"correct\"` or `\"incorrect\"`.\n"
|
| 58 |
+
"2. `\"reasoning\"`: a detailed explanation for your decision, including what was missing or incorrect.\n"
|
| 59 |
+
"3. `\"hint\"`: a subtle hint to guide the student toward the correct answer (only if incorrect)."
|
| 60 |
+
)
|
| 61 |
+
user_prompt = (
|
| 62 |
+
f"Evaluate the following data based on the rubric. Provide your response in the required JSON format.\n\n"
|
| 63 |
+
f"**Topic:** \"{topic}\"\n"
|
| 64 |
+
f"**Proficiency Level to Evaluate:** \"{level}\"\n"
|
| 65 |
+
f"**Question:** \"{question}\"\n"
|
| 66 |
+
f"**Student's Answer:** \"{user_answer}\""
|
| 67 |
+
)
|
| 68 |
+
response = self.client.chat.completions.create(
|
| 69 |
+
model="gpt-4o-mini",
|
| 70 |
+
messages=[
|
| 71 |
+
{"role": "system", "content": system_prompt},
|
| 72 |
+
{"role": "user", "content": user_prompt}
|
| 73 |
+
]
|
| 74 |
+
)
|
| 75 |
+
return response.choices[0].message.content.strip()
|
| 76 |
+
|
| 77 |
+
def challenge_feedback(self, topic, level, question, user_answer, original_evaluation, original_reasoning):
|
| 78 |
+
"""
|
| 79 |
+
Allows a student to challenge the feedback given. Re-evaluates the answer
|
| 80 |
+
with a focus on being fair and reconsidering the evaluation.
|
| 81 |
+
Returns a JSON string with the challenge result.
|
| 82 |
+
"""
|
| 83 |
+
system_prompt = (
|
| 84 |
+
"You are a fair and reconsidering Grader AI. A student has challenged your previous evaluation. "
|
| 85 |
+
"Your task is to carefully re-examine the student's answer with fresh eyes, considering that "
|
| 86 |
+
"your initial evaluation may have been too strict or may have missed valid points.\n\n"
|
| 87 |
+
"**Re-evaluation Guidelines:**\n"
|
| 88 |
+
"1. Be open to reconsidering your initial assessment\n"
|
| 89 |
+
"2. Look for valid points in the answer that may have been overlooked\n"
|
| 90 |
+
"3. Consider alternative interpretations of the question\n"
|
| 91 |
+
"4. If the answer demonstrates understanding (even if not perfect), acknowledge it\n"
|
| 92 |
+
"5. Be fair and balanced in your re-evaluation\n\n"
|
| 93 |
+
"You must respond ONLY with a JSON object containing:\n"
|
| 94 |
+
"1. `\"evaluation\"`: either `\"correct\"` or `\"incorrect\"`\n"
|
| 95 |
+
"2. `\"reasoning\"`: detailed explanation of your re-evaluation, including whether you changed your mind and why\n"
|
| 96 |
+
"3. `\"original_was_fair\"`: true/false indicating if the original evaluation was fair\n"
|
| 97 |
+
"4. `\"hint\"`: a hint if still incorrect"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
user_prompt = (
|
| 101 |
+
f"**Topic:** {topic}\n"
|
| 102 |
+
f"**Proficiency Level:** {level}\n"
|
| 103 |
+
f"**Question:** {question}\n"
|
| 104 |
+
f"**Student's Answer:** {user_answer}\n\n"
|
| 105 |
+
f"**Original Evaluation:** {original_evaluation}\n"
|
| 106 |
+
f"**Original Reasoning:** {original_reasoning}\n\n"
|
| 107 |
+
"Please re-evaluate this answer fairly. The student believes the original evaluation may have been incorrect."
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
response = self.client.chat.completions.create(
|
| 111 |
+
model="gpt-4o-mini",
|
| 112 |
+
messages=[
|
| 113 |
+
{"role": "system", "content": system_prompt},
|
| 114 |
+
{"role": "user", "content": user_prompt}
|
| 115 |
+
]
|
| 116 |
+
)
|
| 117 |
+
return response.choices[0].message.content.strip()
|
| 118 |
+
|
| 119 |
+
def challenge_discussion(self, topic, level, question, user_answer, original_evaluation, original_reasoning, conversation_history, student_message):
|
| 120 |
+
"""
|
| 121 |
+
Handles a conversational challenge discussion between student and grader.
|
| 122 |
+
conversation_history: list of [user_message, assistant_message] pairs
|
| 123 |
+
student_message: the current student's argument/question
|
| 124 |
+
Returns: assistant response and final evaluation (if discussion is ending)
|
| 125 |
+
"""
|
| 126 |
+
system_prompt = (
|
| 127 |
+
"You are a fair and patient Grader AI engaged in a discussion with a student who is challenging your evaluation. "
|
| 128 |
+
"You should be open to reconsidering your assessment, but also maintain academic standards.\n\n"
|
| 129 |
+
"**Your Role:**\n"
|
| 130 |
+
"1. Listen carefully to the student's arguments\n"
|
| 131 |
+
"2. Be willing to reconsider if the student makes valid points\n"
|
| 132 |
+
"3. Explain your reasoning clearly and respectfully\n"
|
| 133 |
+
"4. If the student is right, acknowledge it and update your evaluation\n"
|
| 134 |
+
"5. If the student's argument doesn't change your assessment, explain why clearly\n\n"
|
| 135 |
+
"**Context:**\n"
|
| 136 |
+
f"- Topic: {topic}\n"
|
| 137 |
+
f"- Level: {level}\n"
|
| 138 |
+
f"- Question: {question}\n"
|
| 139 |
+
f"- Student's Original Answer: {user_answer}\n"
|
| 140 |
+
f"- Original Evaluation: {original_evaluation}\n"
|
| 141 |
+
f"- Original Reasoning: {original_reasoning}\n\n"
|
| 142 |
+
"Respond naturally in a conversational manner. Be helpful and educational, not defensive."
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
messages = [{"role": "system", "content": system_prompt}]
|
| 146 |
+
|
| 147 |
+
# Add conversation history
|
| 148 |
+
for user_msg, assistant_msg in conversation_history:
|
| 149 |
+
messages.append({"role": "user", "content": user_msg})
|
| 150 |
+
messages.append({"role": "assistant", "content": assistant_msg})
|
| 151 |
+
|
| 152 |
+
# Add current student message
|
| 153 |
+
messages.append({"role": "user", "content": student_message})
|
| 154 |
+
|
| 155 |
+
response = self.client.chat.completions.create(
|
| 156 |
+
model="gpt-4o-mini",
|
| 157 |
+
messages=messages
|
| 158 |
+
)
|
| 159 |
+
return response.choices[0].message.content.strip()
|
| 160 |
+
|
| 161 |
+
def process(self, topic, level):
|
| 162 |
+
print(f"\n[Assessment] Let's test your knowledge for the '{level}' level in '{topic}'.")
|
| 163 |
+
print("I will ask you 5 questions.")
|
| 164 |
+
|
| 165 |
+
NUM_QUESTIONS = 5
|
| 166 |
+
# New scoring thresholds based on your requirements
|
| 167 |
+
ADVANCED_PASS = 0.70 # 70% for advanced level
|
| 168 |
+
INTERMEDIATE_PASS = 0.65 # 65% for intermediate level
|
| 169 |
+
MINIMUM_THRESHOLD = 0.30 # 30% minimum to avoid novice
|
| 170 |
+
|
| 171 |
+
all_questions_str = self.generate_questions(topic, level)
|
| 172 |
+
questions = [q.strip() for q in all_questions_str.split('\n') if q.strip()]
|
| 173 |
+
questions = questions[:NUM_QUESTIONS]
|
| 174 |
+
|
| 175 |
+
correct_answers = 0
|
| 176 |
+
per_question_feedback = [] # collect feedback for summary
|
| 177 |
+
|
| 178 |
+
for i, question_text in enumerate(questions):
|
| 179 |
+
current_question = ". ".join(question_text.split('. ')[1:])
|
| 180 |
+
|
| 181 |
+
print(f"\n----------\n[Question {i+1}/{len(questions)}] {current_question}")
|
| 182 |
+
user_answer = input("Your answer: ")
|
| 183 |
+
try:
|
| 184 |
+
evaluation_json_str = self.evaluate_answer(topic, level, current_question, user_answer)
|
| 185 |
+
evaluation_data = json.loads(evaluation_json_str)
|
| 186 |
+
|
| 187 |
+
evaluation = str(evaluation_data.get("evaluation", "incorrect")).lower()
|
| 188 |
+
reasoning = evaluation_data.get("reasoning", "No explanation provided.")
|
| 189 |
+
hint = evaluation_data.get("hint", "")
|
| 190 |
+
|
| 191 |
+
is_correct = evaluation == "correct"
|
| 192 |
+
if is_correct:
|
| 193 |
+
correct_answers += 1
|
| 194 |
+
print(f"[Feedback] {reasoning}")
|
| 195 |
+
print("✅ Correct!")
|
| 196 |
+
else:
|
| 197 |
+
print(f"[Feedback] {reasoning}")
|
| 198 |
+
print("❌ Incorrect.")
|
| 199 |
+
if hint:
|
| 200 |
+
print(f"💡 Hint: {hint}")
|
| 201 |
+
|
| 202 |
+
per_question_feedback.append({
|
| 203 |
+
"question": current_question,
|
| 204 |
+
"correct": is_correct,
|
| 205 |
+
"reason": reasoning,
|
| 206 |
+
"hint": hint,
|
| 207 |
+
})
|
| 208 |
+
|
| 209 |
+
except (json.JSONDecodeError, AttributeError) as e:
|
| 210 |
+
print(f"[System Error] Could not parse the evaluation. Let's skip this one. Error: {e}")
|
| 211 |
+
continue
|
| 212 |
+
|
| 213 |
+
score_percentage = (correct_answers / len(questions)) * 100
|
| 214 |
+
# Print detailed feedback summary
|
| 215 |
+
print("\n----------")
|
| 216 |
+
print("ASSESSMENT FEEDBACK SUMMARY")
|
| 217 |
+
for i, fb in enumerate(per_question_feedback, 1):
|
| 218 |
+
tag = "✅" if fb["correct"] else "❌"
|
| 219 |
+
print(f"{tag} Q{i}: {fb['question']}")
|
| 220 |
+
if fb.get("reason"):
|
| 221 |
+
print(f" Reason: {fb['reason']}")
|
| 222 |
+
if (not fb["correct"]) and fb.get("hint"):
|
| 223 |
+
print(f" Hint: {fb['hint']}")
|
| 224 |
+
|
| 225 |
+
print(f"\n[Assessment Complete] You scored {score_percentage:.1f}% ({correct_answers}/{len(questions)} questions correct).")
|
| 226 |
+
|
| 227 |
+
# Level determination based on claimed level thresholds
|
| 228 |
+
if level == "advanced":
|
| 229 |
+
if score_percentage >= 70:
|
| 230 |
+
print("🎯 Assigned Level: advanced")
|
| 231 |
+
return "advanced"
|
| 232 |
+
elif score_percentage >= 65:
|
| 233 |
+
print("🎯 Assigned Level: intermediate")
|
| 234 |
+
return "intermediate"
|
| 235 |
+
else:
|
| 236 |
+
print("🎯 Assigned Level: novice")
|
| 237 |
+
return "novice"
|
| 238 |
+
elif level == "intermediate":
|
| 239 |
+
if score_percentage >= 65:
|
| 240 |
+
print("🎯 Assigned Level: intermediate")
|
| 241 |
+
return "intermediate"
|
| 242 |
+
else:
|
| 243 |
+
print("🎯 Assigned Level: novice")
|
| 244 |
+
return "novice"
|
| 245 |
+
else:
|
| 246 |
+
print("🎯 Assigned Level: novice")
|
| 247 |
+
return "novice"
|
agents/teacher.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# agents/teacher.py
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
import openai
|
| 7 |
+
from .base_agent import BaseAgent
|
| 8 |
+
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TeacherAgent(BaseAgent):
|
| 13 |
+
def __init__(self, level):
|
| 14 |
+
super().__init__(f"{level.capitalize()}TeacherAgent")
|
| 15 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 16 |
+
self.client = openai.OpenAI(api_key=api_key)
|
| 17 |
+
self.level = level.lower()
|
| 18 |
+
|
| 19 |
+
def teach_module(self, module):
|
| 20 |
+
"""
|
| 21 |
+
Returns a rich, k-shot based explanation string for the given module.
|
| 22 |
+
Will attempt a small re-prompt if the output is too short.
|
| 23 |
+
"""
|
| 24 |
+
system_prompt = (
|
| 25 |
+
"You are an engaging and adaptive AI Tutor. Produce a thorough, structured explanation "
|
| 26 |
+
"of the requested module tuned to the user's learning level. Use the structure below."
|
| 27 |
+
"\n\nTeaching style differences by level:\n"
|
| 28 |
+
"- Novice: Simple language, relatable analogies, many concrete examples.\n"
|
| 29 |
+
"- Intermediate: Explain 'how' and 'why', connect to related concepts, include practical examples.\n"
|
| 30 |
+
"- Advanced: Discuss nuances, edge-cases, performance, comparative methods and deeper insights.\n\n"
|
| 31 |
+
"Explanation structure (MANDATORY):\n"
|
| 32 |
+
"1) Core Concept — clear, precise description (several sentences)\n"
|
| 33 |
+
"2) Worked Example or Analogy — at least one detailed example that illustrates application\n"
|
| 34 |
+
"4) Key Takeaway — one concise sentence\n\n"
|
| 35 |
+
"### Few-shot examples (follow style):\n\n"
|
| 36 |
+
"Novice Example:\n"
|
| 37 |
+
"Module: Variables\n"
|
| 38 |
+
"Core Concept: Variables are containers that store values like numbers or text. Example: x = 5.\n"
|
| 39 |
+
"Analogy/Example: Think of a labeled jar.\n"
|
| 40 |
+
"Intermediate Example:\n"
|
| 41 |
+
"Module: For loops\n"
|
| 42 |
+
"Core Concept: For loops iterate over a collection. Example: for item in collection: process(item)\n"
|
| 43 |
+
"Application: Useful for batch-processing and iteration in algorithms.\n\n"
|
| 44 |
+
"Advanced Example:\n"
|
| 45 |
+
"Module: Tail recursion\n"
|
| 46 |
+
"Core Concept: Tail recursion preserves state for compiler optimizations; consider stack usage.\n\n"
|
| 47 |
+
"Produce detailed output (minimum ~130 words). Do NOT output JSON — just plain text explanation."
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
user_prompt = (
|
| 51 |
+
f"Module: {module.name}\n"
|
| 52 |
+
f"Learning objective: {getattr(module, 'learning_objective', '')}\n\n"
|
| 53 |
+
"Please produce the explanation now."
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
resp = self.client.chat.completions.create(
|
| 57 |
+
model="gpt-4o-mini",
|
| 58 |
+
messages=[
|
| 59 |
+
{"role": "system", "content": system_prompt},
|
| 60 |
+
{"role": "user", "content": user_prompt}
|
| 61 |
+
],
|
| 62 |
+
# you can tune temperature if needed
|
| 63 |
+
)
|
| 64 |
+
explanation = resp.choices[0].message.content.strip()
|
| 65 |
+
|
| 66 |
+
# If too short, ask for expansion once
|
| 67 |
+
if len(explanation.split()) < 120:
|
| 68 |
+
followup = (
|
| 69 |
+
"The previous explanation was too short. Please expand the explanation, add another worked example "
|
| 70 |
+
"and a short code or pseudo-code snippet where appropriate. Keep same style & level."
|
| 71 |
+
)
|
| 72 |
+
resp2 = self.client.chat.completions.create(
|
| 73 |
+
model="gpt-4o-mini",
|
| 74 |
+
messages=[
|
| 75 |
+
{"role": "system", "content": system_prompt},
|
| 76 |
+
{"role": "user", "content": user_prompt},
|
| 77 |
+
{"role": "user", "content": followup}
|
| 78 |
+
],
|
| 79 |
+
)
|
| 80 |
+
extra = resp2.choices[0].message.content.strip()
|
| 81 |
+
# concatenate but keep readable
|
| 82 |
+
explanation = explanation + "\n\n" + extra
|
| 83 |
+
|
| 84 |
+
# return explanation string (do not print in agent; app will show it)
|
| 85 |
+
return explanation
|
| 86 |
+
|
| 87 |
+
def evaluate_example(self, module, student_example):
|
| 88 |
+
"""
|
| 89 |
+
Evaluate the student's example for the given module.
|
| 90 |
+
Returns a dict: { "is_correct": bool, "feedback": str, "confidence": float (0-1) }
|
| 91 |
+
The model MUST output JSON only; we robustly parse it and fallback if needed.
|
| 92 |
+
"""
|
| 93 |
+
system_prompt = (
|
| 94 |
+
"You are an expert educational assessor. Evaluate the student's example strictly with respect "
|
| 95 |
+
"to the module's learning objective. Respond with JSON ONLY (no extra text). The JSON object MUST contain:\n"
|
| 96 |
+
" - is_correct: true/false\n"
|
| 97 |
+
" - feedback: short one-sentence constructive feedback\n"
|
| 98 |
+
" - confidence: numeric between 0 and 1\n\n"
|
| 99 |
+
f"Evaluation sensitivity is based on learner level: {self.level}."
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
user_prompt = (
|
| 103 |
+
f"Module: {module.name}\n"
|
| 104 |
+
f"Learning objective: {getattr(module, 'learning_objective', '')}\n\n"
|
| 105 |
+
f"Student example: {student_example}\n\n"
|
| 106 |
+
"Evaluate and return JSON only."
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
resp = self.client.chat.completions.create(
|
| 110 |
+
model="gpt-4o-mini",
|
| 111 |
+
messages=[
|
| 112 |
+
{"role": "system", "content": system_prompt},
|
| 113 |
+
{"role": "user", "content": user_prompt}
|
| 114 |
+
],
|
| 115 |
+
# lower temperature can reduce hallucinations
|
| 116 |
+
)
|
| 117 |
+
raw = resp.choices[0].message.content.strip()
|
| 118 |
+
|
| 119 |
+
# Try to parse JSON; be forgiving (extract first {...})
|
| 120 |
+
try:
|
| 121 |
+
parsed = json.loads(raw)
|
| 122 |
+
except Exception:
|
| 123 |
+
import re
|
| 124 |
+
m = re.search(r'(\{.*\})', raw, re.DOTALL)
|
| 125 |
+
if m:
|
| 126 |
+
try:
|
| 127 |
+
parsed = json.loads(m.group(1))
|
| 128 |
+
except Exception:
|
| 129 |
+
parsed = None
|
| 130 |
+
else:
|
| 131 |
+
parsed = None
|
| 132 |
+
|
| 133 |
+
if not parsed:
|
| 134 |
+
# fallback: simple heuristics (very conservative)
|
| 135 |
+
is_correct = len(student_example.strip()) > 20
|
| 136 |
+
feedback = "Could not parse evaluator output; using conservative heuristic. Provide a more concrete example." \
|
| 137 |
+
if not is_correct else "Example seems plausible but automatic evaluation failed to parse."
|
| 138 |
+
confidence = 0.45 if not is_correct else 0.6
|
| 139 |
+
return {"is_correct": bool(is_correct), "feedback": feedback, "confidence": confidence, "raw": raw}
|
| 140 |
+
|
| 141 |
+
# normalize fields
|
| 142 |
+
is_correct = bool(parsed.get("is_correct", parsed.get("correct", False)))
|
| 143 |
+
feedback = str(parsed.get("feedback", parsed.get("explanation", "")))
|
| 144 |
+
confidence = float(parsed.get("confidence", parsed.get("score", 0))) if parsed.get("confidence") is not None else 0.9
|
| 145 |
+
|
| 146 |
+
return {"is_correct": is_correct, "feedback": feedback, "confidence": confidence, "raw": raw}
|
| 147 |
+
|
| 148 |
+
def check_example(self, module):
|
| 149 |
+
"""
|
| 150 |
+
Interactive method to check if student understands the module by asking for an example.
|
| 151 |
+
Returns True if the student provides a satisfactory example, False otherwise.
|
| 152 |
+
"""
|
| 153 |
+
print(f"\n🎯 Let's test your understanding of '{module.name}'!")
|
| 154 |
+
print("Please provide a specific example or application that demonstrates this concept.")
|
| 155 |
+
|
| 156 |
+
learning_objective = getattr(module, 'learning_objective', '')
|
| 157 |
+
if learning_objective:
|
| 158 |
+
print(f"Learning Objective: {learning_objective}")
|
| 159 |
+
|
| 160 |
+
print("\nYour example should be:")
|
| 161 |
+
print("• Specific and concrete (not just a definition)")
|
| 162 |
+
print("• Relevant to the module content")
|
| 163 |
+
print("• Show your understanding of how to apply the concept")
|
| 164 |
+
print("• At least 2-3 sentences with clear reasoning")
|
| 165 |
+
|
| 166 |
+
while True: # Allow multiple attempts
|
| 167 |
+
student_example = input("\nYour example: ").strip()
|
| 168 |
+
|
| 169 |
+
if not student_example:
|
| 170 |
+
print("❌ Please provide an example to demonstrate your understanding.")
|
| 171 |
+
continue
|
| 172 |
+
|
| 173 |
+
# Check for minimum length and specificity
|
| 174 |
+
if len(student_example.split()) < 10:
|
| 175 |
+
print("❌ Your example is too brief. Please provide a more detailed example with specific details.")
|
| 176 |
+
continue
|
| 177 |
+
|
| 178 |
+
# Evaluate the example
|
| 179 |
+
evaluation = self.evaluate_example(module, student_example)
|
| 180 |
+
|
| 181 |
+
if evaluation['is_correct']:
|
| 182 |
+
print("✅ Correct! Your example demonstrates good understanding.")
|
| 183 |
+
return True
|
| 184 |
+
else:
|
| 185 |
+
print("❌ Incorrect. Your example doesn't demonstrate sufficient understanding.")
|
| 186 |
+
|
| 187 |
+
# Provide a simple hint
|
| 188 |
+
hint = self.get_simple_hint(module)
|
| 189 |
+
if hint:
|
| 190 |
+
print(f"💡 Hint: {hint}")
|
| 191 |
+
|
| 192 |
+
retry = input("\nWould you like to try again with a different example? (y/n): ").lower().strip()
|
| 193 |
+
if retry == 'y':
|
| 194 |
+
continue
|
| 195 |
+
else:
|
| 196 |
+
print("📚 Let's review the concept again and then try the example.")
|
| 197 |
+
return False
|
| 198 |
+
|
| 199 |
+
def get_simple_hint(self, module):
|
| 200 |
+
"""Generate a simple hint to guide the student toward a better example."""
|
| 201 |
+
system_prompt = (
|
| 202 |
+
"You are an expert tutor providing a simple hint. Give a brief, helpful suggestion (1-2 sentences) "
|
| 203 |
+
"to guide the student toward providing a better example. Don't give away the answer, just nudge them in the right direction."
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
user_prompt = (
|
| 207 |
+
f"Module: {module.name}\n"
|
| 208 |
+
f"Learning Objective: {getattr(module, 'learning_objective', '')}\n\n"
|
| 209 |
+
"Provide a simple hint to help the student think of a better example."
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
try:
|
| 213 |
+
response = self.client.chat.completions.create(
|
| 214 |
+
model="gpt-4o-mini",
|
| 215 |
+
messages=[
|
| 216 |
+
{"role": "system", "content": system_prompt},
|
| 217 |
+
{"role": "user", "content": user_prompt}
|
| 218 |
+
]
|
| 219 |
+
)
|
| 220 |
+
return response.choices[0].message.content.strip()
|
| 221 |
+
except Exception:
|
| 222 |
+
return "Think about how this concept applies in real-world situations or practical scenarios."
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# convenience subclasses (optional)
|
| 226 |
+
class NoviceTeacherAgent(TeacherAgent):
|
| 227 |
+
def __init__(self):
|
| 228 |
+
super().__init__("novice")
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class IntermediateTeacherAgent(TeacherAgent):
|
| 232 |
+
def __init__(self):
|
| 233 |
+
super().__init__("intermediate")
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
class AdvancedTeacherAgent(TeacherAgent):
|
| 237 |
+
def __init__(self):
|
| 238 |
+
super().__init__("advanced")
|
app.py
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AI Teacher Bot - Single Panel UI (fixed example-evaluation & progression)
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import io
|
| 8 |
+
from contextlib import redirect_stdout
|
| 9 |
+
|
| 10 |
+
from main import LEVELS, check_api_key, generate_curriculum
|
| 11 |
+
from user_state import UserState
|
| 12 |
+
from agents.level_assess import LevelAssessmentAgent
|
| 13 |
+
from agents.teacher import TeacherAgent
|
| 14 |
+
from agents.bloom_assess import BloomsAssessmentAgent
|
| 15 |
+
import json
|
| 16 |
+
|
| 17 |
+
# Global session
|
| 18 |
+
current_session = {
|
| 19 |
+
"user": None,
|
| 20 |
+
"curriculum": None,
|
| 21 |
+
"chapter_idx": 0,
|
| 22 |
+
"module_idx": 0,
|
| 23 |
+
"mode": None, # modes: None/idle, "assessment", "teaching", "awaiting_example", "module_passed", "bloom", "done"
|
| 24 |
+
"questions": [],
|
| 25 |
+
"answers": [],
|
| 26 |
+
"q_idx": 0,
|
| 27 |
+
"bloom_level": None,
|
| 28 |
+
# assessment flow controls
|
| 29 |
+
"correct_count": 0,
|
| 30 |
+
"assessment_feedback": [], # list of dicts per question with correctness and feedback
|
| 31 |
+
"last_feedback": None, # stores last question, answer, evaluation, reasoning for challenging
|
| 32 |
+
"challenge_chat_history": [], # list of [user_msg, assistant_msg] pairs for challenge discussion
|
| 33 |
+
"challenge_exchanges": 0, # count of challenge exchanges (max 3)
|
| 34 |
+
"challenge_mode": False, # whether challenge chat is active
|
| 35 |
+
"last_output": ""
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
BLOOM_ORDER = ["remember", "understand", "apply", "analyze", "evaluate", "create"]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def update_main_output(text):
|
| 42 |
+
current_session["last_output"] = text
|
| 43 |
+
return text
|
| 44 |
+
|
| 45 |
+
# ------------------------------
|
| 46 |
+
# Chatbot helpers (Gradio 6 safe)
|
| 47 |
+
# ------------------------------
|
| 48 |
+
def empty_chat():
|
| 49 |
+
"""
|
| 50 |
+
Returns empty chat in messages format (dictionaries with 'role' and 'content' keys).
|
| 51 |
+
"""
|
| 52 |
+
return [{"role": "assistant", "content": " "}]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def safe_chat(chat):
|
| 56 |
+
"""
|
| 57 |
+
Ensures chat history is always valid. Uses messages format (list of dicts with 'role' and 'content').
|
| 58 |
+
"""
|
| 59 |
+
if not isinstance(chat, list):
|
| 60 |
+
return empty_chat()
|
| 61 |
+
if len(chat) == 0:
|
| 62 |
+
return empty_chat()
|
| 63 |
+
# Ensure all items are dicts with role and content
|
| 64 |
+
result = []
|
| 65 |
+
for item in chat:
|
| 66 |
+
if isinstance(item, dict) and "role" in item and "content" in item:
|
| 67 |
+
result.append(item)
|
| 68 |
+
elif isinstance(item, tuple) and len(item) == 2:
|
| 69 |
+
# Convert tuple (user_msg, assistant_msg) to dict format
|
| 70 |
+
result.append({"role": "user", "content": item[0]})
|
| 71 |
+
result.append({"role": "assistant", "content": item[1]})
|
| 72 |
+
return result if result else empty_chat()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_output_update():
|
| 76 |
+
return gr.update(value=current_session.get("last_output", ""))
|
| 77 |
+
|
| 78 |
+
# ------------------------------
|
| 79 |
+
# Session & Flow helpers
|
| 80 |
+
# ------------------------------
|
| 81 |
+
def reset_session_state():
|
| 82 |
+
current_session.update({
|
| 83 |
+
"user": None,
|
| 84 |
+
"curriculum": None,
|
| 85 |
+
"chapter_idx": 0,
|
| 86 |
+
"module_idx": 0,
|
| 87 |
+
"mode": None,
|
| 88 |
+
"questions": [],
|
| 89 |
+
"answers": [],
|
| 90 |
+
"q_idx": 0,
|
| 91 |
+
"bloom_level": None,
|
| 92 |
+
"correct_count": 0,
|
| 93 |
+
"assessment_feedback": [],
|
| 94 |
+
"last_feedback": None,
|
| 95 |
+
"challenge_chat_history": [],
|
| 96 |
+
"challenge_exchanges": 0,
|
| 97 |
+
"challenge_mode": False,
|
| 98 |
+
"last_output": ""
|
| 99 |
+
})
|
| 100 |
+
|
| 101 |
+
def start_learning_session(topic, claimed_level):
|
| 102 |
+
if not topic or not topic.strip():
|
| 103 |
+
return "❌ Please enter a topic"
|
| 104 |
+
if not check_api_key():
|
| 105 |
+
return "❌ OpenAI API key not configured! Please set OPENAI_API_KEY in your .env file."
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
reset_session_state()
|
| 109 |
+
user = UserState(topic=topic.strip(), claimed_level=claimed_level)
|
| 110 |
+
current_session["user"] = user
|
| 111 |
+
|
| 112 |
+
# Generate curriculum (level may be updated later after assessment)
|
| 113 |
+
curriculum = generate_curriculum(user.topic, claimed_level)
|
| 114 |
+
if curriculum is None:
|
| 115 |
+
return "❌ Failed to generate curriculum. Please try again or check your API key."
|
| 116 |
+
current_session["curriculum"] = curriculum
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return f"❌ Error starting session: {str(e)}. Please try again."
|
| 119 |
+
|
| 120 |
+
if claimed_level == "novice":
|
| 121 |
+
user.set_actual_level("novice")
|
| 122 |
+
current_session["mode"] = None # ready to start teaching
|
| 123 |
+
return update_main_output(show_curriculum(curriculum) + "\n\nType 'next' to start Module 1.")
|
| 124 |
+
else:
|
| 125 |
+
# start level assessment
|
| 126 |
+
assessor = LevelAssessmentAgent()
|
| 127 |
+
q_text = assessor.generate_questions(user.topic, claimed_level)
|
| 128 |
+
questions = [q.strip() for q in q_text.split("\n") if q.strip() and q[0].isdigit()]
|
| 129 |
+
if not questions:
|
| 130 |
+
# fallback: skip assessment
|
| 131 |
+
user.set_actual_level(claimed_level)
|
| 132 |
+
current_session["mode"] = None
|
| 133 |
+
return update_main_output(show_curriculum(curriculum) + "\n\nType 'next' to start Module 1.")
|
| 134 |
+
current_session.update({
|
| 135 |
+
"mode": "assessment",
|
| 136 |
+
"questions": questions,
|
| 137 |
+
"q_idx": 0,
|
| 138 |
+
"answers": [],
|
| 139 |
+
"correct_count": 0,
|
| 140 |
+
"assessment_feedback": [],
|
| 141 |
+
"last_feedback": None, # Will be set after first answer submission
|
| 142 |
+
"challenge_chat_history": [],
|
| 143 |
+
"challenge_exchanges": 0,
|
| 144 |
+
"challenge_mode": False
|
| 145 |
+
})
|
| 146 |
+
# Button should be hidden initially (no feedback yet), will show after first answer
|
| 147 |
+
return update_main_output(f"📝 LEVEL ASSESSMENT\n\nQuestion 1 of {len(questions)}:\n{questions[0]}\n\nPlease submit your answer.")
|
| 148 |
+
|
| 149 |
+
# ------------------------------
|
| 150 |
+
# Level assessment handlers
|
| 151 |
+
# ------------------------------
|
| 152 |
+
def _strip_number_prefix(q_line: str) -> str:
|
| 153 |
+
# Converts "1. Question" -> "Question" safely
|
| 154 |
+
q = q_line.strip()
|
| 155 |
+
if ". " in q:
|
| 156 |
+
parts = q.split(". ", 1)
|
| 157 |
+
if parts[0].isdigit():
|
| 158 |
+
return parts[1]
|
| 159 |
+
return q
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def handle_assessment(answer):
|
| 163 |
+
qs = current_session["questions"]
|
| 164 |
+
idx = current_session["q_idx"]
|
| 165 |
+
|
| 166 |
+
if not qs:
|
| 167 |
+
return "⚠️ No assessment in progress."
|
| 168 |
+
|
| 169 |
+
question_full = qs[idx]
|
| 170 |
+
question = _strip_number_prefix(question_full)
|
| 171 |
+
|
| 172 |
+
# guard on empty/very short answers
|
| 173 |
+
user_answer = (answer or "").strip()
|
| 174 |
+
if len(user_answer.split()) < 5:
|
| 175 |
+
# Don't set last_feedback for invalid answers, so button won't show
|
| 176 |
+
return "❌ Your answer is too brief. Please provide a more detailed and specific response (at least 5 words or 1-2 sentences)."
|
| 177 |
+
|
| 178 |
+
assessor = LevelAssessmentAgent()
|
| 179 |
+
try:
|
| 180 |
+
raw = assessor.evaluate_answer(
|
| 181 |
+
current_session["user"].topic,
|
| 182 |
+
current_session["user"].claimed_level,
|
| 183 |
+
question,
|
| 184 |
+
user_answer,
|
| 185 |
+
)
|
| 186 |
+
import json as _json
|
| 187 |
+
parsed = _json.loads(raw)
|
| 188 |
+
evaluation = str(parsed.get("evaluation", "incorrect")).lower()
|
| 189 |
+
reasoning = parsed.get("reasoning", "")
|
| 190 |
+
hint = parsed.get("hint", "") # Extract hint for completeness
|
| 191 |
+
except Exception as e:
|
| 192 |
+
# Better error handling
|
| 193 |
+
evaluation = "correct" if len(user_answer.split()) >= 20 else "incorrect"
|
| 194 |
+
reasoning = f"Heuristic grading fallback used. (Error: {str(e)})"
|
| 195 |
+
hint = ""
|
| 196 |
+
|
| 197 |
+
# Record attempt
|
| 198 |
+
current_session["answers"].append(user_answer)
|
| 199 |
+
is_correct = evaluation == "correct"
|
| 200 |
+
if is_correct:
|
| 201 |
+
current_session["correct_count"] += 1
|
| 202 |
+
|
| 203 |
+
# Save per-question feedback
|
| 204 |
+
current_session["assessment_feedback"].append({
|
| 205 |
+
"question": question,
|
| 206 |
+
"correct": is_correct,
|
| 207 |
+
"reason": reasoning,
|
| 208 |
+
"hint": hint,
|
| 209 |
+
})
|
| 210 |
+
|
| 211 |
+
# Store last feedback for challenging
|
| 212 |
+
current_session["last_feedback"] = {
|
| 213 |
+
"question": question,
|
| 214 |
+
"answer": user_answer,
|
| 215 |
+
"evaluation": evaluation,
|
| 216 |
+
"reasoning": reasoning,
|
| 217 |
+
"is_correct": is_correct,
|
| 218 |
+
"hint": hint
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
# Advance to next question or finish
|
| 222 |
+
if idx + 1 < len(qs):
|
| 223 |
+
current_session["q_idx"] += 1
|
| 224 |
+
status = "✅ Correct!" if is_correct else "❌ Incorrect."
|
| 225 |
+
extra = f"\nReason: {reasoning}" if reasoning else ""
|
| 226 |
+
hint_text = f"\n💡 Hint: {hint}" if hint and not is_correct else ""
|
| 227 |
+
return (
|
| 228 |
+
f"{status}{extra}{hint_text}\n\n"
|
| 229 |
+
f"Question {current_session['q_idx']+1} of {len(qs)}:\n{qs[current_session['q_idx']]}"
|
| 230 |
+
)
|
| 231 |
+
else:
|
| 232 |
+
# Finish: compute score and assign level based on claimed level thresholds
|
| 233 |
+
total = len(qs)
|
| 234 |
+
score_pct = (current_session["correct_count"] / total) * 100
|
| 235 |
+
claimed = current_session["user"].claimed_level
|
| 236 |
+
|
| 237 |
+
if claimed == "advanced":
|
| 238 |
+
if score_pct >= 70:
|
| 239 |
+
assigned = "advanced"
|
| 240 |
+
elif score_pct >= 65:
|
| 241 |
+
assigned = "intermediate"
|
| 242 |
+
else:
|
| 243 |
+
assigned = "novice"
|
| 244 |
+
elif claimed == "intermediate":
|
| 245 |
+
assigned = "intermediate" if score_pct >= 65 else "novice"
|
| 246 |
+
else:
|
| 247 |
+
assigned = "novice"
|
| 248 |
+
|
| 249 |
+
# Build full feedback summary
|
| 250 |
+
lines = [
|
| 251 |
+
"🧪 Assessment Feedback:",
|
| 252 |
+
]
|
| 253 |
+
for i, fb in enumerate(current_session["assessment_feedback"], 1):
|
| 254 |
+
tag = "✅" if fb["correct"] else "❌"
|
| 255 |
+
line = f"{tag} Q{i}: {fb['question']}"
|
| 256 |
+
if fb.get("reason"):
|
| 257 |
+
line += f"\n Reason: {fb['reason']}"
|
| 258 |
+
lines.append(line)
|
| 259 |
+
lines.append("")
|
| 260 |
+
lines.append(f"Score: {score_pct:.1f}% | Assigned Level: {assigned}")
|
| 261 |
+
|
| 262 |
+
current_session["user"].set_actual_level(assigned)
|
| 263 |
+
# Prepare curriculum but show it on next screen
|
| 264 |
+
curriculum = generate_curriculum(current_session["user"].topic, assigned)
|
| 265 |
+
if curriculum:
|
| 266 |
+
current_session["curriculum"] = curriculum
|
| 267 |
+
|
| 268 |
+
feedback_text = "\n".join(lines)
|
| 269 |
+
current_session["mode"] = "assessment_summary"
|
| 270 |
+
current_session["last_feedback"] = None # Clear last feedback when assessment completes
|
| 271 |
+
return feedback_text + "\n\n➡️ Press 'Next' to view your personalized curriculum."
|
| 272 |
+
|
| 273 |
+
# ------------------------------
|
| 274 |
+
# Teaching helpers
|
| 275 |
+
# ------------------------------
|
| 276 |
+
def show_curriculum(curriculum):
|
| 277 |
+
txt = f"📖 CURRICULUM FOR {current_session['user'].topic.upper()}\n" + "="*40 + "\n"
|
| 278 |
+
for i, ch in enumerate(curriculum.chapters, 1):
|
| 279 |
+
txt += f"\nChapter {i}: {ch.name}\n"
|
| 280 |
+
for j, mod in enumerate(ch.modules, 1):
|
| 281 |
+
txt += f" {i}.{j} {mod.name}\n"
|
| 282 |
+
if getattr(mod, "learning_objective", None):
|
| 283 |
+
txt += f" → {mod.learning_objective}\n"
|
| 284 |
+
return txt
|
| 285 |
+
|
| 286 |
+
def next_step(_):
|
| 287 |
+
mode = current_session["mode"]
|
| 288 |
+
# If module just passed, Next moves to next module
|
| 289 |
+
if mode == "module_passed":
|
| 290 |
+
# advance module index now
|
| 291 |
+
current_session["module_idx"] += 1
|
| 292 |
+
current_session["mode"] = None
|
| 293 |
+
return update_main_output(start_teaching_module())
|
| 294 |
+
# If idle/none → start teaching module
|
| 295 |
+
if mode is None:
|
| 296 |
+
return update_main_output(start_teaching_module())
|
| 297 |
+
if mode == "teaching":
|
| 298 |
+
return update_main_output(get_explanation())
|
| 299 |
+
if mode == "awaiting_example":
|
| 300 |
+
return update_main_output("✋ Please submit your example using 'Submit Answer' before moving on.")
|
| 301 |
+
if mode == "bloom":
|
| 302 |
+
return update_main_output("🌸 Bloom assessment in progress — answer the Bloom question or submit to retry.")
|
| 303 |
+
if mode == "assessment_summary":
|
| 304 |
+
# show curriculum now and transition to normal teaching flow
|
| 305 |
+
current_session["mode"] = None
|
| 306 |
+
return update_main_output(show_curriculum(current_session["curriculum"]) + "\n\nType 'next' to start Module 1.")
|
| 307 |
+
return update_main_output("⚠️ Invalid state.")
|
| 308 |
+
|
| 309 |
+
def start_teaching_module():
|
| 310 |
+
if not current_session.get("curriculum"):
|
| 311 |
+
return "⚠️ No curriculum loaded. Please start a session first."
|
| 312 |
+
|
| 313 |
+
try:
|
| 314 |
+
cur = current_session["curriculum"]
|
| 315 |
+
ch_i = current_session["chapter_idx"]
|
| 316 |
+
m_i = current_session["module_idx"]
|
| 317 |
+
|
| 318 |
+
if ch_i >= len(cur.chapters):
|
| 319 |
+
current_session["mode"] = "done"
|
| 320 |
+
return "🎉 You have completed the entire curriculum!"
|
| 321 |
+
|
| 322 |
+
chapter = cur.chapters[ch_i]
|
| 323 |
+
# if all modules finished -> start Bloom for the chapter
|
| 324 |
+
if m_i >= len(chapter.modules):
|
| 325 |
+
return start_bloom_assessment()
|
| 326 |
+
|
| 327 |
+
module = chapter.modules[m_i]
|
| 328 |
+
current_session["mode"] = "teaching"
|
| 329 |
+
return f"📚 Chapter {ch_i+1}: {chapter.name}\nModule {ch_i+1}.{m_i+1}: {module.name}\n\nObjective: {getattr(module,'learning_objective','')}\n\nClick 'Next' to get the explanation."
|
| 330 |
+
except Exception as e:
|
| 331 |
+
return f"❌ Error starting teaching module: {str(e)}"
|
| 332 |
+
|
| 333 |
+
def get_explanation():
|
| 334 |
+
if not current_session.get("curriculum") or not current_session.get("user"):
|
| 335 |
+
return "⚠️ Session not properly initialized. Please start a new session."
|
| 336 |
+
|
| 337 |
+
try:
|
| 338 |
+
cur = current_session["curriculum"]
|
| 339 |
+
ch_i = current_session["chapter_idx"]
|
| 340 |
+
m_i = current_session["module_idx"]
|
| 341 |
+
chapter = cur.chapters[ch_i]
|
| 342 |
+
# Guard
|
| 343 |
+
if m_i >= len(chapter.modules):
|
| 344 |
+
return start_bloom_assessment()
|
| 345 |
+
|
| 346 |
+
module = chapter.modules[m_i]
|
| 347 |
+
teacher = TeacherAgent(current_session["user"].actual_level or current_session["user"].claimed_level)
|
| 348 |
+
explanation = teacher.teach_module(module) # returns string
|
| 349 |
+
current_session["mode"] = "awaiting_example"
|
| 350 |
+
return f"📖 Explanation for {module.name}\n\n{explanation}\n\n✍️ Now submit your example in the box and click 'Submit Answer'."
|
| 351 |
+
except Exception as e:
|
| 352 |
+
return f"❌ Error getting explanation: {str(e)}"
|
| 353 |
+
|
| 354 |
+
# ------------------------------
|
| 355 |
+
# Submit handler (single entry point wired to Submit button)
|
| 356 |
+
# ------------------------------
|
| 357 |
+
def submit_answer(answer):
|
| 358 |
+
mode = current_session.get("mode")
|
| 359 |
+
if mode == "assessment":
|
| 360 |
+
response = handle_assessment(answer)
|
| 361 |
+
elif mode == "awaiting_example":
|
| 362 |
+
response = handle_example_submission(answer)
|
| 363 |
+
elif mode == "bloom":
|
| 364 |
+
response = handle_bloom(answer)
|
| 365 |
+
else:
|
| 366 |
+
response = "⚠️ Nothing to submit right now. Click 'Next' to proceed."
|
| 367 |
+
current_session["last_output"] = response
|
| 368 |
+
# Return output + clear input
|
| 369 |
+
return response, ""
|
| 370 |
+
|
| 371 |
+
def start_challenge_discussion():
|
| 372 |
+
"""
|
| 373 |
+
Opens the challenge discussion chat interface as a modal pop-up.
|
| 374 |
+
Shows the original feedback and prompts user to start discussion.
|
| 375 |
+
"""
|
| 376 |
+
mode = current_session.get("mode")
|
| 377 |
+
if mode != "assessment":
|
| 378 |
+
return gr.update(visible=False), [], "⚠️ Challenge is only available during assessment.", get_output_update()
|
| 379 |
+
|
| 380 |
+
last_fb = current_session.get("last_feedback")
|
| 381 |
+
if not last_fb:
|
| 382 |
+
return gr.update(visible=False), [], "⚠️ No feedback available to challenge. Please submit an answer first.", get_output_update()
|
| 383 |
+
|
| 384 |
+
# Initialize challenge discussion
|
| 385 |
+
current_session["challenge_mode"] = True
|
| 386 |
+
current_session["challenge_exchanges"] = 0
|
| 387 |
+
current_session["challenge_chat_history"] = []
|
| 388 |
+
|
| 389 |
+
# Show initial context
|
| 390 |
+
initial_greeting = (
|
| 391 |
+
f"**Grader's Original Feedback:**\n"
|
| 392 |
+
f"Evaluation: {'✅ Correct' if last_fb['is_correct'] else '❌ Incorrect'}\n"
|
| 393 |
+
f"Reasoning: {last_fb['reasoning']}\n\n"
|
| 394 |
+
f"**Question:** {last_fb['question']}\n"
|
| 395 |
+
f"**Your Answer:** {last_fb['answer']}\n\n"
|
| 396 |
+
f"💬 You can now present your arguments. You have up to 3 exchanges with the grader."
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
# Use messages format: list of dicts with 'role' and 'content'
|
| 400 |
+
chat_history = [{"role": "assistant", "content": initial_greeting}]
|
| 401 |
+
status_msg = "💬 Challenge discussion opened. Present your first argument below (3 exchanges remaining)."
|
| 402 |
+
|
| 403 |
+
return (
|
| 404 |
+
gr.update(visible=True),
|
| 405 |
+
chat_history,
|
| 406 |
+
status_msg,
|
| 407 |
+
get_output_update()
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
def handle_challenge_message(message, chat_history):
|
| 411 |
+
"""
|
| 412 |
+
Handles a message in the challenge discussion.
|
| 413 |
+
Limits to 3 total exchanges (student messages).
|
| 414 |
+
Returns: chat_history, status_msg, msg_enabled, btn_enabled, output_update
|
| 415 |
+
"""
|
| 416 |
+
if not current_session.get("challenge_mode"):
|
| 417 |
+
return chat_history, "⚠️ Challenge discussion is not active.", False, False, get_output_update()
|
| 418 |
+
|
| 419 |
+
if not message or not message.strip():
|
| 420 |
+
return chat_history, "", True, True, gr.update()
|
| 421 |
+
|
| 422 |
+
if current_session["challenge_exchanges"] >= 3:
|
| 423 |
+
return chat_history, "⚠️ Maximum exchanges (3) reached. Discussion closed. Click 'Close Discussion' to continue.", False, False, get_output_update()
|
| 424 |
+
|
| 425 |
+
last_fb = current_session.get("last_feedback")
|
| 426 |
+
if not last_fb:
|
| 427 |
+
return chat_history, "⚠️ No feedback available.", False, False, get_output_update()
|
| 428 |
+
|
| 429 |
+
assessor = LevelAssessmentAgent()
|
| 430 |
+
try:
|
| 431 |
+
grader_response = assessor.challenge_discussion(
|
| 432 |
+
current_session["user"].topic,
|
| 433 |
+
current_session["user"].claimed_level,
|
| 434 |
+
last_fb["question"],
|
| 435 |
+
last_fb["answer"],
|
| 436 |
+
last_fb["evaluation"],
|
| 437 |
+
last_fb["reasoning"],
|
| 438 |
+
current_session["challenge_chat_history"],
|
| 439 |
+
message
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
current_session["challenge_chat_history"].append([message, grader_response])
|
| 443 |
+
current_session["challenge_exchanges"] += 1
|
| 444 |
+
|
| 445 |
+
# Ensure chat_history is in messages format (list of dicts)
|
| 446 |
+
chat_history = safe_chat(chat_history)
|
| 447 |
+
|
| 448 |
+
# Add the new messages in messages format
|
| 449 |
+
chat_history.append({"role": "user", "content": message})
|
| 450 |
+
chat_history.append({"role": "assistant", "content": grader_response})
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
remaining = 3 - current_session["challenge_exchanges"]
|
| 454 |
+
if remaining > 0:
|
| 455 |
+
status_msg = f"💬 {remaining} exchange(s) remaining. You can continue the discussion."
|
| 456 |
+
output_update = gr.update()
|
| 457 |
+
msg_enabled = True
|
| 458 |
+
btn_enabled = True
|
| 459 |
+
else:
|
| 460 |
+
# Finalize evaluation after 3 exchanges
|
| 461 |
+
status_msg, output_update = finalize_challenge_discussion(grader_response)
|
| 462 |
+
msg_enabled = False
|
| 463 |
+
btn_enabled = False
|
| 464 |
+
|
| 465 |
+
return chat_history, status_msg, msg_enabled, btn_enabled, output_update
|
| 466 |
+
except Exception as e:
|
| 467 |
+
return chat_history, f"❌ Error: {str(e)}", True, True, gr.update()
|
| 468 |
+
|
| 469 |
+
def finalize_challenge_discussion(final_response):
|
| 470 |
+
"""
|
| 471 |
+
Finalizes the challenge discussion and updates evaluation if needed.
|
| 472 |
+
Extracts final evaluation from the last grader response.
|
| 473 |
+
Returns: (status_msg, main_output_update)
|
| 474 |
+
"""
|
| 475 |
+
last_fb = current_session.get("last_feedback")
|
| 476 |
+
if not last_fb:
|
| 477 |
+
return "⚠️ Could not finalize challenge.", get_output_update()
|
| 478 |
+
|
| 479 |
+
# Try to extract evaluation from the final response
|
| 480 |
+
# Use the challenge_feedback method to get a structured final evaluation
|
| 481 |
+
assessor = LevelAssessmentAgent()
|
| 482 |
+
try:
|
| 483 |
+
# Get the full conversation context
|
| 484 |
+
conversation_text = "\n".join([
|
| 485 |
+
f"Student: {msg[0]}\nGrader: {msg[1]}"
|
| 486 |
+
for msg in current_session["challenge_chat_history"]
|
| 487 |
+
])
|
| 488 |
+
|
| 489 |
+
# Final re-evaluation request
|
| 490 |
+
final_prompt = (
|
| 491 |
+
f"Based on our discussion:\n{conversation_text}\n\n"
|
| 492 |
+
"Please provide your FINAL evaluation as JSON with: "
|
| 493 |
+
'{"evaluation": "correct" or "incorrect", "reasoning": "explanation", "original_was_fair": true/false}'
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
raw = assessor.challenge_feedback(
|
| 497 |
+
current_session["user"].topic,
|
| 498 |
+
current_session["user"].claimed_level,
|
| 499 |
+
last_fb["question"],
|
| 500 |
+
last_fb["answer"],
|
| 501 |
+
last_fb["evaluation"],
|
| 502 |
+
last_fb["reasoning"]
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
import json as _json
|
| 506 |
+
parsed = _json.loads(raw)
|
| 507 |
+
new_evaluation = str(parsed.get("evaluation", "incorrect")).lower()
|
| 508 |
+
new_reasoning = parsed.get("reasoning", "")
|
| 509 |
+
original_was_fair = parsed.get("original_was_fair", True)
|
| 510 |
+
|
| 511 |
+
new_is_correct = new_evaluation == "correct"
|
| 512 |
+
old_is_correct = last_fb["is_correct"]
|
| 513 |
+
|
| 514 |
+
# Build the main output update
|
| 515 |
+
qs = current_session["questions"]
|
| 516 |
+
idx = current_session["q_idx"]
|
| 517 |
+
current_question_text = ""
|
| 518 |
+
if idx < len(qs):
|
| 519 |
+
current_question_text = f"\n\n📝 Current Question {idx+1} of {len(qs)}:\n{qs[idx]}"
|
| 520 |
+
|
| 521 |
+
# Update if evaluation changed
|
| 522 |
+
if new_is_correct != old_is_correct:
|
| 523 |
+
if new_is_correct and not old_is_correct:
|
| 524 |
+
current_session["correct_count"] += 1
|
| 525 |
+
elif not new_is_correct and old_is_correct:
|
| 526 |
+
current_session["correct_count"] = max(0, current_session["correct_count"] - 1)
|
| 527 |
+
|
| 528 |
+
if current_session["assessment_feedback"]:
|
| 529 |
+
current_session["assessment_feedback"][-1]["correct"] = new_is_correct
|
| 530 |
+
current_session["assessment_feedback"][-1]["reason"] = new_reasoning
|
| 531 |
+
|
| 532 |
+
current_session["last_feedback"]["evaluation"] = new_evaluation
|
| 533 |
+
current_session["last_feedback"]["reasoning"] = new_reasoning
|
| 534 |
+
current_session["last_feedback"]["is_correct"] = new_is_correct
|
| 535 |
+
|
| 536 |
+
# Build updated main output
|
| 537 |
+
main_output = (
|
| 538 |
+
f"🔄 **EVALUATION UPDATED AFTER CHALLENGE**\n\n"
|
| 539 |
+
f"**Question:** {last_fb['question']}\n"
|
| 540 |
+
f"**Your Answer:** {last_fb['answer']}\n\n"
|
| 541 |
+
f"**Original Evaluation:** {'✅ Correct' if old_is_correct else '❌ Incorrect'}\n"
|
| 542 |
+
f"**Updated Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'}\n\n"
|
| 543 |
+
f"**Updated Reasoning:** {new_reasoning}\n"
|
| 544 |
+
f"{current_question_text}"
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
status_msg = "🔄 **FINAL EVALUATION UPDATED**\n\n" + \
|
| 548 |
+
f"**New Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'}\n" + \
|
| 549 |
+
f"**Final Reasoning:** {new_reasoning}\n\n" + \
|
| 550 |
+
"✅ Challenge discussion completed. Evaluation has been updated on the main screen."
|
| 551 |
+
else:
|
| 552 |
+
# Build main output showing final evaluation
|
| 553 |
+
main_output = (
|
| 554 |
+
f"📋 **FINAL EVALUATION AFTER CHALLENGE**\n\n"
|
| 555 |
+
f"**Question:** {last_fb['question']}\n"
|
| 556 |
+
f"**Your Answer:** {last_fb['answer']}\n\n"
|
| 557 |
+
f"**Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'} (unchanged)\n\n"
|
| 558 |
+
f"**Final Reasoning:** {new_reasoning}\n"
|
| 559 |
+
f"{current_question_text}"
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
status_msg = "📋 **FINAL EVALUATION**\n\n" + \
|
| 563 |
+
f"**Evaluation:** {'✅ Correct' if new_is_correct else '❌ Incorrect'} (unchanged)\n" + \
|
| 564 |
+
f"**Final Reasoning:** {new_reasoning}\n\n" + \
|
| 565 |
+
"✅ Challenge discussion completed. You may continue with the assessment."
|
| 566 |
+
|
| 567 |
+
# Close challenge mode
|
| 568 |
+
current_session["challenge_mode"] = False
|
| 569 |
+
current_session["last_output"] = main_output
|
| 570 |
+
|
| 571 |
+
return status_msg, gr.update(value=main_output)
|
| 572 |
+
except Exception as e:
|
| 573 |
+
current_session["challenge_mode"] = False
|
| 574 |
+
error_msg = f"⚠️ Error finalizing challenge: {str(e)}"
|
| 575 |
+
return error_msg, get_output_update()
|
| 576 |
+
|
| 577 |
+
def handle_example_submission(example_text):
|
| 578 |
+
"""
|
| 579 |
+
Uses TeacherAgent.evaluate_example(module, example) to decide correctness.
|
| 580 |
+
If correct -> set mode to 'module_passed' and require user to press Next to move on.
|
| 581 |
+
If incorrect -> remain in 'awaiting_example' and show feedback.
|
| 582 |
+
"""
|
| 583 |
+
if not current_session.get("curriculum"):
|
| 584 |
+
return "⚠️ No curriculum loaded. Please start a session first."
|
| 585 |
+
|
| 586 |
+
cur = current_session["curriculum"]
|
| 587 |
+
ch_i = current_session["chapter_idx"]
|
| 588 |
+
m_i = current_session["module_idx"]
|
| 589 |
+
|
| 590 |
+
if ch_i >= len(cur.chapters):
|
| 591 |
+
return "⚠️ No active chapter available."
|
| 592 |
+
|
| 593 |
+
chapter = cur.chapters[ch_i]
|
| 594 |
+
if m_i >= len(chapter.modules):
|
| 595 |
+
# Shouldn't happen, but guard
|
| 596 |
+
return "⚠️ No active module to evaluate."
|
| 597 |
+
|
| 598 |
+
module = chapter.modules[m_i]
|
| 599 |
+
teacher = TeacherAgent(current_session["user"].actual_level or current_session["user"].claimed_level)
|
| 600 |
+
|
| 601 |
+
# quick client-side guardrails for empty/brief examples
|
| 602 |
+
if not example_text or not str(example_text).strip():
|
| 603 |
+
return "❌ Please provide an example to demonstrate your understanding."
|
| 604 |
+
if len(str(example_text).strip().split()) < 10:
|
| 605 |
+
return "❌ Your example is too brief. Provide 2-3 sentences with specific details."
|
| 606 |
+
|
| 607 |
+
try:
|
| 608 |
+
eval_result = teacher.evaluate_example(module, example_text)
|
| 609 |
+
is_correct = bool(eval_result.get("is_correct"))
|
| 610 |
+
feedback = eval_result.get("feedback", "No feedback provided.")
|
| 611 |
+
confidence = eval_result.get("confidence", None)
|
| 612 |
+
except Exception as e:
|
| 613 |
+
return f"❌ Error evaluating example: {str(e)}. Please try again."
|
| 614 |
+
|
| 615 |
+
if is_correct:
|
| 616 |
+
# mark module as passed (do not auto-increment module_idx — require Next)
|
| 617 |
+
current_session["mode"] = "module_passed"
|
| 618 |
+
return f"✅ Example accepted. Feedback: {feedback}\n\n➡️ Click 'Next' to continue to the next module."
|
| 619 |
+
else:
|
| 620 |
+
# remain in awaiting_example — must retry
|
| 621 |
+
current_session["mode"] = "awaiting_example"
|
| 622 |
+
return f"❌ Example not sufficient. Feedback: {feedback}\n\nPlease try another example for the same module."
|
| 623 |
+
|
| 624 |
+
# ------------------------------
|
| 625 |
+
# Bloom assessment (per chapter)
|
| 626 |
+
# ------------------------------
|
| 627 |
+
def start_bloom_assessment():
|
| 628 |
+
if not current_session.get("curriculum"):
|
| 629 |
+
return "⚠️ No curriculum loaded. Please start a session first."
|
| 630 |
+
|
| 631 |
+
try:
|
| 632 |
+
current_session["mode"] = "bloom"
|
| 633 |
+
current_session["bloom_level"] = BLOOM_ORDER[0]
|
| 634 |
+
chapter = current_session["curriculum"].chapters[current_session["chapter_idx"]]
|
| 635 |
+
return ask_bloom_question(current_session["bloom_level"], chapter)
|
| 636 |
+
except Exception as e:
|
| 637 |
+
return f"❌ Error starting Bloom assessment: {str(e)}"
|
| 638 |
+
|
| 639 |
+
def ask_bloom_question(level, chapter):
|
| 640 |
+
try:
|
| 641 |
+
agent = BloomsAssessmentAgent()
|
| 642 |
+
# agent.generate_bloom_question expects (chapter, bloom_level)
|
| 643 |
+
q = agent.generate_bloom_question(chapter, level)
|
| 644 |
+
current_session["questions"] = [q]
|
| 645 |
+
return f"🌸 Bloom's Assessment ({level.title()}) for Chapter: {chapter.name}\n\n{q}\n\n✍️ Answer below and click Submit."
|
| 646 |
+
except Exception as e:
|
| 647 |
+
return f"❌ Error generating Bloom question: {str(e)}"
|
| 648 |
+
|
| 649 |
+
def handle_bloom(answer):
|
| 650 |
+
if not current_session.get("curriculum"):
|
| 651 |
+
return "⚠️ No curriculum loaded. Please start a session first."
|
| 652 |
+
|
| 653 |
+
if not answer or not str(answer).strip():
|
| 654 |
+
return "❌ Please provide an answer for the Bloom assessment question."
|
| 655 |
+
|
| 656 |
+
try:
|
| 657 |
+
level = current_session["bloom_level"]
|
| 658 |
+
if not current_session["questions"]:
|
| 659 |
+
return "⚠️ No question available. Please start a new session."
|
| 660 |
+
question = current_session["questions"][0]
|
| 661 |
+
chapter = current_session["curriculum"].chapters[current_session["chapter_idx"]]
|
| 662 |
+
agent = BloomsAssessmentAgent()
|
| 663 |
+
|
| 664 |
+
# agent.evaluate_bloom_answer returns a JSON string per your agent implementation
|
| 665 |
+
raw_eval = agent.evaluate_bloom_answer(question, answer, level, chapter)
|
| 666 |
+
|
| 667 |
+
# parse evaluation JSON
|
| 668 |
+
try:
|
| 669 |
+
eval_obj = json.loads(raw_eval)
|
| 670 |
+
score = float(eval_obj.get("score", 0))
|
| 671 |
+
feedback = str(eval_obj.get("feedback", "No feedback"))
|
| 672 |
+
except (json.JSONDecodeError, ValueError) as e:
|
| 673 |
+
# fallback: if text contains 'correct' treat as pass
|
| 674 |
+
feedback = f"Could not parse evaluation response. Raw: {raw_eval[:100]}..."
|
| 675 |
+
score = 10 if "correct" in raw_eval.lower() else 0
|
| 676 |
+
except KeyError as e:
|
| 677 |
+
return f"❌ Session error: Missing required data ({str(e)}). Please restart the session."
|
| 678 |
+
except Exception as e:
|
| 679 |
+
return f"❌ Error evaluating Bloom answer: {str(e)}. Please try again."
|
| 680 |
+
|
| 681 |
+
# use threshold (e.g., >=6/10)
|
| 682 |
+
if score >= 6:
|
| 683 |
+
# advance bloom level
|
| 684 |
+
next_idx = BLOOM_ORDER.index(level) + 1
|
| 685 |
+
if next_idx < len(BLOOM_ORDER):
|
| 686 |
+
current_session["bloom_level"] = BLOOM_ORDER[next_idx]
|
| 687 |
+
# generate new question for next level
|
| 688 |
+
return f"✅ {feedback}\n\n➡️ Moving to {BLOOM_ORDER[next_idx].title()}.\n\n" + ask_bloom_question(current_session["bloom_level"], chapter)
|
| 689 |
+
else:
|
| 690 |
+
# finished Bloom for chapter -> next chapter
|
| 691 |
+
current_session["chapter_idx"] += 1
|
| 692 |
+
current_session["module_idx"] = 0
|
| 693 |
+
current_session["mode"] = None
|
| 694 |
+
return f"🎉 {feedback}\n\n✅ Bloom’s assessment completed for chapter '{chapter.name}'.\nType 'next' to continue."
|
| 695 |
+
else:
|
| 696 |
+
# ask a new question for same level
|
| 697 |
+
return f"❌ {feedback}\n\n🔁 Try another question at the same level.\n\n" + ask_bloom_question(level, chapter)
|
| 698 |
+
|
| 699 |
+
# ------------------------------
|
| 700 |
+
# Gradio UI wiring (single unified output)
|
| 701 |
+
# ------------------------------
|
| 702 |
+
# Custom CSS for modal pop-up
|
| 703 |
+
modal_css = """
|
| 704 |
+
.modal-overlay:not([style*="display: none"]) {
|
| 705 |
+
position: fixed !important;
|
| 706 |
+
top: 0 !important;
|
| 707 |
+
left: 0 !important;
|
| 708 |
+
width: 100% !important;
|
| 709 |
+
height: 100% !important;
|
| 710 |
+
background-color: rgba(0, 0, 0, 0.5) !important;
|
| 711 |
+
z-index: 1000 !important;
|
| 712 |
+
display: flex !important;
|
| 713 |
+
align-items: center !important;
|
| 714 |
+
justify-content: center !important;
|
| 715 |
+
padding: 20px !important;
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
/* When Gradio hides the element, ensure it doesn't block interactions */
|
| 719 |
+
.modal-overlay[style*="display: none"],
|
| 720 |
+
.modal-overlay[style*="display:none"] {
|
| 721 |
+
display: none !important;
|
| 722 |
+
visibility: hidden !important;
|
| 723 |
+
pointer-events: none !important;
|
| 724 |
+
z-index: -1 !important;
|
| 725 |
+
opacity: 0 !important;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
.modal-content {
|
| 729 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important;
|
| 730 |
+
border-radius: 10px !important;
|
| 731 |
+
padding: 20px !important;
|
| 732 |
+
max-width: 900px !important;
|
| 733 |
+
width: 100% !important;
|
| 734 |
+
max-height: 90vh !important;
|
| 735 |
+
overflow-y: auto !important;
|
| 736 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3) !important;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
/* Chat window styling */
|
| 740 |
+
.modal-content .gradio-chatbot {
|
| 741 |
+
background-color: #ffffff !important;
|
| 742 |
+
border-radius: 8px !important;
|
| 743 |
+
padding: 15px !important;
|
| 744 |
+
border: 2px solid #e0e0e0 !important;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.modal-content .gradio-chatbot .message {
|
| 748 |
+
background-color: #f8f9fa !important;
|
| 749 |
+
border-radius: 8px !important;
|
| 750 |
+
padding: 10px !important;
|
| 751 |
+
margin: 5px 0 !important;
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.modal-content .gradio-chatbot .user-message {
|
| 755 |
+
background-color: #e3f2fd !important;
|
| 756 |
+
border-left: 4px solid #2196f3 !important;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.modal-content .gradio-chatbot .bot-message {
|
| 760 |
+
background-color: #f1f8e9 !important;
|
| 761 |
+
border-left: 4px solid #8bc34a !important;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
.modal-header {
|
| 765 |
+
margin: 0 !important;
|
| 766 |
+
padding: 0 !important;
|
| 767 |
+
flex-grow: 1 !important;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
.modal-close-btn {
|
| 771 |
+
min-width: 40px !important;
|
| 772 |
+
height: 40px !important;
|
| 773 |
+
border-radius: 50% !important;
|
| 774 |
+
font-size: 20px !important;
|
| 775 |
+
font-weight: bold !important;
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
.modal-note {
|
| 779 |
+
font-size: 12px !important;
|
| 780 |
+
color: #666 !important;
|
| 781 |
+
margin-top: 10px !important;
|
| 782 |
+
}
|
| 783 |
+
"""
|
| 784 |
+
|
| 785 |
+
with gr.Blocks(title="AI Teacher Bot") as demo:
|
| 786 |
+
gr.Markdown("""
|
| 787 |
+
# 🧠 AI Teacher Bot
|
| 788 |
+
|
| 789 |
+
In this interactive learning experience, you will be prompted to select your learning level—**Beginner**, **Intermediate**, or **Advanced**.
|
| 790 |
+
|
| 791 |
+
- If you choose **Intermediate** or **Advanced**, you will be presented with an assessment designed to test your understanding of the material. The questions in these assessments are tailored to the selected level, ensuring they are **challenging and reflective of the knowledge expected at that stage**. For instance, **Intermediate-level questions** will require more in-depth explanations, not just basic one-liner answers. Similarly, **Advanced-level assessments** will be comprehensive and demand a higher level of critical thinking and subject mastery.
|
| 792 |
+
""")
|
| 793 |
+
|
| 794 |
+
|
| 795 |
+
with gr.Row():
|
| 796 |
+
topic = gr.Textbox(label="📝 Topic", placeholder="e.g., Python Programming", scale=2)
|
| 797 |
+
level = gr.Dropdown(choices=LEVELS, value="novice", label="🎓 Your Level", scale=1)
|
| 798 |
+
start_btn = gr.Button("🚀 Start Session")
|
| 799 |
+
|
| 800 |
+
output = gr.Textbox(label="📚 Session Output", lines=25, interactive=False, autoscroll=True)
|
| 801 |
+
answer_box = gr.Textbox(label="✍️ Your Answer / Example", placeholder="Type your answer...", lines=5, max_lines=10)
|
| 802 |
+
with gr.Row():
|
| 803 |
+
submit_btn = gr.Button("Submit Answer")
|
| 804 |
+
next_btn = gr.Button("Next")
|
| 805 |
+
challenge_btn = gr.Button("Challenge Assessment", visible=False)
|
| 806 |
+
|
| 807 |
+
# Challenge discussion chat interface - Modal Pop-up
|
| 808 |
+
with gr.Column(visible=False, elem_classes="modal-overlay") as challenge_modal_overlay:
|
| 809 |
+
with gr.Column(elem_classes="modal-content"):
|
| 810 |
+
with gr.Row():
|
| 811 |
+
gr.Markdown("### 💬 Challenge Discussion with Grader", elem_classes="modal-header")
|
| 812 |
+
challenge_close_btn = gr.Button("✕", elem_classes="modal-close-btn", scale=0)
|
| 813 |
+
|
| 814 |
+
with gr.Row():
|
| 815 |
+
with gr.Column(scale=3):
|
| 816 |
+
challenge_chat = gr.Chatbot(
|
| 817 |
+
label="",
|
| 818 |
+
height=400,
|
| 819 |
+
show_label=False,
|
| 820 |
+
container=True
|
| 821 |
+
)
|
| 822 |
+
challenge_status = gr.Textbox(
|
| 823 |
+
label="Status",
|
| 824 |
+
interactive=False,
|
| 825 |
+
lines=2,
|
| 826 |
+
container=True
|
| 827 |
+
)
|
| 828 |
+
with gr.Column(scale=1):
|
| 829 |
+
challenge_msg_box = gr.Textbox(
|
| 830 |
+
label="Your Argument/Question",
|
| 831 |
+
placeholder="Explain why you think your answer is correct...",
|
| 832 |
+
lines=5,
|
| 833 |
+
container=True
|
| 834 |
+
)
|
| 835 |
+
challenge_send_btn = gr.Button("Send", variant="primary")
|
| 836 |
+
gr.Markdown("**Note:** You have up to 3 exchanges with the grader.", elem_classes="modal-note")
|
| 837 |
+
|
| 838 |
+
# Function to update challenge button visibility
|
| 839 |
+
def update_challenge_visibility():
|
| 840 |
+
mode = current_session.get("mode")
|
| 841 |
+
has_feedback = current_session.get("last_feedback") is not None
|
| 842 |
+
# Show button during assessment mode after first answer is submitted
|
| 843 |
+
should_show = (mode == "assessment" and has_feedback)
|
| 844 |
+
return gr.update(visible=should_show)
|
| 845 |
+
|
| 846 |
+
# handlers
|
| 847 |
+
start_btn.click(
|
| 848 |
+
fn=start_learning_session,
|
| 849 |
+
inputs=[topic, level],
|
| 850 |
+
outputs=[output]
|
| 851 |
+
).then(
|
| 852 |
+
fn=update_challenge_visibility,
|
| 853 |
+
inputs=None,
|
| 854 |
+
outputs=[challenge_btn]
|
| 855 |
+
)
|
| 856 |
+
|
| 857 |
+
submit_btn.click(
|
| 858 |
+
fn=submit_answer,
|
| 859 |
+
inputs=[answer_box],
|
| 860 |
+
outputs=[output, answer_box]
|
| 861 |
+
).then(
|
| 862 |
+
fn=update_challenge_visibility,
|
| 863 |
+
inputs=None,
|
| 864 |
+
outputs=[challenge_btn]
|
| 865 |
+
)
|
| 866 |
+
|
| 867 |
+
next_btn.click(
|
| 868 |
+
fn=next_step,
|
| 869 |
+
inputs=[answer_box],
|
| 870 |
+
outputs=[output]
|
| 871 |
+
).then(
|
| 872 |
+
fn=update_challenge_visibility,
|
| 873 |
+
inputs=None,
|
| 874 |
+
outputs=[challenge_btn]
|
| 875 |
+
)
|
| 876 |
+
challenge_btn.click(
|
| 877 |
+
fn=start_challenge_discussion,
|
| 878 |
+
inputs=None,
|
| 879 |
+
outputs=[challenge_modal_overlay, challenge_chat, challenge_status, output]
|
| 880 |
+
).then(
|
| 881 |
+
fn=lambda: (gr.update(value="", interactive=True), gr.update(interactive=True)),
|
| 882 |
+
inputs=None,
|
| 883 |
+
outputs=[challenge_msg_box, challenge_send_btn]
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
def send_and_clear(message, chat_history):
|
| 887 |
+
"""Send message and clear input box (if needed)"""
|
| 888 |
+
history, status, msg_enabled, btn_enabled, output_update = handle_challenge_message(message, chat_history)
|
| 889 |
+
if message and message.strip():
|
| 890 |
+
msg_update = gr.update(value="", interactive=msg_enabled)
|
| 891 |
+
else:
|
| 892 |
+
msg_update = gr.update(value=message, interactive=msg_enabled)
|
| 893 |
+
btn_update = gr.update(interactive=btn_enabled)
|
| 894 |
+
return history, msg_update, status, btn_update, output_update
|
| 895 |
+
|
| 896 |
+
challenge_send_btn.click(
|
| 897 |
+
fn=send_and_clear,
|
| 898 |
+
inputs=[challenge_msg_box, challenge_chat],
|
| 899 |
+
outputs=[challenge_chat, challenge_msg_box, challenge_status, challenge_send_btn, output]
|
| 900 |
+
)
|
| 901 |
+
|
| 902 |
+
challenge_msg_box.submit(
|
| 903 |
+
fn=send_and_clear,
|
| 904 |
+
inputs=[challenge_msg_box, challenge_chat],
|
| 905 |
+
outputs=[challenge_chat, challenge_msg_box, challenge_status, challenge_send_btn, output]
|
| 906 |
+
)
|
| 907 |
+
|
| 908 |
+
def close_challenge_discussion():
|
| 909 |
+
"""
|
| 910 |
+
Properly closes the challenge modal and restores UI control
|
| 911 |
+
"""
|
| 912 |
+
# Reset backend state
|
| 913 |
+
current_session["challenge_mode"] = False
|
| 914 |
+
current_session["challenge_chat_history"] = []
|
| 915 |
+
current_session["challenge_exchanges"] = 0
|
| 916 |
+
|
| 917 |
+
# IMPORTANT: Use visible=False to hide the overlay completely
|
| 918 |
+
# This should remove it from the DOM or set display:none which CSS will respect
|
| 919 |
+
return (
|
| 920 |
+
gr.update(visible=False), # Hide overlay - this should remove blocking
|
| 921 |
+
empty_chat(), # Reset chat
|
| 922 |
+
"✅ Challenge closed.", # Status message
|
| 923 |
+
gr.update(value=current_session.get("last_output", "")) # Keep main output
|
| 924 |
+
)
|
| 925 |
+
|
| 926 |
+
|
| 927 |
+
challenge_close_btn.click(
|
| 928 |
+
fn=close_challenge_discussion,
|
| 929 |
+
inputs=None,
|
| 930 |
+
outputs=[
|
| 931 |
+
challenge_modal_overlay, # visible=False
|
| 932 |
+
challenge_chat, # reset chat
|
| 933 |
+
challenge_status, # status text
|
| 934 |
+
output # main output (reevaluated decision)
|
| 935 |
+
]
|
| 936 |
+
).then(
|
| 937 |
+
fn=update_challenge_visibility,
|
| 938 |
+
inputs=None,
|
| 939 |
+
outputs=[challenge_btn]
|
| 940 |
+
)
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
if __name__ == "__main__":
|
| 944 |
+
demo.launch(
|
| 945 |
+
share=True,
|
| 946 |
+
server_name="0.0.0.0",
|
| 947 |
+
server_port=7860,
|
| 948 |
+
theme=gr.themes.Soft(),
|
| 949 |
+
css=modal_css
|
| 950 |
+
)
|
| 951 |
+
|
curriculum.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class Curriculum:
|
| 2 |
+
def __init__(self, topic, chapters=None):
|
| 3 |
+
self.topic = topic
|
| 4 |
+
self.chapters = chapters if chapters else [] # List of Chapter objects
|
| 5 |
+
|
| 6 |
+
def add_chapter(self, chapter):
|
| 7 |
+
self.chapters.append(chapter)
|
| 8 |
+
|
| 9 |
+
def print_curriculum(self):
|
| 10 |
+
print(f"\nCurriculum for: {self.topic}\n" + "="*40)
|
| 11 |
+
for i, chapter in enumerate(self.chapters, 1):
|
| 12 |
+
print(f"\nChapter {i}: {chapter.name}")
|
| 13 |
+
print("-" * (10 + len(chapter.name)))
|
| 14 |
+
for j, module in enumerate(chapter.modules, 1):
|
| 15 |
+
print(f" {i}.{j} {module.name}")
|
| 16 |
+
if hasattr(module, 'learning_objective') and module.learning_objective:
|
| 17 |
+
print(f" Learning Objective: {module.learning_objective}")
|
| 18 |
+
|
| 19 |
+
def __repr__(self):
|
| 20 |
+
return f"Curriculum(topic={self.topic}, chapters={self.chapters})"
|
| 21 |
+
|
| 22 |
+
class Chapter:
|
| 23 |
+
def __init__(self, name, modules=None):
|
| 24 |
+
self.name = name
|
| 25 |
+
self.modules = modules if modules else [] # List of Module objects
|
| 26 |
+
|
| 27 |
+
def add_module(self, module):
|
| 28 |
+
self.modules.append(module)
|
| 29 |
+
|
| 30 |
+
def __repr__(self):
|
| 31 |
+
return f"Chapter(name={self.name}, modules={self.modules})"
|
| 32 |
+
|
| 33 |
+
class Module:
|
| 34 |
+
def __init__(self, name, learning_objective=None, content=None):
|
| 35 |
+
self.name = name
|
| 36 |
+
self.learning_objective = learning_objective
|
| 37 |
+
self.content = content
|
| 38 |
+
|
| 39 |
+
def __repr__(self):
|
| 40 |
+
return f"Module(name={self.name}, learning_objective={self.learning_objective})"
|
| 41 |
+
|
main.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AI Teacher Bot - Complete CLI Interface
|
| 4 |
+
This is the main command-line interface for the AI Teacher Bot system.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
# Load environment variables
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
from user_state import UserState
|
| 17 |
+
from curriculum import Curriculum, Chapter, Module
|
| 18 |
+
from agents.level_assess import LevelAssessmentAgent
|
| 19 |
+
from agents.curriculum_planner import CurriculumPlannerAgent
|
| 20 |
+
from agents.coordinator import CoordinatorAgent
|
| 21 |
+
from agents.bloom_assess import BloomsAssessmentAgent
|
| 22 |
+
|
| 23 |
+
LEVELS = ['novice', 'intermediate', 'advanced']
|
| 24 |
+
|
| 25 |
+
def print_banner():
|
| 26 |
+
"""Print the application banner"""
|
| 27 |
+
print("=" * 60)
|
| 28 |
+
print("🧠 AI TEACHER BOT - COMPLETE LEARNING SYSTEM")
|
| 29 |
+
print("=" * 60)
|
| 30 |
+
print("Multi-Agent Adaptive Learning Platform")
|
| 31 |
+
print("Features: Level Assessment, Dynamic Curriculum, Bloom's Taxonomy")
|
| 32 |
+
print("=" * 60)
|
| 33 |
+
|
| 34 |
+
def check_api_key():
|
| 35 |
+
"""Check if OpenAI API key is configured"""
|
| 36 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 37 |
+
if not api_key or api_key == "your_openai_api_key_here":
|
| 38 |
+
print("❌ OpenAI API key not configured!")
|
| 39 |
+
print("Please set your OPENAI_API_KEY in the .env file")
|
| 40 |
+
print("Get your API key from: https://platform.openai.com/api-keys")
|
| 41 |
+
return False
|
| 42 |
+
return True
|
| 43 |
+
|
| 44 |
+
# Removed progress loading for now - focusing on core flow
|
| 45 |
+
|
| 46 |
+
def get_user_input():
|
| 47 |
+
"""Get topic and level from user"""
|
| 48 |
+
print("\n🎯 Let's start your learning journey!")
|
| 49 |
+
|
| 50 |
+
# Get topic
|
| 51 |
+
while True:
|
| 52 |
+
topic = input("\nWhat topic would you like to study? ").strip()
|
| 53 |
+
if topic:
|
| 54 |
+
break
|
| 55 |
+
print("Please enter a valid topic")
|
| 56 |
+
|
| 57 |
+
# Get level
|
| 58 |
+
print("\n📚 What's your current experience level?")
|
| 59 |
+
for i, level in enumerate(LEVELS, 1):
|
| 60 |
+
print(f" {i}. {level.capitalize()}")
|
| 61 |
+
|
| 62 |
+
while True:
|
| 63 |
+
try:
|
| 64 |
+
level_choice = int(input("Enter the number for your level: "))
|
| 65 |
+
if 1 <= level_choice <= len(LEVELS):
|
| 66 |
+
claimed_level = LEVELS[level_choice - 1]
|
| 67 |
+
break
|
| 68 |
+
else:
|
| 69 |
+
print("Please enter a valid number")
|
| 70 |
+
except ValueError:
|
| 71 |
+
print("Please enter a valid number")
|
| 72 |
+
|
| 73 |
+
return topic, claimed_level
|
| 74 |
+
|
| 75 |
+
def run_level_assessment(topic, claimed_level):
|
| 76 |
+
"""Run level assessment for non-novice users"""
|
| 77 |
+
print(f"\n📝 Assessing your knowledge level for {topic}...")
|
| 78 |
+
print("This will help us create the perfect learning path for you.")
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
assessor = LevelAssessmentAgent()
|
| 82 |
+
actual_level = assessor.process(topic, claimed_level)
|
| 83 |
+
return actual_level
|
| 84 |
+
except Exception as e:
|
| 85 |
+
print(f"❌ Error during assessment: {e}")
|
| 86 |
+
print("⚠️ Defaulting to novice level")
|
| 87 |
+
return "novice"
|
| 88 |
+
|
| 89 |
+
def generate_curriculum(topic, level):
|
| 90 |
+
"""Generate curriculum for the topic and level"""
|
| 91 |
+
print(f"\n📚 Generating personalized curriculum for {topic} at {level} level...")
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
planner = CurriculumPlannerAgent()
|
| 95 |
+
curriculum = planner.process(topic, level)
|
| 96 |
+
print("✅ Curriculum generated successfully!")
|
| 97 |
+
return curriculum
|
| 98 |
+
except Exception as e:
|
| 99 |
+
print(f"❌ Error generating curriculum: {e}")
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
"""Main application function - Core Learning Flow"""
|
| 104 |
+
print_banner()
|
| 105 |
+
|
| 106 |
+
# Check API key
|
| 107 |
+
if not check_api_key():
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
# Get user input for new session
|
| 111 |
+
topic, claimed_level = get_user_input()
|
| 112 |
+
user = UserState(topic=topic, claimed_level=claimed_level)
|
| 113 |
+
print(f"\n👤 User Profile: {user.topic} at {user.claimed_level} level")
|
| 114 |
+
|
| 115 |
+
# Level assessment
|
| 116 |
+
if claimed_level == 'novice':
|
| 117 |
+
user.set_actual_level("novice")
|
| 118 |
+
print("✅ Skipping level assessment for novice users")
|
| 119 |
+
else:
|
| 120 |
+
actual_level = run_level_assessment(topic, claimed_level)
|
| 121 |
+
user.set_actual_level(actual_level)
|
| 122 |
+
print(f"✅ Assessment complete! Your level: {actual_level}")
|
| 123 |
+
|
| 124 |
+
# Generate curriculum
|
| 125 |
+
curriculum = generate_curriculum(user.topic, user.actual_level)
|
| 126 |
+
if curriculum is None:
|
| 127 |
+
print("❌ Cannot proceed without curriculum")
|
| 128 |
+
return
|
| 129 |
+
|
| 130 |
+
# Display curriculum
|
| 131 |
+
print("\n" + "="*60)
|
| 132 |
+
print("📖 YOUR PERSONALIZED CURRICULUM")
|
| 133 |
+
print("="*60)
|
| 134 |
+
curriculum.print_curriculum()
|
| 135 |
+
|
| 136 |
+
# Start teaching
|
| 137 |
+
print(f"\n🚀 Starting your learning journey at {user.actual_level} level!")
|
| 138 |
+
print("The AI teacher will guide you through each module step by step.")
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
coordinator = CoordinatorAgent(user.actual_level, user)
|
| 142 |
+
coordinator.teach_curriculum(curriculum)
|
| 143 |
+
|
| 144 |
+
# Final summary
|
| 145 |
+
print("\n" + "="*60)
|
| 146 |
+
print("🎉 LEARNING SESSION COMPLETE!")
|
| 147 |
+
print("="*60)
|
| 148 |
+
print("You've completed your learning journey!")
|
| 149 |
+
print("Great job on mastering the concepts! 🌟")
|
| 150 |
+
|
| 151 |
+
except KeyboardInterrupt:
|
| 152 |
+
print("\n\n⏸️ Learning session stopped by user")
|
| 153 |
+
print("Thanks for learning with us!")
|
| 154 |
+
except Exception as e:
|
| 155 |
+
print(f"\n❌ Error during learning session: {e}")
|
| 156 |
+
print("Please try again or check your configuration.")
|
| 157 |
+
|
| 158 |
+
print("\n👋 Thank you for using AI Teacher Bot!")
|
| 159 |
+
print("Keep learning and growing! 🌱")
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
openai
|
| 3 |
+
python-dotenv
|
| 4 |
+
requests
|
user_state.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
class UserState:
|
| 5 |
+
def __init__(self, topic=None, claimed_level=None):
|
| 6 |
+
self.topic = topic
|
| 7 |
+
self.claimed_level = claimed_level # e.g., 'novice', 'intermediate', 'advanced'
|
| 8 |
+
self.actual_level = None # Set after assessment
|
| 9 |
+
self.progress = {
|
| 10 |
+
'modules_completed': {}, # {chapter_name: {module_name: status}}
|
| 11 |
+
'chapters_completed': {}, # {chapter_name: completion_status}
|
| 12 |
+
'bloom_assessments': {}, # {chapter_name: assessment_results}
|
| 13 |
+
'overall_progress': {
|
| 14 |
+
'total_modules': 0,
|
| 15 |
+
'completed_modules': 0,
|
| 16 |
+
'total_chapters': 0,
|
| 17 |
+
'completed_chapters': 0,
|
| 18 |
+
'average_bloom_score': 0
|
| 19 |
+
},
|
| 20 |
+
'session_data': {
|
| 21 |
+
'start_time': datetime.now().isoformat(),
|
| 22 |
+
'last_activity': datetime.now().isoformat()
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
def set_actual_level(self, level):
|
| 27 |
+
self.actual_level = level
|
| 28 |
+
|
| 29 |
+
@property
|
| 30 |
+
def display_level(self):
|
| 31 |
+
"""Returns the actual level if set, otherwise the claimed level"""
|
| 32 |
+
return self.actual_level or self.claimed_level
|
| 33 |
+
|
| 34 |
+
def update_module_progress(self, chapter_name, module_name, status="completed"):
|
| 35 |
+
"""Update the completion status of a module"""
|
| 36 |
+
if chapter_name not in self.progress['modules_completed']:
|
| 37 |
+
self.progress['modules_completed'][chapter_name] = {}
|
| 38 |
+
|
| 39 |
+
self.progress['modules_completed'][chapter_name][module_name] = {
|
| 40 |
+
'status': status,
|
| 41 |
+
'completed_at': datetime.now().isoformat()
|
| 42 |
+
}
|
| 43 |
+
self.progress['session_data']['last_activity'] = datetime.now().isoformat()
|
| 44 |
+
|
| 45 |
+
def update_chapter_progress(self, chapter_name, status="completed"):
|
| 46 |
+
"""Update the completion status of a chapter"""
|
| 47 |
+
self.progress['chapters_completed'][chapter_name] = {
|
| 48 |
+
'status': status,
|
| 49 |
+
'completed_at': datetime.now().isoformat()
|
| 50 |
+
}
|
| 51 |
+
self.progress['session_data']['last_activity'] = datetime.now().isoformat()
|
| 52 |
+
|
| 53 |
+
def update_bloom_assessment(self, chapter_name, assessment_results):
|
| 54 |
+
"""Store Bloom's assessment results for a chapter"""
|
| 55 |
+
# Store the assessment results directly without wrapping in 'results'
|
| 56 |
+
self.progress['bloom_assessments'][chapter_name] = assessment_results
|
| 57 |
+
self.progress['bloom_assessments'][chapter_name]['completed_at'] = datetime.now().isoformat()
|
| 58 |
+
self.progress['session_data']['last_activity'] = datetime.now().isoformat()
|
| 59 |
+
|
| 60 |
+
def calculate_overall_progress(self, curriculum):
|
| 61 |
+
"""Calculate overall progress based on curriculum"""
|
| 62 |
+
total_modules = sum(len(chapter.modules) for chapter in curriculum.chapters)
|
| 63 |
+
completed_modules = sum(
|
| 64 |
+
len([m for m in modules.values() if m.get('status') == 'completed'])
|
| 65 |
+
for modules in self.progress['modules_completed'].values()
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
total_chapters = len(curriculum.chapters)
|
| 69 |
+
completed_chapters = len([
|
| 70 |
+
c for c in self.progress['chapters_completed'].values()
|
| 71 |
+
if c.get('status') == 'completed'
|
| 72 |
+
])
|
| 73 |
+
|
| 74 |
+
# Calculate average Bloom's score
|
| 75 |
+
bloom_scores = [
|
| 76 |
+
assessment.get('average_score', 0)
|
| 77 |
+
for assessment in self.progress['bloom_assessments'].values()
|
| 78 |
+
]
|
| 79 |
+
average_bloom_score = sum(bloom_scores) / len(bloom_scores) if bloom_scores else 0
|
| 80 |
+
|
| 81 |
+
self.progress['overall_progress'] = {
|
| 82 |
+
'total_modules': total_modules,
|
| 83 |
+
'completed_modules': completed_modules,
|
| 84 |
+
'total_chapters': total_chapters,
|
| 85 |
+
'completed_chapters': completed_chapters,
|
| 86 |
+
'average_bloom_score': average_bloom_score,
|
| 87 |
+
'completion_percentage': (completed_modules / total_modules * 100) if total_modules > 0 else 0
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
def get_progress_summary(self, curriculum=None):
|
| 91 |
+
"""Get a formatted progress summary"""
|
| 92 |
+
if curriculum:
|
| 93 |
+
self.calculate_overall_progress(curriculum)
|
| 94 |
+
|
| 95 |
+
overall = self.progress['overall_progress']
|
| 96 |
+
|
| 97 |
+
summary = f"""
|
| 98 |
+
PROGRESS SUMMARY
|
| 99 |
+
===============
|
| 100 |
+
Topic: {self.topic}
|
| 101 |
+
Level: {self.actual_level or self.claimed_level}
|
| 102 |
+
|
| 103 |
+
Overall Progress:
|
| 104 |
+
- Modules: {overall['completed_modules']}/{overall['total_modules']} ({overall['completion_percentage']:.1f}%)
|
| 105 |
+
- Chapters: {overall['completed_chapters']}/{overall['total_chapters']}
|
| 106 |
+
- Average Bloom's Score: {overall['average_bloom_score']:.1f}/10
|
| 107 |
+
|
| 108 |
+
Chapter Progress:
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
for chapter_name, chapter_data in self.progress['chapters_completed'].items():
|
| 112 |
+
status = chapter_data.get('status', 'not_started')
|
| 113 |
+
summary += f"- {chapter_name}: {status}\n"
|
| 114 |
+
|
| 115 |
+
return summary
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def __repr__(self):
|
| 119 |
+
return f"UserState(topic={self.topic}, claimed_level={self.claimed_level}, actual_level={self.actual_level})"
|