Spaces:
Sleeping
Sleeping
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- main.py +124 -1
- 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
|
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 |
+
}
|