Meet Patel commited on
Commit
0228818
·
1 Parent(s): 14940e1

Step 4: Added assessment and analytics features with question generation and performance tracking

Browse files
Files changed (2) hide show
  1. main.py +124 -1
  2. utils/assessment.py +357 -0
main.py CHANGED
@@ -4,13 +4,19 @@ import json
4
  from typing import List, Dict, Any, Optional
5
  from datetime import datetime
6
 
7
- # Import utility functions for multi-modal interactions
8
  from utils.multimodal import (
9
  process_text_query,
10
  process_voice_input,
11
  process_handwriting,
12
  generate_speech_response
13
  )
 
 
 
 
 
 
14
 
15
  # Create the TutorX MCP server
16
  mcp = FastMCP("TutorX")
@@ -455,5 +461,122 @@ def handwriting_recognition(image_data_base64: str, student_id: str) -> Dict[str
455
  "timestamp": datetime.now().isoformat()
456
  }
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  if __name__ == "__main__":
459
  mcp.run()
 
4
  from typing import List, Dict, Any, Optional
5
  from datetime import datetime
6
 
7
+ # Import utility functions
8
  from utils.multimodal import (
9
  process_text_query,
10
  process_voice_input,
11
  process_handwriting,
12
  generate_speech_response
13
  )
14
+ from utils.assessment import (
15
+ generate_question,
16
+ evaluate_student_answer,
17
+ generate_performance_analytics,
18
+ detect_plagiarism
19
+ )
20
 
21
  # Create the TutorX MCP server
22
  mcp = FastMCP("TutorX")
 
461
  "timestamp": datetime.now().isoformat()
462
  }
463
 
464
+ # ------------------ Advanced Assessment Tools ------------------
465
+
466
+ @mcp.tool()
467
+ def create_assessment(concept_ids: List[str], num_questions: int, difficulty: int = 3) -> Dict[str, Any]:
468
+ """
469
+ Create a complete assessment for given concepts
470
+
471
+ Args:
472
+ concept_ids: List of concept IDs to include
473
+ num_questions: Number of questions to generate
474
+ difficulty: Difficulty level (1-5)
475
+
476
+ Returns:
477
+ Complete assessment with questions
478
+ """
479
+ questions = []
480
+
481
+ # Distribute questions evenly among concepts
482
+ questions_per_concept = num_questions // len(concept_ids)
483
+ extra_questions = num_questions % len(concept_ids)
484
+
485
+ for i, concept_id in enumerate(concept_ids):
486
+ # Determine how many questions for this concept
487
+ concept_questions = questions_per_concept
488
+ if i < extra_questions:
489
+ concept_questions += 1
490
+
491
+ # Generate questions for this concept
492
+ for _ in range(concept_questions):
493
+ questions.append(generate_question(concept_id, difficulty))
494
+
495
+ return {
496
+ "assessment_id": f"assessment_{datetime.now().strftime('%Y%m%d%H%M%S')}",
497
+ "concept_ids": concept_ids,
498
+ "difficulty": difficulty,
499
+ "num_questions": len(questions),
500
+ "questions": questions,
501
+ "created_at": datetime.now().isoformat()
502
+ }
503
+
504
+ @mcp.tool()
505
+ def grade_assessment(assessment_id: str, student_answers: Dict[str, str], questions: List[Dict[str, Any]]) -> Dict[str, Any]:
506
+ """
507
+ Grade a completed assessment
508
+
509
+ Args:
510
+ assessment_id: The ID of the assessment
511
+ student_answers: Dictionary mapping question IDs to student answers
512
+ questions: List of question objects
513
+
514
+ Returns:
515
+ Grading results
516
+ """
517
+ results = []
518
+ correct_count = 0
519
+
520
+ for question in questions:
521
+ question_id = question["id"]
522
+ if question_id in student_answers:
523
+ evaluation = evaluate_student_answer(question, student_answers[question_id])
524
+ results.append(evaluation)
525
+ if evaluation["is_correct"]:
526
+ correct_count += 1
527
+
528
+ # Calculate score
529
+ score = correct_count / len(questions) if questions else 0
530
+
531
+ # Analyze error patterns
532
+ error_types = {}
533
+ for result in results:
534
+ if result["error_type"]:
535
+ error_type = result["error_type"]
536
+ error_types[error_type] = error_types.get(error_type, 0) + 1
537
+
538
+ # Find most common error
539
+ most_common_error = None
540
+ if error_types:
541
+ most_common_error = max(error_types.items(), key=lambda x: x[1])
542
+
543
+ return {
544
+ "assessment_id": assessment_id,
545
+ "score": score,
546
+ "correct_count": correct_count,
547
+ "total_questions": len(questions),
548
+ "results": results,
549
+ "most_common_error": most_common_error,
550
+ "completed_at": datetime.now().isoformat()
551
+ }
552
+
553
+ @mcp.tool()
554
+ def get_student_analytics(student_id: str, timeframe_days: int = 30) -> Dict[str, Any]:
555
+ """
556
+ Get comprehensive analytics for a student
557
+
558
+ Args:
559
+ student_id: The student's unique identifier
560
+ timeframe_days: Number of days to include in analysis
561
+
562
+ Returns:
563
+ Performance analytics
564
+ """
565
+ return generate_performance_analytics(student_id, timeframe_days)
566
+
567
+ @mcp.tool()
568
+ def check_submission_originality(submission: str, reference_sources: List[str]) -> Dict[str, Any]:
569
+ """
570
+ Check student submission for potential plagiarism
571
+
572
+ Args:
573
+ submission: The student's submission text
574
+ reference_sources: List of reference texts to check against
575
+
576
+ Returns:
577
+ Originality analysis
578
+ """
579
+ return detect_plagiarism(submission, reference_sources)
580
+
581
  if __name__ == "__main__":
582
  mcp.run()
utils/assessment.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Assessment and analytics utilities for the TutorX MCP server.
3
+ """
4
+
5
+ from typing import Dict, Any, List, Optional, Tuple
6
+ from datetime import datetime, timedelta
7
+ import random
8
+ import json
9
+
10
+ def generate_question(concept_id: str, difficulty: int) -> Dict[str, Any]:
11
+ """
12
+ Generate a question for a specific concept at the given difficulty level
13
+
14
+ Args:
15
+ concept_id: The concept identifier
16
+ difficulty: Difficulty level from 1-5
17
+
18
+ Returns:
19
+ Question object
20
+ """
21
+ # In a real implementation, this would use templates and context to generate appropriate questions
22
+ # Here we'll simulate with some hardcoded examples
23
+
24
+ question_templates = {
25
+ "math_algebra_basics": [
26
+ {
27
+ "text": "Simplify: {a}x + {b}x",
28
+ "variables": {"a": (1, 10), "b": (1, 10)},
29
+ "solution_template": "{a}x + {b}x = {sum}x",
30
+ "answer_template": "{sum}x"
31
+ },
32
+ {
33
+ "text": "Solve: x + {a} = {b}",
34
+ "variables": {"a": (1, 20), "b": (5, 30)},
35
+ "solution_template": "x + {a} = {b}\nx = {b} - {a}\nx = {answer}",
36
+ "answer_template": "x = {answer}"
37
+ }
38
+ ],
39
+ "math_algebra_linear_equations": [
40
+ {
41
+ "text": "Solve for x: {a}x + {b} = {c}",
42
+ "variables": {"a": (2, 5), "b": (1, 10), "c": (10, 30)},
43
+ "solution_template": "{a}x + {b} = {c}\n{a}x = {c} - {b}\n{a}x = {c_minus_b}\nx = {c_minus_b} / {a}\nx = {answer}",
44
+ "answer_template": "x = {answer}"
45
+ }
46
+ ],
47
+ "math_algebra_quadratic_equations": [
48
+ {
49
+ "text": "Solve: x² + {b}x + {c} = 0",
50
+ "variables": {"b": (-10, 10), "c": (-20, 20)},
51
+ "solution_template": "Using the quadratic formula: x = (-b ± √(b² - 4ac)) / 2a\nWith a=1, b={b}, c={c}:\nx = (-({b}) ± √(({b})² - 4(1)({c}))) / 2(1)\nx = (-{b} ± √({b_squared} - {four_c})) / 2\nx = (-{b} ± √{discriminant}) / 2\nx = (-{b} ± {sqrt_discriminant}) / 2\nx = ({neg_b_plus_sqrt} / 2) or x = ({neg_b_minus_sqrt} / 2)\nx = {answer1} or x = {answer2}",
52
+ "answer_template": "x = {answer1} or x = {answer2}"
53
+ }
54
+ ]
55
+ }
56
+
57
+ # Select a template based on concept_id or return a default
58
+ templates = question_templates.get(concept_id, [
59
+ {
60
+ "text": "Define the term: {concept}",
61
+ "variables": {"concept": concept_id.replace("_", " ")},
62
+ "solution_template": "Definition of {concept}",
63
+ "answer_template": "Definition varies"
64
+ }
65
+ ])
66
+
67
+ # Select a template based on difficulty
68
+ template_index = min(int(difficulty / 2), len(templates) - 1)
69
+ template = templates[template_index]
70
+
71
+ # Fill in template variables
72
+ variables = {}
73
+ for var_name, var_range in template.get("variables", {}).items():
74
+ if isinstance(var_range, tuple) and len(var_range) == 2:
75
+ # For numeric ranges
76
+ variables[var_name] = random.randint(var_range[0], var_range[1])
77
+ else:
78
+ # For non-numeric values
79
+ variables[var_name] = var_range
80
+
81
+ # Process the variables further for the solution
82
+ solution_vars = dict(variables)
83
+
84
+ # For algebra basics
85
+ if concept_id == "math_algebra_basics" and "a" in variables and "b" in variables:
86
+ solution_vars["sum"] = variables["a"] + variables["b"]
87
+ solution_vars["answer"] = variables["b"] - variables["a"]
88
+
89
+ # For linear equations
90
+ if concept_id == "math_algebra_linear_equations" and all(k in variables for k in ["a", "b", "c"]):
91
+ solution_vars["c_minus_b"] = variables["c"] - variables["b"]
92
+ solution_vars["answer"] = (variables["c"] - variables["b"]) / variables["a"]
93
+
94
+ # For quadratic equations
95
+ if concept_id == "math_algebra_quadratic_equations" and all(k in variables for k in ["b", "c"]):
96
+ a = 1 # Assuming a=1 for simplicity
97
+ b = variables["b"]
98
+ c = variables["c"]
99
+ solution_vars["b_squared"] = b**2
100
+ solution_vars["four_c"] = 4 * c
101
+ solution_vars["discriminant"] = b**2 - 4*a*c
102
+
103
+ if solution_vars["discriminant"] >= 0:
104
+ solution_vars["sqrt_discriminant"] = round(solution_vars["discriminant"] ** 0.5, 3)
105
+ solution_vars["neg_b_plus_sqrt"] = -b + solution_vars["sqrt_discriminant"]
106
+ solution_vars["neg_b_minus_sqrt"] = -b - solution_vars["sqrt_discriminant"]
107
+ solution_vars["answer1"] = round((-b + solution_vars["sqrt_discriminant"]) / (2*a), 3)
108
+ solution_vars["answer2"] = round((-b - solution_vars["sqrt_discriminant"]) / (2*a), 3)
109
+ else:
110
+ # Complex roots
111
+ solution_vars["sqrt_discriminant"] = f"{round((-solution_vars['discriminant']) ** 0.5, 3)}i"
112
+ solution_vars["answer1"] = f"{round(-b/(2*a), 3)} + {round(((-solution_vars['discriminant']) ** 0.5)/(2*a), 3)}i"
113
+ solution_vars["answer2"] = f"{round(-b/(2*a), 3)} - {round(((-solution_vars['discriminant']) ** 0.5)/(2*a), 3)}i"
114
+
115
+ # Format text and solution
116
+ text = template["text"].format(**variables)
117
+ solution = template["solution_template"].format(**solution_vars) if "solution_template" in template else ""
118
+ answer = template["answer_template"].format(**solution_vars) if "answer_template" in template else ""
119
+
120
+ return {
121
+ "id": f"q_{concept_id}_{random.randint(1000, 9999)}",
122
+ "concept_id": concept_id,
123
+ "difficulty": difficulty,
124
+ "text": text,
125
+ "solution": solution,
126
+ "answer": answer,
127
+ "variables": variables
128
+ }
129
+
130
+
131
+ def evaluate_student_answer(question: Dict[str, Any], student_answer: str) -> Dict[str, Any]:
132
+ """
133
+ Evaluate a student's answer to a question
134
+
135
+ Args:
136
+ question: The question object
137
+ student_answer: The student's answer as a string
138
+
139
+ Returns:
140
+ Evaluation results
141
+ """
142
+ # In a real implementation, this would use NLP and math parsing to evaluate the answer
143
+ # Here we'll do a simple string comparison with some basic normalization
144
+
145
+ def normalize_answer(answer):
146
+ """Normalize an answer string for comparison"""
147
+ return (answer.lower()
148
+ .replace(" ", "")
149
+ .replace("x=", "")
150
+ .replace("y=", ""))
151
+
152
+ correct_answer = normalize_answer(question["answer"])
153
+ student_answer_norm = normalize_answer(student_answer)
154
+
155
+ # Simple exact match for now
156
+ is_correct = student_answer_norm == correct_answer
157
+
158
+ # In a real implementation, we would have partial matching and error analysis
159
+ error_type = None
160
+ if not is_correct:
161
+ # Try to guess error type - very simplified example
162
+ if question["concept_id"] == "math_algebra_linear_equations":
163
+ # Check for sign error
164
+ if "-" in correct_answer and "+" in student_answer_norm:
165
+ error_type = "sign_error"
166
+ # Check for arithmetic error (within 20% of correct value)
167
+ elif student_answer_norm.replace("-", "").isdigit() and correct_answer.replace("-", "").isdigit():
168
+ try:
169
+ student_val = float(student_answer_norm)
170
+ correct_val = float(correct_answer)
171
+ if abs((student_val - correct_val) / correct_val) < 0.2:
172
+ error_type = "arithmetic_error"
173
+ except (ValueError, ZeroDivisionError):
174
+ pass
175
+
176
+ return {
177
+ "question_id": question["id"],
178
+ "is_correct": is_correct,
179
+ "error_type": error_type,
180
+ "correct_answer": question["answer"],
181
+ "student_answer": student_answer,
182
+ "timestamp": datetime.now().isoformat()
183
+ }
184
+
185
+
186
+ def generate_performance_analytics(student_id: str, timeframe_days: int = 30) -> Dict[str, Any]:
187
+ """
188
+ Generate performance analytics for a student
189
+
190
+ Args:
191
+ student_id: The student's unique identifier
192
+ timeframe_days: Number of days to include in the analysis
193
+
194
+ Returns:
195
+ Performance analytics
196
+ """
197
+ # In a real implementation, this would query a database
198
+ # Here we'll generate sample data
199
+
200
+ # Generate some sample data points over the timeframe
201
+ start_date = datetime.now() - timedelta(days=timeframe_days)
202
+ data_points = []
203
+
204
+ # Simulate an improving learning curve
205
+ accuracy_base = 0.65
206
+ speed_base = 120 # seconds
207
+
208
+ for day in range(timeframe_days):
209
+ current_date = start_date + timedelta(days=day)
210
+
211
+ # Simulate improvement over time with some random variation
212
+ improvement_factor = min(day / timeframe_days * 0.3, 0.3) # Max 30% improvement
213
+ random_variation = random.uniform(-0.05, 0.05)
214
+
215
+ accuracy = min(accuracy_base + improvement_factor + random_variation, 0.98)
216
+ speed = max(speed_base * (1 - improvement_factor) + random.uniform(-10, 10), 30)
217
+
218
+ # Generate 1-3 data points per day
219
+ daily_points = random.randint(1, 3)
220
+ for _ in range(daily_points):
221
+ hour = random.randint(9, 20) # Between 9 AM and 8 PM
222
+ timestamp = current_date.replace(hour=hour, minute=random.randint(0, 59))
223
+
224
+ data_points.append({
225
+ "timestamp": timestamp.isoformat(),
226
+ "accuracy": round(accuracy, 2),
227
+ "speed_seconds": round(speed),
228
+ "difficulty": random.randint(1, 5),
229
+ "concepts": [f"concept_{random.randint(1, 10)}" for _ in range(random.randint(1, 3))]
230
+ })
231
+
232
+ # Calculate aggregate metrics
233
+ if data_points:
234
+ avg_accuracy = sum(point["accuracy"] for point in data_points) / len(data_points)
235
+ avg_speed = sum(point["speed_seconds"] for point in data_points) / len(data_points)
236
+
237
+ # Calculate improvement
238
+ first_week = [p for p in data_points if datetime.fromisoformat(p["timestamp"]) < start_date + timedelta(days=7)]
239
+ last_week = [p for p in data_points if datetime.fromisoformat(p["timestamp"]) > datetime.now() - timedelta(days=7)]
240
+
241
+ accuracy_improvement = 0
242
+ speed_improvement = 0
243
+
244
+ if first_week and last_week:
245
+ first_week_acc = sum(p["accuracy"] for p in first_week) / len(first_week)
246
+ last_week_acc = sum(p["accuracy"] for p in last_week) / len(last_week)
247
+ accuracy_improvement = round((last_week_acc - first_week_acc) * 100, 1)
248
+
249
+ first_week_speed = sum(p["speed_seconds"] for p in first_week) / len(first_week)
250
+ last_week_speed = sum(p["speed_seconds"] for p in last_week) / len(last_week)
251
+ speed_improvement = round((first_week_speed - last_week_speed) / first_week_speed * 100, 1)
252
+ else:
253
+ avg_accuracy = 0
254
+ avg_speed = 0
255
+ accuracy_improvement = 0
256
+ speed_improvement = 0
257
+
258
+ # Compile strengths and weaknesses
259
+ concept_performance = {}
260
+ for point in data_points:
261
+ for concept in point["concepts"]:
262
+ if concept not in concept_performance:
263
+ concept_performance[concept] = {"total": 0, "correct": 0}
264
+ concept_performance[concept]["total"] += 1
265
+ concept_performance[concept]["correct"] += point["accuracy"]
266
+
267
+ strengths = []
268
+ weaknesses = []
269
+
270
+ for concept, perf in concept_performance.items():
271
+ avg = perf["correct"] / perf["total"] if perf["total"] > 0 else 0
272
+ if avg > 0.85 and perf["total"] >= 3:
273
+ strengths.append(concept)
274
+ elif avg < 0.7 and perf["total"] >= 3:
275
+ weaknesses.append(concept)
276
+
277
+ return {
278
+ "student_id": student_id,
279
+ "timeframe_days": timeframe_days,
280
+ "metrics": {
281
+ "avg_accuracy": round(avg_accuracy * 100, 1),
282
+ "avg_speed_seconds": round(avg_speed, 1),
283
+ "accuracy_improvement": accuracy_improvement, # percentage points
284
+ "speed_improvement": speed_improvement, # percentage
285
+ "total_questions_attempted": len(data_points),
286
+ "study_sessions": len(set(p["timestamp"].split("T")[0] for p in data_points))
287
+ },
288
+ "strengths": strengths[:3], # Top 3 strengths
289
+ "weaknesses": weaknesses[:3], # Top 3 weaknesses
290
+ "learning_style": "visual" if random.random() > 0.5 else "interactive",
291
+ "recommendations": [
292
+ "Focus on quadratic equations",
293
+ "Try more word problems",
294
+ "Schedule a tutoring session for challenging topics"
295
+ ],
296
+ "generated_at": datetime.now().isoformat()
297
+ }
298
+
299
+
300
+ def detect_plagiarism(submission: str, reference_sources: List[str]) -> Dict[str, Any]:
301
+ """
302
+ Check for potential plagiarism in a student's submission
303
+
304
+ Args:
305
+ submission: The student's submission
306
+ reference_sources: List of reference sources to check against
307
+
308
+ Returns:
309
+ Plagiarism analysis
310
+ """
311
+ # In a real implementation, this would use sophisticated text comparison
312
+ # Here we'll do a simple similarity check
313
+
314
+ def normalize_text(text):
315
+ return text.lower().replace(" ", "")
316
+
317
+ norm_submission = normalize_text(submission)
318
+ matches = []
319
+
320
+ for i, source in enumerate(reference_sources):
321
+ norm_source = normalize_text(source)
322
+
323
+ # Check for exact substring matches of significant length
324
+ min_match_length = 30 # Characters
325
+
326
+ for start in range(len(norm_submission) - min_match_length + 1):
327
+ chunk = norm_submission[start:start + min_match_length]
328
+ if chunk in norm_source:
329
+ source_start = norm_source.find(chunk)
330
+
331
+ # Try to extend the match
332
+ match_length = min_match_length
333
+ while (start + match_length < len(norm_submission) and
334
+ source_start + match_length < len(norm_source) and
335
+ norm_submission[start + match_length] == norm_source[source_start + match_length]):
336
+ match_length += 1
337
+
338
+ matches.append({
339
+ "source_index": i,
340
+ "source_start": source_start,
341
+ "submission_start": start,
342
+ "length": match_length,
343
+ "match_text": submission[start:start + match_length]
344
+ })
345
+
346
+ # Calculate overall similarity
347
+ total_matched_chars = sum(match["length"] for match in matches)
348
+ similarity_score = min(total_matched_chars / len(submission) if submission else 0, 1.0)
349
+
350
+ return {
351
+ "similarity_score": round(similarity_score, 2),
352
+ "plagiarism_detected": similarity_score > 0.2,
353
+ "suspicious_threshold": 0.2,
354
+ "matches": matches,
355
+ "recommendation": "Review academic integrity guidelines" if similarity_score > 0.2 else "No issues detected",
356
+ "timestamp": datetime.now().isoformat()
357
+ }