Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
@@ -252,158 +252,84 @@ class LearningStyleQuiz:
|
|
252 |
# Initialize learning style quiz
|
253 |
learning_style_quiz = LearningStyleQuiz()
|
254 |
|
255 |
-
# ========== MODEL LOADER ==========
|
256 |
-
class ModelLoader:
|
257 |
-
def __init__(self):
|
258 |
-
self.model = None
|
259 |
-
self.tokenizer = None
|
260 |
-
self.loaded = False
|
261 |
-
self.loading = False
|
262 |
-
self.error = None
|
263 |
-
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
264 |
-
self.load_attempts = 0
|
265 |
-
self.max_retries = 3
|
266 |
-
|
267 |
-
def load_model(self, progress: gr.Progress = None) -> Tuple[Optional[AutoModelForCausalLM], Optional[AutoTokenizer]]:
|
268 |
-
if self.loaded:
|
269 |
-
return self.model, self.tokenizer
|
270 |
-
|
271 |
-
if self.loading:
|
272 |
-
while self.loading and self.load_attempts < self.max_retries:
|
273 |
-
time.sleep(0.5)
|
274 |
-
return self.model, self.tokenizer
|
275 |
-
|
276 |
-
self.loading = True
|
277 |
-
self.load_attempts += 1
|
278 |
-
|
279 |
-
try:
|
280 |
-
if progress:
|
281 |
-
progress(0.1, desc="Initializing model environment...")
|
282 |
-
|
283 |
-
if self.device == "cuda":
|
284 |
-
torch.cuda.empty_cache()
|
285 |
-
torch.cuda.reset_peak_memory_stats()
|
286 |
-
|
287 |
-
if progress:
|
288 |
-
progress(0.2, desc="Loading tokenizer...")
|
289 |
-
|
290 |
-
tokenizer = None
|
291 |
-
for attempt in range(3):
|
292 |
-
try:
|
293 |
-
tokenizer = AutoTokenizer.from_pretrained(
|
294 |
-
MODEL_NAME,
|
295 |
-
trust_remote_code=True,
|
296 |
-
use_fast=True
|
297 |
-
)
|
298 |
-
break
|
299 |
-
except Exception as e:
|
300 |
-
if attempt == 2:
|
301 |
-
raise
|
302 |
-
logger.warning(f"Tokenizer loading attempt {attempt + 1} failed: {str(e)}")
|
303 |
-
time.sleep(2 ** attempt)
|
304 |
-
|
305 |
-
if progress:
|
306 |
-
progress(0.5, desc="Loading model (this may take a few minutes)...")
|
307 |
-
|
308 |
-
model_kwargs = {
|
309 |
-
"trust_remote_code": True,
|
310 |
-
"torch_dtype": torch.float16 if self.device == "cuda" else torch.float32,
|
311 |
-
"device_map": "auto" if self.device == "cuda" else None,
|
312 |
-
"low_cpu_mem_usage": True,
|
313 |
-
"offload_folder": "offload"
|
314 |
-
}
|
315 |
-
|
316 |
-
if torch.cuda.device_count() > 1:
|
317 |
-
model_kwargs["max_memory"] = {i: "20GiB" for i in range(torch.cuda.device_count())}
|
318 |
-
|
319 |
-
model = None
|
320 |
-
for attempt in range(3):
|
321 |
-
try:
|
322 |
-
model = AutoModelForCausalLM.from_pretrained(
|
323 |
-
MODEL_NAME,
|
324 |
-
**model_kwargs
|
325 |
-
)
|
326 |
-
break
|
327 |
-
except torch.cuda.OutOfMemoryError:
|
328 |
-
logger.warning("CUDA OOM encountered, trying CPU offloading")
|
329 |
-
model_kwargs["device_map"] = None
|
330 |
-
model = AutoModelForCausalLM.from_pretrained(
|
331 |
-
MODEL_NAME,
|
332 |
-
**model_kwargs
|
333 |
-
).to('cpu')
|
334 |
-
self.device = 'cpu'
|
335 |
-
break
|
336 |
-
except Exception as e:
|
337 |
-
if attempt == 2:
|
338 |
-
raise
|
339 |
-
logger.warning(f"Model loading attempt {attempt + 1} failed: {str(e)}")
|
340 |
-
time.sleep(2 ** attempt)
|
341 |
-
|
342 |
-
if progress:
|
343 |
-
progress(0.8, desc="Verifying model...")
|
344 |
-
test_input = tokenizer("Test", return_tensors="pt").to(self.device)
|
345 |
-
with torch.no_grad():
|
346 |
-
_ = model.generate(**test_input, max_new_tokens=1)
|
347 |
-
|
348 |
-
self.model = model.eval()
|
349 |
-
self.tokenizer = tokenizer
|
350 |
-
self.loaded = True
|
351 |
-
logger.info("Model loaded successfully")
|
352 |
-
|
353 |
-
return model, tokenizer
|
354 |
-
|
355 |
-
except Exception as e:
|
356 |
-
self.error = f"Model loading failed after {self.load_attempts} attempts: {str(e)}"
|
357 |
-
logger.error(self.error)
|
358 |
-
if self.load_attempts < self.max_retries:
|
359 |
-
logger.info(f"Retrying model loading ({self.load_attempts}/{self.max_retries})")
|
360 |
-
time.sleep(5)
|
361 |
-
return self.load_model(progress)
|
362 |
-
return None, None
|
363 |
-
finally:
|
364 |
-
self.loading = False
|
365 |
-
|
366 |
-
# Initialize model loader
|
367 |
-
model_loader = ModelLoader()
|
368 |
-
|
369 |
-
@lru_cache(maxsize=1)
|
370 |
-
def get_model_and_tokenizer():
|
371 |
-
return model_loader.load_model()
|
372 |
-
|
373 |
# ========== TRANSCRIPT PARSER ==========
|
374 |
class MiamiDadeTranscriptParser:
|
375 |
def __init__(self):
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
385 |
|
386 |
-
self.
|
387 |
-
|
388 |
-
|
389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
390 |
|
391 |
def parse_transcript(self, file_path: str) -> Dict:
|
392 |
-
"""Parse Miami-Dade
|
393 |
with pdfplumber.open(file_path) as pdf:
|
394 |
text = "\n".join(page.extract_text() for page in pdf.pages)
|
395 |
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
'course_history': self._parse_courses(text)
|
400 |
-
}
|
401 |
|
402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
|
404 |
-
def
|
405 |
-
"""Extract student information"""
|
406 |
-
match = self.
|
407 |
if not match:
|
408 |
return {}
|
409 |
|
@@ -418,10 +344,10 @@ class MiamiDadeTranscriptParser:
|
|
418 |
'district': 'Miami-Dade'
|
419 |
}
|
420 |
|
421 |
-
def
|
422 |
-
"""Parse graduation requirements section"""
|
423 |
requirements = {}
|
424 |
-
for match in self.
|
425 |
requirements[match.group(1).strip()] = {
|
426 |
'description': match.group(2).strip(),
|
427 |
'required': float(match.group(3)),
|
@@ -431,10 +357,10 @@ class MiamiDadeTranscriptParser:
|
|
431 |
}
|
432 |
return requirements
|
433 |
|
434 |
-
def
|
435 |
-
"""Parse course history section"""
|
436 |
courses = []
|
437 |
-
for match in self.
|
438 |
courses.append({
|
439 |
'requirement': match.group(1).strip(),
|
440 |
'school_year': match.group(2),
|
@@ -449,6 +375,122 @@ class MiamiDadeTranscriptParser:
|
|
449 |
})
|
450 |
return courses
|
451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
452 |
# Initialize transcript parser
|
453 |
transcript_parser = MiamiDadeTranscriptParser()
|
454 |
|
@@ -476,8 +518,12 @@ class AcademicAnalyzer:
|
|
476 |
}
|
477 |
|
478 |
try:
|
479 |
-
|
480 |
-
|
|
|
|
|
|
|
|
|
481 |
|
482 |
if weighted_gpa >= 4.5:
|
483 |
analysis['rating'] = 'Excellent'
|
@@ -553,30 +599,59 @@ class AcademicAnalyzer:
|
|
553 |
}
|
554 |
|
555 |
try:
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
if
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
'
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
580 |
|
581 |
current_grade = parsed_data.get('student_info', {}).get('grade', '')
|
582 |
grad_year = parsed_data.get('student_info', {}).get('year_of_graduation', '')
|
@@ -598,7 +673,7 @@ class AcademicAnalyzer:
|
|
598 |
analysis['on_track'] = False
|
599 |
|
600 |
if current_grade and grad_year:
|
601 |
-
remaining_credits = total_required -
|
602 |
years_remaining = int(grad_year) - datetime.datetime.now().year - int(current_grade)
|
603 |
|
604 |
if years_remaining > 0:
|
@@ -630,23 +705,27 @@ class AcademicAnalyzer:
|
|
630 |
}
|
631 |
|
632 |
try:
|
633 |
-
|
634 |
-
|
635 |
-
|
|
|
|
|
|
|
|
|
636 |
analysis['ap_courses'] += 1
|
637 |
analysis['advanced_courses'] += 1
|
638 |
-
elif 'IB' in
|
639 |
analysis['ib_courses'] += 1
|
640 |
analysis['advanced_courses'] += 1
|
641 |
-
elif 'DE' in
|
642 |
analysis['de_courses'] += 1
|
643 |
analysis['advanced_courses'] += 1
|
644 |
-
elif 'HONORS' in
|
645 |
analysis['honors_courses'] += 1
|
646 |
analysis['advanced_courses'] += 1
|
647 |
|
648 |
total_advanced = analysis['advanced_courses']
|
649 |
-
total_courses = len(
|
650 |
|
651 |
if total_courses == 0:
|
652 |
return analysis
|
@@ -701,9 +780,14 @@ class AcademicAnalyzer:
|
|
701 |
}
|
702 |
|
703 |
try:
|
704 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
705 |
rigor_analysis = self.analyze_course_rigor(parsed_data)
|
706 |
-
service_hours = int(parsed_data.get('student_info', {}).get('community_service_hours', 0))
|
707 |
|
708 |
if weighted_gpa >= 4.3 and rigor_analysis['advanced_courses'] >= 8 and service_hours >= 100:
|
709 |
recommendations['reach'].extend([
|
@@ -799,7 +883,8 @@ class AcademicAnalyzer:
|
|
799 |
try:
|
800 |
current_courses = [
|
801 |
course for course in parsed_data.get('course_history', [])
|
802 |
-
if course.get('status', '').lower() == 'in progress'
|
|
|
803 |
]
|
804 |
|
805 |
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
@@ -839,8 +924,9 @@ class AcademicAnalyzer:
|
|
839 |
for i, course in enumerate(current_courses):
|
840 |
day_index = i % 5
|
841 |
day = days[day_index]
|
|
|
842 |
plan['weekly_schedule'][day].append({
|
843 |
-
'course':
|
844 |
'duration': '45-60 minutes',
|
845 |
'activities': [
|
846 |
"Review notes",
|
@@ -889,12 +975,16 @@ class DataVisualizer:
|
|
889 |
|
890 |
def create_gpa_visualization(self, parsed_data: Dict):
|
891 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
892 |
gpa_data = {
|
893 |
"Type": ["Weighted GPA", "Unweighted GPA"],
|
894 |
-
"Value": [
|
895 |
-
float(parsed_data.get('student_info', {}).get('weighted_gpa', 0)),
|
896 |
-
float(parsed_data.get('student_info', {}).get('unweighted_gpa', 0))
|
897 |
-
],
|
898 |
"Color": [self.color_palette['gpa_weighted'], self.color_palette['gpa_unweighted']]
|
899 |
}
|
900 |
|
@@ -941,18 +1031,33 @@ class DataVisualizer:
|
|
941 |
|
942 |
def create_requirements_visualization(self, parsed_data: Dict):
|
943 |
try:
|
944 |
-
|
945 |
-
|
946 |
-
|
947 |
-
|
948 |
-
|
949 |
-
|
950 |
-
|
951 |
-
|
952 |
-
|
953 |
-
|
954 |
-
|
955 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
956 |
|
957 |
if not req_data:
|
958 |
return None
|
@@ -999,20 +1104,35 @@ class DataVisualizer:
|
|
999 |
|
1000 |
def create_credits_distribution_visualization(self, parsed_data: Dict):
|
1001 |
try:
|
1002 |
-
|
1003 |
-
|
1004 |
-
|
1005 |
-
|
1006 |
-
|
1007 |
-
|
1008 |
-
|
1009 |
-
|
1010 |
-
|
1011 |
-
|
1012 |
-
|
1013 |
-
|
1014 |
-
|
1015 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1016 |
|
1017 |
credit_values = [core_credits, elective_credits, other_credits]
|
1018 |
credit_labels = ['Core Subjects', 'Electives', 'Arts/PE']
|
@@ -1171,7 +1291,7 @@ class EnhancedProfileManager:
|
|
1171 |
"study_plan": study_plan if study_plan else {},
|
1172 |
"session_token": self.current_session,
|
1173 |
"last_updated": time.time(),
|
1174 |
-
"version": "2.
|
1175 |
}
|
1176 |
|
1177 |
filepath = self.get_profile_path(name)
|
@@ -1237,7 +1357,7 @@ class EnhancedProfileManager:
|
|
1237 |
if time.time() - profile_data.get('last_updated', 0) > SESSION_TIMEOUT:
|
1238 |
raise gr.Error("Session expired. Please start a new session.")
|
1239 |
|
1240 |
-
if profile_data.get('version', '1.0')
|
1241 |
try:
|
1242 |
profile_data['name'] = self.encryptor.decrypt(profile_data['name'])
|
1243 |
profile_data['interests'] = self.encryptor.decrypt(profile_data.get('interests', ''))
|
@@ -1270,7 +1390,7 @@ class EnhancedProfileManager:
|
|
1270 |
try:
|
1271 |
with open(p, "r", encoding='utf-8') as f:
|
1272 |
data = json.load(f)
|
1273 |
-
if data.get('version', '1.0')
|
1274 |
try:
|
1275 |
name = self.encryptor.decrypt(data['name'])
|
1276 |
profile_names.append(name)
|
@@ -1339,7 +1459,15 @@ class EnhancedTeachingAssistant:
|
|
1339 |
self._update_context(message, history)
|
1340 |
|
1341 |
student_name = profile.get('name', 'Student')
|
1342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1343 |
learning_style = re.search(r"Your primary learning style is\s*\*\*(.*?)\*\*",
|
1344 |
profile.get('learning_style', ''))
|
1345 |
learning_style = learning_style.group(1) if learning_style else None
|
@@ -1349,6 +1477,8 @@ class EnhancedTeachingAssistant:
|
|
1349 |
context += f"{student_name}'s current weighted GPA is {gpa}. "
|
1350 |
if learning_style:
|
1351 |
context += f"They are a {learning_style.lower()} learner. "
|
|
|
|
|
1352 |
|
1353 |
if self.context_history:
|
1354 |
context += "Recent conversation:\n"
|
@@ -1400,29 +1530,45 @@ class EnhancedTeachingAssistant:
|
|
1400 |
return await self._generate_general_response(message, context)
|
1401 |
|
1402 |
def _generate_gpa_response(self, profile: Dict) -> str:
|
1403 |
-
|
1404 |
-
|
1405 |
-
return "I couldn't find your GPA information. Please upload your transcript first."
|
1406 |
-
|
1407 |
-
analysis = academic_analyzer.analyze_gpa(profile['transcript'])
|
1408 |
response = [
|
1409 |
f"## π GPA Analysis",
|
1410 |
f"**Rating:** {analysis['rating']}",
|
1411 |
f"{analysis['description']}",
|
1412 |
f"{analysis['comparison']}",
|
1413 |
"",
|
1414 |
-
f"## π Graduation Status"
|
1415 |
-
analysis['status'],
|
1416 |
-
f"**Completion:** {analysis['completion_percentage']:.1f}%",
|
1417 |
-
"",
|
1418 |
-
f"## π« College Recommendations"
|
1419 |
]
|
1420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1421 |
if analysis.get('improvement_tips'):
|
1422 |
response.append("\n**Improvement Tips:**")
|
1423 |
response.extend([f"- {tip}" for tip in analysis['improvement_tips']])
|
1424 |
|
1425 |
-
return "\n
|
1426 |
|
1427 |
def _generate_study_response(self, profile: Dict) -> str:
|
1428 |
learning_style_match = re.search(r"Your primary learning style is\s*\*\*(.*?)\*\*",
|
@@ -1461,7 +1607,7 @@ class EnhancedTeachingAssistant:
|
|
1461 |
elif learning_style.lower() == 'kinesthetic':
|
1462 |
response.extend([
|
1463 |
"- Use hands-on activities when possible",
|
1464 |
-
"- Study while
|
1465 |
"- Create physical models to represent concepts"
|
1466 |
])
|
1467 |
|
@@ -1469,7 +1615,7 @@ class EnhancedTeachingAssistant:
|
|
1469 |
response.append("\n**Time Management Tips:**")
|
1470 |
response.extend([f"- {tip}" for tip in study_plan['time_management_tips']])
|
1471 |
|
1472 |
-
return "\n
|
1473 |
|
1474 |
def _generate_courses_response(self, profile: Dict) -> str:
|
1475 |
transcript = profile.get('transcript', {})
|
@@ -1478,12 +1624,14 @@ class EnhancedTeachingAssistant:
|
|
1478 |
|
1479 |
current_courses = [
|
1480 |
course for course in transcript['course_history']
|
1481 |
-
if course.get('status', '').lower() == 'in progress'
|
|
|
1482 |
]
|
1483 |
|
1484 |
completed_courses = [
|
1485 |
course for course in transcript['course_history']
|
1486 |
-
if course.get('status', '').lower() == 'completed'
|
|
|
1487 |
]
|
1488 |
|
1489 |
response = []
|
@@ -1491,8 +1639,9 @@ class EnhancedTeachingAssistant:
|
|
1491 |
if current_courses:
|
1492 |
response.append("**Your Current Courses:**")
|
1493 |
for course in current_courses[:5]:
|
|
|
1494 |
response.append(
|
1495 |
-
f"- {
|
1496 |
f"({course.get('course_code', '')})"
|
1497 |
)
|
1498 |
else:
|
@@ -1501,14 +1650,15 @@ class EnhancedTeachingAssistant:
|
|
1501 |
if completed_courses:
|
1502 |
response.append("\n**Recently Completed Courses:**")
|
1503 |
for course in completed_courses[:5]:
|
1504 |
-
|
|
|
1505 |
if grade:
|
1506 |
response.append(
|
1507 |
-
f"- {
|
1508 |
f"(Grade: {grade})"
|
1509 |
)
|
1510 |
else:
|
1511 |
-
response.append(f"- {
|
1512 |
|
1513 |
rigor = academic_analyzer.analyze_course_rigor(transcript)
|
1514 |
if rigor['rating']:
|
@@ -1590,13 +1740,14 @@ class EnhancedTeachingAssistant:
|
|
1590 |
|
1591 |
current_courses = [
|
1592 |
course for course in transcript.get('course_history', [])
|
1593 |
-
if course.get('status', '').lower() == 'in progress'
|
|
|
1594 |
]
|
1595 |
|
1596 |
if current_courses:
|
1597 |
response.append("\n**Course-Specific Resources:**")
|
1598 |
for course in current_courses[:2]:
|
1599 |
-
course_name = course.get('description', 'your course')
|
1600 |
if 'MATH' in course_name.upper():
|
1601 |
response.append(f"- For {course_name}: Desmos Graphing Calculator, Art of Problem Solving")
|
1602 |
elif 'SCIENCE' in course_name.upper():
|
@@ -1915,10 +2066,6 @@ def create_enhanced_interface():
|
|
1915 |
results.append("\n**Target Schools:**")
|
1916 |
results.extend([f"- {school}" for school in college_recs['target'][:3]])
|
1917 |
|
1918 |
-
if college_recs['safety']:
|
1919 |
-
results.append("\n**Safety Schools:**")
|
1920 |
-
results.extend([f"- {school}" for school in college_recs['safety'][:3]])
|
1921 |
-
|
1922 |
if gpa_analysis.get('improvement_tips'):
|
1923 |
results.append("\n**Improvement Tips:**")
|
1924 |
results.extend([f"- {tip}" for tip in gpa_analysis['improvement_tips']])
|
|
|
252 |
# Initialize learning style quiz
|
253 |
learning_style_quiz = LearningStyleQuiz()
|
254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
# ========== TRANSCRIPT PARSER ==========
|
256 |
class MiamiDadeTranscriptParser:
|
257 |
def __init__(self):
|
258 |
+
# Patterns for both transcript formats
|
259 |
+
self.format1_patterns = {
|
260 |
+
'student_info': re.compile(
|
261 |
+
r"(\d{7}) - (.*?)\s*\|\s*Current Grade:\s*(\d+)\s*\|\s*YOG\s*(\d{4})"
|
262 |
+
r"\s*\|\s*Weighted GPA\s*([\d.]+)\s*\|\s*Comm Serv Date\s*(\d{2}/\d{2}/\d{4})"
|
263 |
+
r"\s*\|\s*Total Credits Earned\s*([\d.]+)"
|
264 |
+
),
|
265 |
+
'requirement': re.compile(
|
266 |
+
r"([A-Z]-[A-Za-z ]+)\s*\|\s*([^|]+)\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([^|]+)%"
|
267 |
+
),
|
268 |
+
'course': re.compile(
|
269 |
+
r"([A-Z]-[A-Za-z ]+)\s*\|\s*(\d{4}-\d{4})\s*\|\s*(\d{2})\s*\|\s*([A-Z0-9]+)\s*\|\s*([^|]+)\|"
|
270 |
+
r"\s*([A-Z0-9])\s*\|\s*(\d+)\s*\|\s*([A-Z])\s*\|\s*([A-Z])\s*\|\s*([\d.]+|inProgress)"
|
271 |
+
)
|
272 |
+
}
|
273 |
|
274 |
+
self.format2_patterns = {
|
275 |
+
'student_info': re.compile(
|
276 |
+
r"LEGAL NAME:\s*([A-Z]+,\s*[A-Z]+).*?"
|
277 |
+
r"GRADE LEVEL:\s*(\d+).*?"
|
278 |
+
r"FL STUDENT ID:\s*(\w+).*?"
|
279 |
+
r"CURRENT SCHOOL:\s*(\d+\s+[\w\s]+?)\s*\(",
|
280 |
+
re.DOTALL
|
281 |
+
),
|
282 |
+
'gpa': re.compile(
|
283 |
+
r"DISTRICT:\s*([\d.]+).*?STATE:\s*([\d.]+)",
|
284 |
+
re.DOTALL
|
285 |
+
),
|
286 |
+
'credits': re.compile(
|
287 |
+
r"\*\s+([A-Z\s]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\*",
|
288 |
+
re.DOTALL
|
289 |
+
),
|
290 |
+
'course': re.compile(
|
291 |
+
r"(\d)\s+(\w+)\s+([\w\s]+?)\s+([A-Z]{2})\s+([A-Z])\s+([A-Z])\s+([A-Z])\s+([\d.]+)\s+([\d.]+)",
|
292 |
+
re.DOTALL
|
293 |
+
),
|
294 |
+
'assessment': re.compile(
|
295 |
+
r"ENGLISH/LANGUAGE ARTS:\s*(\d{2}/\d{4})|"
|
296 |
+
r"ALGEBRA I ASSESSMENT REQUIREMENT MET:\s*(YES|NO)|"
|
297 |
+
r"BIOLOGY ASSESSMENT PASSED|"
|
298 |
+
r"DISTRICT COMM/VOL SERVICE RQMT MET:\s*(YES).*?HRS:\s*(\d+)",
|
299 |
+
re.DOTALL
|
300 |
+
)
|
301 |
+
}
|
302 |
|
303 |
def parse_transcript(self, file_path: str) -> Dict:
|
304 |
+
"""Parse Miami-Dade transcript PDF, automatically detecting format"""
|
305 |
with pdfplumber.open(file_path) as pdf:
|
306 |
text = "\n".join(page.extract_text() for page in pdf.pages)
|
307 |
|
308 |
+
# Clean up text
|
309 |
+
text = re.sub(r'\s+', ' ', text)
|
310 |
+
text = re.sub(r'(?<=\d)\s+(?=\d)', '', text)
|
|
|
|
|
311 |
|
312 |
+
# Detect format
|
313 |
+
if "GRADUATION PROGRESS SUMMARY" in text:
|
314 |
+
return self._parse_format1(text)
|
315 |
+
elif "CUMULATIVE SUMMARY" in text:
|
316 |
+
return self._parse_format2(text)
|
317 |
+
else:
|
318 |
+
raise ValueError("Unrecognized transcript format")
|
319 |
+
|
320 |
+
def _parse_format1(self, text: str) -> Dict:
|
321 |
+
"""Parse the first transcript format"""
|
322 |
+
parsed_data = {
|
323 |
+
'student_info': self._parse_format1_student_info(text),
|
324 |
+
'requirements': self._parse_format1_requirements(text),
|
325 |
+
'course_history': self._parse_format1_courses(text),
|
326 |
+
'format': 'progress_summary'
|
327 |
+
}
|
328 |
+
return parsed_data
|
329 |
|
330 |
+
def _parse_format1_student_info(self, text: str) -> Dict:
|
331 |
+
"""Extract student information from format 1"""
|
332 |
+
match = self.format1_patterns['student_info'].search(text)
|
333 |
if not match:
|
334 |
return {}
|
335 |
|
|
|
344 |
'district': 'Miami-Dade'
|
345 |
}
|
346 |
|
347 |
+
def _parse_format1_requirements(self, text: str) -> Dict:
|
348 |
+
"""Parse graduation requirements section from format 1"""
|
349 |
requirements = {}
|
350 |
+
for match in self.format1_patterns['requirement'].finditer(text):
|
351 |
requirements[match.group(1).strip()] = {
|
352 |
'description': match.group(2).strip(),
|
353 |
'required': float(match.group(3)),
|
|
|
357 |
}
|
358 |
return requirements
|
359 |
|
360 |
+
def _parse_format1_courses(self, text: str) -> List[Dict]:
|
361 |
+
"""Parse course history section from format 1"""
|
362 |
courses = []
|
363 |
+
for match in self.format1_patterns['course'].finditer(text):
|
364 |
courses.append({
|
365 |
'requirement': match.group(1).strip(),
|
366 |
'school_year': match.group(2),
|
|
|
375 |
})
|
376 |
return courses
|
377 |
|
378 |
+
def _parse_format2(self, text: str) -> Dict:
|
379 |
+
"""Parse the second transcript format"""
|
380 |
+
parsed_data = {
|
381 |
+
'student_info': self._parse_format2_student_info(text),
|
382 |
+
'academic_summary': self._parse_format2_academic_summary(text),
|
383 |
+
'course_history': self._parse_format2_courses(text),
|
384 |
+
'assessments': self._parse_format2_assessments(text),
|
385 |
+
'format': 'cumulative_summary'
|
386 |
+
}
|
387 |
+
return parsed_data
|
388 |
+
|
389 |
+
def _parse_format2_student_info(self, text: str) -> Dict:
|
390 |
+
"""Extract student information from format 2"""
|
391 |
+
match = self.format2_patterns['student_info'].search(text)
|
392 |
+
if not match:
|
393 |
+
return {}
|
394 |
+
|
395 |
+
return {
|
396 |
+
'name': match.group(1).replace(',', ' ').strip(),
|
397 |
+
'grade': match.group(2),
|
398 |
+
'student_id': match.group(3),
|
399 |
+
'school': match.group(4).strip(),
|
400 |
+
'birth_date': self._extract_birth_date(text),
|
401 |
+
'ethnicity': self._extract_ethnicity(text)
|
402 |
+
}
|
403 |
+
|
404 |
+
def _extract_birth_date(self, text: str) -> Optional[str]:
|
405 |
+
"""Extract birth date from transcript"""
|
406 |
+
birth_match = re.search(r"BIRTH DATE:\s*(\d{2}/\d{2}/\d{4})", text)
|
407 |
+
return birth_match.group(1) if birth_match else None
|
408 |
+
|
409 |
+
def _extract_ethnicity(self, text: str) -> Optional[str]:
|
410 |
+
"""Extract ethnicity information"""
|
411 |
+
eth_match = re.search(r"ETHNICITY:\s*([^\n]+)", text)
|
412 |
+
return eth_match.group(1).strip() if eth_match else None
|
413 |
+
|
414 |
+
def _parse_format2_academic_summary(self, text: str) -> Dict:
|
415 |
+
"""Parse academic summary section from format 2"""
|
416 |
+
gpa_match = self.format2_patterns['gpa'].search(text)
|
417 |
+
credits_matches = self.format2_patterns['credits'].finditer(text)
|
418 |
+
|
419 |
+
summary = {
|
420 |
+
'gpa': {
|
421 |
+
'district': float(gpa_match.group(1)) if gpa_match else None,
|
422 |
+
'state': float(gpa_match.group(2)) if gpa_match else None
|
423 |
+
},
|
424 |
+
'credits': {},
|
425 |
+
'class_rank': self._extract_class_rank(text)
|
426 |
+
}
|
427 |
+
|
428 |
+
for match in credits_matches:
|
429 |
+
subject = match.group(1).strip()
|
430 |
+
summary['credits'][subject] = {
|
431 |
+
'earned': float(match.group(2)),
|
432 |
+
'required': float(match.group(3)) if match.group(3) else None,
|
433 |
+
'remaining': float(match.group(4)) if match.group(4) else None
|
434 |
+
}
|
435 |
+
|
436 |
+
return summary
|
437 |
+
|
438 |
+
def _extract_class_rank(self, text: str) -> Dict:
|
439 |
+
"""Extract class rank information"""
|
440 |
+
rank_match = re.search(
|
441 |
+
r"\*\s+PERCENTILE:\s*(\d+)\s*\*\s*TOTAL NUMBER IN CLASS:\s*(\d+)",
|
442 |
+
text
|
443 |
+
)
|
444 |
+
return {
|
445 |
+
'percentile': int(rank_match.group(1)) if rank_match else None,
|
446 |
+
'class_size': int(rank_match.group(2)) if rank_match else None
|
447 |
+
}
|
448 |
+
|
449 |
+
def _parse_format2_courses(self, text: str) -> List[Dict]:
|
450 |
+
"""Parse course history section from format 2"""
|
451 |
+
courses = []
|
452 |
+
for match in self.format2_patterns['course'].finditer(text):
|
453 |
+
courses.append({
|
454 |
+
'term': match.group(1),
|
455 |
+
'course_code': match.group(2),
|
456 |
+
'course_title': match.group(3).strip(),
|
457 |
+
'subject_area': match.group(4),
|
458 |
+
'grade': match.group(5),
|
459 |
+
'flag': match.group(6),
|
460 |
+
'credit_status': match.group(7),
|
461 |
+
'credit_attempted': float(match.group(8)),
|
462 |
+
'credit_earned': float(match.group(9))
|
463 |
+
})
|
464 |
+
return courses
|
465 |
+
|
466 |
+
def _parse_format2_assessments(self, text: str) -> Dict:
|
467 |
+
"""Parse assessment and requirement information from format 2"""
|
468 |
+
matches = self.format2_patterns['assessment'].finditer(text)
|
469 |
+
assessments = {
|
470 |
+
'ela_passed_date': None,
|
471 |
+
'algebra_passed': False,
|
472 |
+
'biology_passed': False,
|
473 |
+
'community_service': {
|
474 |
+
'met': False,
|
475 |
+
'hours': 0
|
476 |
+
}
|
477 |
+
}
|
478 |
+
|
479 |
+
for match in matches:
|
480 |
+
if match.group(1): # ELA date
|
481 |
+
assessments['ela_passed_date'] = match.group(1)
|
482 |
+
elif match.group(2): # Algebra
|
483 |
+
assessments['algebra_passed'] = match.group(2) == "YES"
|
484 |
+
elif "BIOLOGY ASSESSMENT PASSED" in match.group(0):
|
485 |
+
assessments['biology_passed'] = True
|
486 |
+
elif match.group(3): # Community service
|
487 |
+
assessments['community_service'] = {
|
488 |
+
'met': True,
|
489 |
+
'hours': int(match.group(4))
|
490 |
+
}
|
491 |
+
|
492 |
+
return assessments
|
493 |
+
|
494 |
# Initialize transcript parser
|
495 |
transcript_parser = MiamiDadeTranscriptParser()
|
496 |
|
|
|
518 |
}
|
519 |
|
520 |
try:
|
521 |
+
if parsed_data.get('format') == 'progress_summary':
|
522 |
+
weighted_gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0))
|
523 |
+
unweighted_gpa = float(parsed_data.get('student_info', {}).get('unweighted_gpa', 0))
|
524 |
+
else:
|
525 |
+
weighted_gpa = float(parsed_data.get('academic_summary', {}).get('gpa', {}).get('district', 0))
|
526 |
+
unweighted_gpa = float(parsed_data.get('academic_summary', {}).get('gpa', {}).get('state', 0))
|
527 |
|
528 |
if weighted_gpa >= 4.5:
|
529 |
analysis['rating'] = 'Excellent'
|
|
|
599 |
}
|
600 |
|
601 |
try:
|
602 |
+
if parsed_data.get('format') == 'progress_summary':
|
603 |
+
# Format 1 analysis
|
604 |
+
total_match = re.search(r'Total\s*\|\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)%', text)
|
605 |
+
if total_match:
|
606 |
+
analysis['completion_percentage'] = float(total_match.group(4))
|
607 |
+
else:
|
608 |
+
total_required = sum(
|
609 |
+
float(req.get('required', 0))
|
610 |
+
for req in parsed_data.get('requirements', {}).values()
|
611 |
+
if req and str(req.get('required', '0')).replace('.','').isdigit()
|
612 |
+
)
|
613 |
+
total_completed = sum(
|
614 |
+
float(req.get('completed', 0))
|
615 |
+
for req in parsed_data.get('requirements', {}).values()
|
616 |
+
if req and str(req.get('completed', '0')).replace('.','').isdigit()
|
617 |
+
)
|
618 |
+
analysis['completion_percentage'] = (total_completed / total_required) * 100 if total_required > 0 else 0
|
619 |
+
|
620 |
+
analysis['missing_requirements'] = [
|
621 |
+
{
|
622 |
+
'code': code,
|
623 |
+
'description': req.get('description', ''),
|
624 |
+
'remaining': max(0, float(req.get('required', 0)) - float(req.get('completed', 0))),
|
625 |
+
'status': req.get('status', '')
|
626 |
+
}
|
627 |
+
for code, req in parsed_data.get('requirements', {}).items()
|
628 |
+
if req and float(req.get('completed', 0)) < float(req.get('required', 0))
|
629 |
+
]
|
630 |
+
else:
|
631 |
+
# Format 2 analysis
|
632 |
+
credits = parsed_data.get('academic_summary', {}).get('credits', {})
|
633 |
+
total_required = sum(
|
634 |
+
v.get('required', 0)
|
635 |
+
for v in credits.values()
|
636 |
+
if v and isinstance(v.get('required'), (int, float))
|
637 |
+
)
|
638 |
+
total_earned = sum(
|
639 |
+
v.get('earned', 0)
|
640 |
+
for v in credits.values()
|
641 |
+
if v and isinstance(v.get('earned'), (int, float))
|
642 |
+
)
|
643 |
+
analysis['completion_percentage'] = (total_earned / total_required) * 100 if total_required > 0 else 0
|
644 |
+
|
645 |
+
analysis['missing_requirements'] = [
|
646 |
+
{
|
647 |
+
'subject': subject,
|
648 |
+
'earned': info.get('earned', 0),
|
649 |
+
'required': info.get('required', 0),
|
650 |
+
'remaining': max(0, info.get('required', 0) - info.get('earned', 0))
|
651 |
+
}
|
652 |
+
for subject, info in credits.items()
|
653 |
+
if info and info.get('required', 0) > info.get('earned', 0)
|
654 |
+
]
|
655 |
|
656 |
current_grade = parsed_data.get('student_info', {}).get('grade', '')
|
657 |
grad_year = parsed_data.get('student_info', {}).get('year_of_graduation', '')
|
|
|
673 |
analysis['on_track'] = False
|
674 |
|
675 |
if current_grade and grad_year:
|
676 |
+
remaining_credits = total_required - total_earned
|
677 |
years_remaining = int(grad_year) - datetime.datetime.now().year - int(current_grade)
|
678 |
|
679 |
if years_remaining > 0:
|
|
|
705 |
}
|
706 |
|
707 |
try:
|
708 |
+
courses = parsed_data.get('course_history', [])
|
709 |
+
|
710 |
+
for course in courses:
|
711 |
+
course_title = course.get('description', '') or course.get('course_title', '')
|
712 |
+
course_title = course_title.upper()
|
713 |
+
|
714 |
+
if 'AP' in course_title or 'ADVANCED PLACEMENT' in course_title:
|
715 |
analysis['ap_courses'] += 1
|
716 |
analysis['advanced_courses'] += 1
|
717 |
+
elif 'IB' in course_title or 'INTERNATIONAL BACCALAUREATE' in course_title:
|
718 |
analysis['ib_courses'] += 1
|
719 |
analysis['advanced_courses'] += 1
|
720 |
+
elif 'DE' in course_title or 'DUAL ENROLLMENT' in course_title or 'COLLEGE' in course_title:
|
721 |
analysis['de_courses'] += 1
|
722 |
analysis['advanced_courses'] += 1
|
723 |
+
elif 'HONORS' in course_title or course.get('flag', '') == 'H':
|
724 |
analysis['honors_courses'] += 1
|
725 |
analysis['advanced_courses'] += 1
|
726 |
|
727 |
total_advanced = analysis['advanced_courses']
|
728 |
+
total_courses = len(courses)
|
729 |
|
730 |
if total_courses == 0:
|
731 |
return analysis
|
|
|
780 |
}
|
781 |
|
782 |
try:
|
783 |
+
if parsed_data.get('format') == 'progress_summary':
|
784 |
+
weighted_gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0))
|
785 |
+
service_hours = int(parsed_data.get('student_info', {}).get('community_service_hours', 0))
|
786 |
+
else:
|
787 |
+
weighted_gpa = float(parsed_data.get('academic_summary', {}).get('gpa', {}).get('district', 0))
|
788 |
+
service_hours = int(parsed_data.get('assessments', {}).get('community_service', {}).get('hours', 0))
|
789 |
+
|
790 |
rigor_analysis = self.analyze_course_rigor(parsed_data)
|
|
|
791 |
|
792 |
if weighted_gpa >= 4.3 and rigor_analysis['advanced_courses'] >= 8 and service_hours >= 100:
|
793 |
recommendations['reach'].extend([
|
|
|
883 |
try:
|
884 |
current_courses = [
|
885 |
course for course in parsed_data.get('course_history', [])
|
886 |
+
if course.get('status', '').lower() == 'in progress' or
|
887 |
+
(isinstance(course.get('credit_earned'), float) and course['credit_earned'] == 0)
|
888 |
]
|
889 |
|
890 |
days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
|
|
924 |
for i, course in enumerate(current_courses):
|
925 |
day_index = i % 5
|
926 |
day = days[day_index]
|
927 |
+
course_name = course.get('description') or course.get('course_title', 'Course')
|
928 |
plan['weekly_schedule'][day].append({
|
929 |
+
'course': course_name,
|
930 |
'duration': '45-60 minutes',
|
931 |
'activities': [
|
932 |
"Review notes",
|
|
|
975 |
|
976 |
def create_gpa_visualization(self, parsed_data: Dict):
|
977 |
try:
|
978 |
+
if parsed_data.get('format') == 'progress_summary':
|
979 |
+
weighted_gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0))
|
980 |
+
unweighted_gpa = float(parsed_data.get('student_info', {}).get('unweighted_gpa', 0))
|
981 |
+
else:
|
982 |
+
weighted_gpa = float(parsed_data.get('academic_summary', {}).get('gpa', {}).get('district', 0))
|
983 |
+
unweighted_gpa = float(parsed_data.get('academic_summary', {}).get('gpa', {}).get('state', 0))
|
984 |
+
|
985 |
gpa_data = {
|
986 |
"Type": ["Weighted GPA", "Unweighted GPA"],
|
987 |
+
"Value": [weighted_gpa, unweighted_gpa],
|
|
|
|
|
|
|
988 |
"Color": [self.color_palette['gpa_weighted'], self.color_palette['gpa_unweighted']]
|
989 |
}
|
990 |
|
|
|
1031 |
|
1032 |
def create_requirements_visualization(self, parsed_data: Dict):
|
1033 |
try:
|
1034 |
+
if parsed_data.get('format') == 'progress_summary':
|
1035 |
+
req_data = []
|
1036 |
+
for code, req in parsed_data.get('requirements', {}).items():
|
1037 |
+
if req and req.get('percent_complete'):
|
1038 |
+
completion = float(req['percent_complete'])
|
1039 |
+
req_data.append({
|
1040 |
+
"Requirement": f"{code}: {req.get('description', '')[:30]}...",
|
1041 |
+
"Completion (%)": completion,
|
1042 |
+
"Status": "Complete" if completion >= 100 else "In Progress" if completion > 0 else "Not Started",
|
1043 |
+
"Required": req.get('required', 0),
|
1044 |
+
"Completed": req.get('completed', 0),
|
1045 |
+
"Remaining": max(0, float(req.get('required', 0)) - float(req.get('completed', 0)))
|
1046 |
+
})
|
1047 |
+
else:
|
1048 |
+
req_data = []
|
1049 |
+
credits = parsed_data.get('academic_summary', {}).get('credits', {})
|
1050 |
+
for subject, info in credits.items():
|
1051 |
+
if info.get('required') and info.get('earned'):
|
1052 |
+
completion = (info['earned'] / info['required']) * 100 if info['required'] > 0 else 0
|
1053 |
+
req_data.append({
|
1054 |
+
"Requirement": subject,
|
1055 |
+
"Completion (%)": completion,
|
1056 |
+
"Status": "Complete" if completion >= 100 else "In Progress" if completion > 0 else "Not Started",
|
1057 |
+
"Required": info.get('required', 0),
|
1058 |
+
"Completed": info.get('earned', 0),
|
1059 |
+
"Remaining": max(0, info.get('required', 0) - info.get('earned', 0))
|
1060 |
+
})
|
1061 |
|
1062 |
if not req_data:
|
1063 |
return None
|
|
|
1104 |
|
1105 |
def create_credits_distribution_visualization(self, parsed_data: Dict):
|
1106 |
try:
|
1107 |
+
if parsed_data.get('format') == 'progress_summary':
|
1108 |
+
core_credits = sum(
|
1109 |
+
req['completed'] for req in parsed_data.get('requirements', {}).values()
|
1110 |
+
if req and req.get('code', '').split('-')[0] in ['A', 'B', 'C', 'D']
|
1111 |
+
)
|
1112 |
+
|
1113 |
+
elective_credits = sum(
|
1114 |
+
req['completed'] for req in parsed_data.get('requirements', {}).values()
|
1115 |
+
if req and req.get('code', '').split('-')[0] in ['G', 'H']
|
1116 |
+
)
|
1117 |
+
|
1118 |
+
other_credits = sum(
|
1119 |
+
req['completed'] for req in parsed_data.get('requirements', {}).values()
|
1120 |
+
if req and req.get('code', '').split('-')[0] in ['E', 'F']
|
1121 |
+
)
|
1122 |
+
else:
|
1123 |
+
credits = parsed_data.get('academic_summary', {}).get('credits', {})
|
1124 |
+
core_credits = sum(
|
1125 |
+
info['earned'] for subject, info in credits.items()
|
1126 |
+
if subject.split()[0] in ['ENGLISH', 'ALGEBRA1', 'GEOMETRY', 'MATHEMATICS', 'BIOLOGY', 'SCIENCE']
|
1127 |
+
)
|
1128 |
+
elective_credits = sum(
|
1129 |
+
info['earned'] for subject, info in credits.items()
|
1130 |
+
if subject.split()[0] in ['ELECTIVE', 'WORLD']
|
1131 |
+
)
|
1132 |
+
other_credits = sum(
|
1133 |
+
info['earned'] for subject, info in credits.items()
|
1134 |
+
if subject.split()[0] in ['ARTS', 'PHYSICAL', 'PERFORMING']
|
1135 |
+
)
|
1136 |
|
1137 |
credit_values = [core_credits, elective_credits, other_credits]
|
1138 |
credit_labels = ['Core Subjects', 'Electives', 'Arts/PE']
|
|
|
1291 |
"study_plan": study_plan if study_plan else {},
|
1292 |
"session_token": self.current_session,
|
1293 |
"last_updated": time.time(),
|
1294 |
+
"version": "2.1"
|
1295 |
}
|
1296 |
|
1297 |
filepath = self.get_profile_path(name)
|
|
|
1357 |
if time.time() - profile_data.get('last_updated', 0) > SESSION_TIMEOUT:
|
1358 |
raise gr.Error("Session expired. Please start a new session.")
|
1359 |
|
1360 |
+
if profile_data.get('version', '1.0') in ['2.0', '2.1']:
|
1361 |
try:
|
1362 |
profile_data['name'] = self.encryptor.decrypt(profile_data['name'])
|
1363 |
profile_data['interests'] = self.encryptor.decrypt(profile_data.get('interests', ''))
|
|
|
1390 |
try:
|
1391 |
with open(p, "r", encoding='utf-8') as f:
|
1392 |
data = json.load(f)
|
1393 |
+
if data.get('version', '1.0') in ['2.0', '2.1']:
|
1394 |
try:
|
1395 |
name = self.encryptor.decrypt(data['name'])
|
1396 |
profile_names.append(name)
|
|
|
1459 |
self._update_context(message, history)
|
1460 |
|
1461 |
student_name = profile.get('name', 'Student')
|
1462 |
+
transcript = profile.get('transcript', {})
|
1463 |
+
|
1464 |
+
if transcript.get('format') == 'progress_summary':
|
1465 |
+
gpa = transcript.get('student_info', {}).get('weighted_gpa', None)
|
1466 |
+
service_hours = transcript.get('student_info', {}).get('community_service_hours', 0)
|
1467 |
+
else:
|
1468 |
+
gpa = transcript.get('academic_summary', {}).get('gpa', {}).get('district', None)
|
1469 |
+
service_hours = transcript.get('assessments', {}).get('community_service', {}).get('hours', 0)
|
1470 |
+
|
1471 |
learning_style = re.search(r"Your primary learning style is\s*\*\*(.*?)\*\*",
|
1472 |
profile.get('learning_style', ''))
|
1473 |
learning_style = learning_style.group(1) if learning_style else None
|
|
|
1477 |
context += f"{student_name}'s current weighted GPA is {gpa}. "
|
1478 |
if learning_style:
|
1479 |
context += f"They are a {learning_style.lower()} learner. "
|
1480 |
+
if service_hours:
|
1481 |
+
context += f"They have completed {service_hours} community service hours. "
|
1482 |
|
1483 |
if self.context_history:
|
1484 |
context += "Recent conversation:\n"
|
|
|
1530 |
return await self._generate_general_response(message, context)
|
1531 |
|
1532 |
def _generate_gpa_response(self, profile: Dict) -> str:
|
1533 |
+
transcript = profile.get('transcript', {})
|
1534 |
+
analysis = academic_analyzer.analyze_gpa(transcript)
|
|
|
|
|
|
|
1535 |
response = [
|
1536 |
f"## π GPA Analysis",
|
1537 |
f"**Rating:** {analysis['rating']}",
|
1538 |
f"{analysis['description']}",
|
1539 |
f"{analysis['comparison']}",
|
1540 |
"",
|
1541 |
+
f"## π Graduation Status"
|
|
|
|
|
|
|
|
|
1542 |
]
|
1543 |
|
1544 |
+
grad_status = academic_analyzer.analyze_graduation_status(transcript)
|
1545 |
+
response.append(grad_status['status'])
|
1546 |
+
response.append(f"**Completion:** {grad_status['completion_percentage']:.1f}%")
|
1547 |
+
|
1548 |
+
if grad_status.get('missing_requirements'):
|
1549 |
+
response.append("\n**Missing Requirements:**")
|
1550 |
+
for req in grad_status['missing_requirements'][:3]: # Show top 3 missing
|
1551 |
+
if transcript.get('format') == 'progress_summary':
|
1552 |
+
response.append(f"- {req['code']}: {req['description']} ({req['remaining']} credits remaining)")
|
1553 |
+
else:
|
1554 |
+
response.append(f"- {req['subject']}: {req['remaining']} credits remaining")
|
1555 |
+
|
1556 |
+
response.append("\n## π« College Recommendations")
|
1557 |
+
college_recs = academic_analyzer.generate_college_recommendations(transcript)
|
1558 |
+
|
1559 |
+
if college_recs['reach']:
|
1560 |
+
response.append("\n**Reach Schools:**")
|
1561 |
+
response.extend([f"- {school}" for school in college_recs['reach'][:3]])
|
1562 |
+
|
1563 |
+
if college_recs['target']:
|
1564 |
+
response.append("\n**Target Schools:**")
|
1565 |
+
response.extend([f"- {school}" for school in college_recs['target'][:3]])
|
1566 |
+
|
1567 |
if analysis.get('improvement_tips'):
|
1568 |
response.append("\n**Improvement Tips:**")
|
1569 |
response.extend([f"- {tip}" for tip in analysis['improvement_tips']])
|
1570 |
|
1571 |
+
return "\n".join(response)
|
1572 |
|
1573 |
def _generate_study_response(self, profile: Dict) -> str:
|
1574 |
learning_style_match = re.search(r"Your primary learning style is\s*\*\*(.*?)\*\*",
|
|
|
1607 |
elif learning_style.lower() == 'kinesthetic':
|
1608 |
response.extend([
|
1609 |
"- Use hands-on activities when possible",
|
1610 |
+
"- Study while walking or pacing",
|
1611 |
"- Create physical models to represent concepts"
|
1612 |
])
|
1613 |
|
|
|
1615 |
response.append("\n**Time Management Tips:**")
|
1616 |
response.extend([f"- {tip}" for tip in study_plan['time_management_tips']])
|
1617 |
|
1618 |
+
return "\n".join(response)
|
1619 |
|
1620 |
def _generate_courses_response(self, profile: Dict) -> str:
|
1621 |
transcript = profile.get('transcript', {})
|
|
|
1624 |
|
1625 |
current_courses = [
|
1626 |
course for course in transcript['course_history']
|
1627 |
+
if (course.get('status', '').lower() == 'in progress' or
|
1628 |
+
(isinstance(course.get('credit_earned'), float) and course['credit_earned'] == 0))
|
1629 |
]
|
1630 |
|
1631 |
completed_courses = [
|
1632 |
course for course in transcript['course_history']
|
1633 |
+
if (course.get('status', '').lower() == 'completed' or
|
1634 |
+
(isinstance(course.get('credit_earned'), float) and course['credit_earned'] > 0))
|
1635 |
]
|
1636 |
|
1637 |
response = []
|
|
|
1639 |
if current_courses:
|
1640 |
response.append("**Your Current Courses:**")
|
1641 |
for course in current_courses[:5]:
|
1642 |
+
course_name = course.get('description') or course.get('course_title', 'Unknown')
|
1643 |
response.append(
|
1644 |
+
f"- {course_name} "
|
1645 |
f"({course.get('course_code', '')})"
|
1646 |
)
|
1647 |
else:
|
|
|
1650 |
if completed_courses:
|
1651 |
response.append("\n**Recently Completed Courses:**")
|
1652 |
for course in completed_courses[:5]:
|
1653 |
+
course_name = course.get('description') or course.get('course_title', 'Unknown')
|
1654 |
+
grade = course.get('grade_earned', '') or course.get('grade', '')
|
1655 |
if grade:
|
1656 |
response.append(
|
1657 |
+
f"- {course_name} "
|
1658 |
f"(Grade: {grade})"
|
1659 |
)
|
1660 |
else:
|
1661 |
+
response.append(f"- {course_name}")
|
1662 |
|
1663 |
rigor = academic_analyzer.analyze_course_rigor(transcript)
|
1664 |
if rigor['rating']:
|
|
|
1740 |
|
1741 |
current_courses = [
|
1742 |
course for course in transcript.get('course_history', [])
|
1743 |
+
if (course.get('status', '').lower() == 'in progress' or
|
1744 |
+
(isinstance(course.get('credit_earned'), float) and course['credit_earned'] == 0))
|
1745 |
]
|
1746 |
|
1747 |
if current_courses:
|
1748 |
response.append("\n**Course-Specific Resources:**")
|
1749 |
for course in current_courses[:2]:
|
1750 |
+
course_name = course.get('description') or course.get('course_title', 'your course')
|
1751 |
if 'MATH' in course_name.upper():
|
1752 |
response.append(f"- For {course_name}: Desmos Graphing Calculator, Art of Problem Solving")
|
1753 |
elif 'SCIENCE' in course_name.upper():
|
|
|
2066 |
results.append("\n**Target Schools:**")
|
2067 |
results.extend([f"- {school}" for school in college_recs['target'][:3]])
|
2068 |
|
|
|
|
|
|
|
|
|
2069 |
if gpa_analysis.get('improvement_tips'):
|
2070 |
results.append("\n**Improvement Tips:**")
|
2071 |
results.extend([f"- {tip}" for tip in gpa_analysis['improvement_tips']])
|