Dannyar608 commited on
Commit
17a6b1d
Β·
verified Β·
1 Parent(s): 88ad853

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +391 -244
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
- self.student_info_pattern = re.compile(
377
- r"(\d{7}) - (.*?)\s*\|\s*Current Grade:\s*(\d+)\s*\|\s*YOG\s*(\d{4})"
378
- r"\s*\|\s*Weighted GPA\s*([\d.]+)\s*\|\s*Comm Serv Date\s*(\d{2}/\d{2}/\d{4})"
379
- r"\s*\|\s*Total Credits Earned\s*([\d.]+)"
380
- )
381
-
382
- self.requirement_pattern = re.compile(
383
- r"([A-Z]-[A-Za-z ]+)\s*\|\s*([^|]+)\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([^|]+)%"
384
- )
 
 
 
 
 
 
385
 
386
- self.course_pattern = re.compile(
387
- r"([A-Z]-[A-Za-z ]+)\s*\|\s*(\d{4}-\d{4})\s*\|\s*(\d{2})\s*\|\s*([A-Z0-9]+)\s*\|\s*([^|]+)\|"
388
- r"\s*([A-Z0-9])\s*\|\s*(\d+)\s*\|\s*([A-Z])\s*\|\s*([A-Z])\s*\|\s*([\d.]+|inProgress)"
389
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
  def parse_transcript(self, file_path: str) -> Dict:
392
- """Parse Miami-Dade County transcript PDF"""
393
  with pdfplumber.open(file_path) as pdf:
394
  text = "\n".join(page.extract_text() for page in pdf.pages)
395
 
396
- parsed_data = {
397
- 'student_info': self._parse_student_info(text),
398
- 'requirements': self._parse_requirements(text),
399
- 'course_history': self._parse_courses(text)
400
- }
401
 
402
- return parsed_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
 
404
- def _parse_student_info(self, text: str) -> Dict:
405
- """Extract student information"""
406
- match = self.student_info_pattern.search(text)
407
  if not match:
408
  return {}
409
 
@@ -418,10 +344,10 @@ class MiamiDadeTranscriptParser:
418
  'district': 'Miami-Dade'
419
  }
420
 
421
- def _parse_requirements(self, text: str) -> Dict:
422
- """Parse graduation requirements section"""
423
  requirements = {}
424
- for match in self.requirement_pattern.finditer(text):
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 _parse_courses(self, text: str) -> List[Dict]:
435
- """Parse course history section"""
436
  courses = []
437
- for match in self.course_pattern.finditer(text):
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
- weighted_gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0))
480
- unweighted_gpa = float(parsed_data.get('student_info', {}).get('unweighted_gpa', 0))
 
 
 
 
481
 
482
  if weighted_gpa >= 4.5:
483
  analysis['rating'] = 'Excellent'
@@ -553,30 +599,59 @@ class AcademicAnalyzer:
553
  }
554
 
555
  try:
556
- total_required = sum(
557
- float(req.get('required', 0))
558
- for req in parsed_data.get('requirements', {}).values()
559
- if req and str(req.get('required', '0')).replace('.','').isdigit()
560
- )
561
-
562
- total_completed = sum(
563
- float(req.get('completed', 0))
564
- for req in parsed_data.get('requirements', {}).values()
565
- if req and str(req.get('completed', '0')).replace('.','').isdigit()
566
- )
567
-
568
- analysis['completion_percentage'] = (total_completed / total_required) * 100 if total_required > 0 else 0
569
-
570
- analysis['missing_requirements'] = [
571
- {
572
- 'code': code,
573
- 'description': req.get('description', ''),
574
- 'remaining': max(0, float(req.get('required', 0)) - float(req.get('completed', 0))),
575
- 'status': req.get('status', '')
576
- }
577
- for code, req in parsed_data.get('requirements', {}).items()
578
- if req and float(req.get('completed', 0)) < float(req.get('required', 0))
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 - total_completed
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
- for course in parsed_data.get('course_history', []):
634
- course_name = course.get('description', '').upper()
635
- if 'AP' in course_name:
 
 
 
 
636
  analysis['ap_courses'] += 1
637
  analysis['advanced_courses'] += 1
638
- elif 'IB' in course_name:
639
  analysis['ib_courses'] += 1
640
  analysis['advanced_courses'] += 1
641
- elif 'DE' in course_name or 'DUAL ENROLLMENT' in course_name:
642
  analysis['de_courses'] += 1
643
  analysis['advanced_courses'] += 1
644
- elif 'HONORS' in course_name:
645
  analysis['honors_courses'] += 1
646
  analysis['advanced_courses'] += 1
647
 
648
  total_advanced = analysis['advanced_courses']
649
- total_courses = len(parsed_data.get('course_history', []))
650
 
651
  if total_courses == 0:
652
  return analysis
@@ -701,9 +780,14 @@ class AcademicAnalyzer:
701
  }
702
 
703
  try:
704
- weighted_gpa = float(parsed_data.get('student_info', {}).get('weighted_gpa', 0))
 
 
 
 
 
 
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': course.get('description', '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
- req_data = []
945
- for code, req in parsed_data.get('requirements', {}).items():
946
- if req and req.get('percent_complete'):
947
- completion = float(req['percent_complete'])
948
- req_data.append({
949
- "Requirement": f"{code}: {req.get('description', '')[:30]}...",
950
- "Completion (%)": completion,
951
- "Status": "Complete" if completion >= 100 else "In Progress" if completion > 0 else "Not Started",
952
- "Required": req.get('required', 0),
953
- "Completed": req.get('completed', 0),
954
- "Remaining": max(0, float(req.get('required', 0)) - float(req.get('completed', 0)))
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
- core_credits = sum(
1003
- req['completed'] for req in parsed_data.get('requirements', {}).values()
1004
- if req and req.get('code', '').split('-')[0] in ['A', 'B', 'C', 'D']
1005
- )
1006
-
1007
- elective_credits = sum(
1008
- req['completed'] for req in parsed_data.get('requirements', {}).values()
1009
- if req and req.get('code', '').split('-')[0] in ['G', 'H']
1010
- )
1011
-
1012
- other_credits = sum(
1013
- req['completed'] for req in parsed_data.get('requirements', {}).values()
1014
- if req and req.get('code', '').split('-')[0] in ['E', 'F']
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.0"
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') == '2.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') == '2.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
- gpa = profile.get('transcript', {}).get('student_info', {}).get('weighted_gpa', None)
 
 
 
 
 
 
 
 
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
- gpa = profile.get('transcript', {}).get('student_info', {}).get('weighted_gpa', None)
1404
- if not gpa:
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\n".join(response)
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 moving or pacing",
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\n".join(response)
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"- {course.get('description', 'Unknown')} "
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
- grade = course.get('grade_earned', '')
 
1505
  if grade:
1506
  response.append(
1507
- f"- {course.get('description', 'Unknown')} "
1508
  f"(Grade: {grade})"
1509
  )
1510
  else:
1511
- response.append(f"- {course.get('description', 'Unknown')}")
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']])