michsethowusu commited on
Commit
a12ac8a
·
verified ·
1 Parent(s): 3e1e888

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +76 -417
app.py CHANGED
@@ -22,9 +22,6 @@ MODEL = "meta/llama-4-maverick-17b-128e-instruct"
22
  AFSCTP_FOLDER = "./frameworks/standards"
23
  ATQF_FOLDER = "./frameworks/qualifications"
24
 
25
- # Knowledge base storage
26
- KNOWLEDGE_DB_FILE = "./frameworks/knowledge_base.json"
27
-
28
  # ---------- AFSCTP SCORECARD (Standards and Competencies) ----------
29
  AFSCTP_CRITERIA = {
30
  "S1": {
@@ -177,248 +174,9 @@ ATQF_CRITERIA = {
177
  }
178
  }
179
 
180
- # ---------- KNOWLEDGE BASE MANAGEMENT ----------
181
-
182
- def load_knowledge_base():
183
- """Load existing knowledge base or return empty structure"""
184
- if os.path.exists(KNOWLEDGE_DB_FILE):
185
- try:
186
- with open(KNOWLEDGE_DB_FILE, 'r', encoding='utf-8') as f:
187
- return json.load(f)
188
- except:
189
- return {"countries": {}, "last_updated": None}
190
- return {"countries": {}, "last_updated": None}
191
-
192
- def save_knowledge_base(kb):
193
- """Save knowledge base to file"""
194
- os.makedirs(os.path.dirname(KNOWLEDGE_DB_FILE), exist_ok=True)
195
- kb["last_updated"] = datetime.now().isoformat()
196
- with open(KNOWLEDGE_DB_FILE, 'w', encoding='utf-8') as f:
197
- json.dump(kb, f, indent=2)
198
-
199
- def scan_folders_for_documents():
200
- """Scan both folders and return list of all documents with their framework types"""
201
- documents = []
202
-
203
- # Scan AFSCTP folder
204
- if os.path.exists(AFSCTP_FOLDER):
205
- for f in os.listdir(AFSCTP_FOLDER):
206
- if f.lower().endswith('.pdf'):
207
- documents.append({
208
- "filename": f,
209
- "framework_type": "Standards and Competencies (AFSCTP)",
210
- "framework_code": "AFSCTP",
211
- "path": os.path.join(AFSCTP_FOLDER, f)
212
- })
213
-
214
- # Scan ATQF folder
215
- if os.path.exists(ATQF_FOLDER):
216
- for f in os.listdir(ATQF_FOLDER):
217
- if f.lower().endswith('.pdf'):
218
- documents.append({
219
- "filename": f,
220
- "framework_type": "Qualifications Framework (ATQF/CTQF)",
221
- "framework_code": "ATQF",
222
- "path": os.path.join(ATQF_FOLDER, f)
223
- })
224
-
225
- return documents
226
-
227
- def build_knowledge_base_from_documents():
228
- """Scan all documents and build/update knowledge base"""
229
- kb = load_knowledge_base()
230
- documents = scan_folders_for_documents()
231
-
232
- if not NVIDIA_API_KEY:
233
- print("Warning: NVIDIA_API_KEY not set. Cannot build knowledge base.")
234
- return kb
235
-
236
- processed_count = 0
237
- for doc in documents:
238
- # Check if already processed and up to date
239
- doc_id = f"{doc['framework_code']}_{doc['filename']}"
240
- if doc_id in kb.get("processed_docs", {}):
241
- # Check if file has been modified
242
- stored_mtime = kb["processed_docs"][doc_id].get("mtime", 0)
243
- current_mtime = os.path.getmtime(doc["path"])
244
- if stored_mtime == current_mtime:
245
- continue # Skip already processed files
246
-
247
- try:
248
- print(f"Processing: {doc['filename']}...")
249
- text = extract_text_from_file(doc["path"])
250
- country = detect_country(text)
251
-
252
- # Determine criteria dict
253
- if doc["framework_code"] == "AFSCTP":
254
- criteria_dict = AFSCTP_CRITERIA
255
- else:
256
- criteria_dict = ATQF_CRITERIA
257
-
258
- # Evaluate
259
- prompt = build_evaluation_prompt(text, criteria_dict, doc["framework_code"])
260
- results = call_nvidia_llm(prompt)
261
-
262
- # Calculate overall score
263
- scores = []
264
- for cid in criteria_dict.keys():
265
- score = results.get(cid, 0)
266
- try:
267
- scores.append(int(score))
268
- except:
269
- scores.append(0)
270
-
271
- total_score = sum(scores)
272
- max_score = len(criteria_dict) * 5
273
- percent = (total_score / max_score) * 100 if max_score > 0 else 0
274
-
275
- # Store in knowledge base
276
- if country not in kb["countries"]:
277
- kb["countries"][country] = {}
278
-
279
- kb["countries"][country][doc["framework_code"]] = {
280
- "filename": doc["filename"],
281
- "overall_score": round(percent, 1),
282
- "criterion_scores": results,
283
- "individual_scores": scores,
284
- "processed_date": datetime.now().isoformat(),
285
- "document_path": doc["path"]
286
- }
287
-
288
- # Track processed document
289
- if "processed_docs" not in kb:
290
- kb["processed_docs"] = {}
291
- kb["processed_docs"][doc_id] = {
292
- "mtime": os.path.getmtime(doc["path"]),
293
- "processed_date": datetime.now().isoformat()
294
- }
295
-
296
- processed_count += 1
297
-
298
- except Exception as e:
299
- print(f"Error processing {doc['filename']}: {e}")
300
-
301
- save_knowledge_base(kb)
302
- print(f"Knowledge base updated. Processed {processed_count} new/updated documents.")
303
- return kb
304
-
305
- def get_knowledge_glossary():
306
- """Generate glossary display from knowledge base"""
307
- kb = load_knowledge_base()
308
-
309
- if not kb["countries"]:
310
- return "### 📚 Knowledge Base\\n\\nNo documents have been processed yet. Upload documents and click 'Refresh Knowledge Base' to build the glossary."
311
-
312
- md = "### 📚 African Union Teacher Frameworks Knowledge Base\\n\\n"
313
- md += f"*Last Updated: {kb.get('last_updated', 'Unknown')[:10] if kb.get('last_updated') else 'Unknown'}*\\n\\n"
314
-
315
- # Summary statistics
316
- total_countries = len(kb["countries"])
317
- afsctp_count = sum(1 for c in kb["countries"].values() if "AFSCTP" in c)
318
- atqf_count = sum(1 for c in kb["countries"].values() if "ATQF" in c)
319
-
320
- md += f"**📊 Overview:** {total_countries} countries | {afsctp_count} AFSCTP evaluations | {atqf_count} ATQF/CTQF evaluations\\n\\n"
321
- md += "---\\n\\n"
322
-
323
- # Sort countries alphabetically
324
- for country in sorted(kb["countries"].keys()):
325
- data = kb["countries"][country]
326
- md += f"#### 🌍 {country}\\n\\n"
327
-
328
- for framework_code, eval_data in data.items():
329
- framework_name = "AFSCTP (Standards)" if framework_code == "AFSCTP" else "ATQF/CTQF (Qualifications)"
330
- score = eval_data["overall_score"]
331
-
332
- # Color code the score
333
- if score >= 75:
334
- emoji = "🟢"
335
- color = "green"
336
- elif score >= 50:
337
- emoji = "🟡"
338
- color = "orange"
339
- else:
340
- emoji = "🔴"
341
- color = "red"
342
-
343
- md += f"**{framework_name}:** {emoji} **{score:.1f}%** alignment\\n"
344
-
345
- # Show individual criterion scores
346
- scores = eval_data.get("individual_scores", [])
347
- criteria_dict = AFSCTP_CRITERIA if framework_code == "AFSCTP" else ATQF_CRITERIA
348
-
349
- if scores and len(scores) == len(criteria_dict):
350
- score_details = []
351
- for (cid, data_criteria), score_val in zip(criteria_dict.items(), scores):
352
- score_emoji = "🟢" if score_val >= 4 else "🟡" if score_val == 3 else "🔴"
353
- score_details.append(f"{cid}:{score_emoji}{score_val}")
354
- md += f" *Scores:* {' | '.join(score_details)}\\n"
355
-
356
- md += "\\n"
357
-
358
- md += "---\\n\\n"
359
-
360
- return md
361
-
362
- def get_country_comparison_data():
363
- """Get data for country comparison charts"""
364
- kb = load_knowledge_base()
365
-
366
- countries = []
367
- afsctp_scores = []
368
- atqf_scores = []
369
-
370
- for country in sorted(kb["countries"].keys()):
371
- countries.append(country)
372
- data = kb["countries"][country]
373
- afsctp_scores.append(data.get("AFSCTP", {}).get("overall_score", 0))
374
- atqf_scores.append(data.get("ATQF", {}).get("overall_score", 0))
375
-
376
- return countries, afsctp_scores, atqf_scores
377
-
378
- def generate_comparison_charts():
379
- """Generate comparison charts for knowledge base"""
380
- countries, afsctp_scores, atqf_scores = get_country_comparison_data()
381
-
382
- if not countries:
383
- return None, None
384
-
385
- # Bar chart comparing countries
386
- fig, ax = plt.subplots(figsize=(14, max(6, len(countries) * 0.5)))
387
-
388
- y_pos = np.arange(len(countries))
389
- height = 0.35
390
-
391
- # Only show frameworks that have data
392
- has_afsctp = any(s > 0 for s in afsctp_scores)
393
- has_atqf = any(s > 0 for s in atqf_scores)
394
-
395
- if has_afsctp:
396
- bars1 = ax.barh(y_pos - height/2 if has_atqf else y_pos, afsctp_scores, height,
397
- label='AFSCTP (Standards)', color='#1565c0', alpha=0.8)
398
- if has_atqf:
399
- bars2 = ax.barh(y_pos + height/2 if has_afsctp else y_pos, atqf_scores, height,
400
- label='ATQF/CTQF (Qualifications)', color='#2e7d32', alpha=0.8)
401
-
402
- ax.set_yticks(y_pos)
403
- ax.set_yticklabels(countries)
404
- ax.set_xlabel('Alignment Score (%)', fontsize=12, fontweight='bold')
405
- ax.set_title('Country Comparison: Framework Alignment Scores', fontsize=14, fontweight='bold')
406
- ax.set_xlim(0, 100)
407
- ax.axvline(x=50, color='gray', linestyle='--', alpha=0.5, label='50% threshold')
408
- ax.legend(loc='lower right')
409
-
410
- plt.tight_layout()
411
- buf = BytesIO()
412
- plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
413
- buf.seek(0)
414
- chart_img = base64.b64encode(buf.read()).decode()
415
- plt.close()
416
-
417
- return chart_img
418
-
419
  def get_criteria_display(criteria_dict, framework_name):
420
  """Generate markdown display of criteria for a specific framework"""
421
- md = f"### 📋 {framework_name} Evaluation Criteria\\n\\n"
422
 
423
  sections = {}
424
  for cid, data in criteria_dict.items():
@@ -428,14 +186,14 @@ def get_criteria_display(criteria_dict, framework_name):
428
  sections[section].append((cid, data))
429
 
430
  for section, items in sections.items():
431
- md += f"\\n#### {section}\\n\\n"
432
  for cid, data in items:
433
- md += f"**{cid} - {data['short_label']}**\\n"
434
- md += "<details><summary>View Scoring Descriptors (0-5)</summary>\\n\\n"
435
  for level, desc in data['levels'].items():
436
  emoji = "🔴" if level <= 1 else "🟡" if level <= 3 else "🟢"
437
- md += f"{emoji} **{level}:** {desc}\\n\\n"
438
- md += "</details>\\n\\n"
439
 
440
  return md
441
 
@@ -467,33 +225,15 @@ def get_folder_for_framework(framework_type):
467
  else:
468
  return ATQF_FOLDER
469
 
470
- def get_available_framework_categories():
471
- """Get list of available framework categories for upload selection"""
472
- categories = []
473
-
474
- # Check AFSCTP
475
- if os.path.exists(AFSCTP_FOLDER) and any(f.endswith('.pdf') for f in os.listdir(AFSCTP_FOLDER)):
476
- categories.append("Standards and Competencies (AFSCTP)")
477
- else:
478
- categories.append("Standards and Competencies (AFSCTP) [No documents yet]")
479
-
480
- # Check ATQF
481
- if os.path.exists(ATQF_FOLDER) and any(f.endswith('.pdf') for f in os.listdir(ATQF_FOLDER)):
482
- categories.append("Qualifications Framework (ATQF/CTQF)")
483
- else:
484
- categories.append("Qualifications Framework (ATQF/CTQF) [No documents yet]")
485
-
486
- return categories
487
-
488
  def extract_text_from_file(file_path):
489
  if file_path.endswith('.pdf'):
490
  with open(file_path, 'rb') as f:
491
  reader = PyPDF2.PdfReader(f)
492
  text = "".join(page.extract_text() for page in reader.pages)
493
- return re.sub(r'\\s+', ' ', text)
494
  elif file_path.endswith('.docx'):
495
  doc = Document(file_path)
496
- return "\\n".join([para.text for para in doc.paragraphs])
497
  else:
498
  with open(file_path, 'r', encoding='utf-8') as f:
499
  return f.read()
@@ -523,7 +263,7 @@ def detect_country(document_text):
523
  response = requests.post(NVIDIA_URL, headers=headers, json=payload)
524
  if response.status_code == 200:
525
  country = response.json()["choices"][0]["message"]["content"].strip()
526
- country = country.replace("The ", "").replace("the ", "").replace(".", "").replace("\\n", " ").strip()
527
  return country if country else "Unknown Country"
528
  except:
529
  pass
@@ -534,17 +274,17 @@ def build_evaluation_prompt(document_text, criteria_dict, framework_name):
534
  prompt_parts = []
535
  prompt_parts.append(f"You are evaluating a national teacher framework against the {framework_name}. ")
536
  prompt_parts.append("For each criterion below, select the SINGLE best description (0-5) that matches the document content. ")
537
- prompt_parts.append("Return ONLY a JSON object where keys are criterion IDs (e.g., 'S1' or 'Q1') and values are integers 0-5. No explanations.\\n")
538
 
539
  for cid, data in criteria_dict.items():
540
- prompt_parts.append(f"\\n{cid} - {data['short_label']}:")
541
  for level, desc in data['levels'].items():
542
  prompt_parts.append(f" [{level}] {desc}")
543
 
544
- prompt_parts.append(f"\\n\\nDOCUMENT TEXT:\\n{document_text[:20000]}\\n")
545
- prompt_parts.append("\\nRespond with valid JSON only: {\\\"S1\\\": 4, \\\"S2\\\": 3, ...} or {\\\"Q1\\\": 5, \\\"Q2\\\": 4, ...}")
546
 
547
- return "\\n".join(prompt_parts)
548
 
549
  def call_nvidia_llm(prompt):
550
  headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"}
@@ -569,7 +309,7 @@ def call_nvidia_llm(prompt):
569
  try:
570
  return json.loads(content)
571
  except json.JSONDecodeError:
572
- match = re.search(r'\\{.*\\}', content, re.DOTALL)
573
  if match:
574
  return json.loads(match.group())
575
  raise Exception(f"Could not parse JSON from: {content[:200]}")
@@ -586,7 +326,7 @@ def generate_charts(df, country, framework_name):
586
  bars = plt.barh(labels, values, color=colors, edgecolor='black', linewidth=0.5)
587
  plt.xlim(0, 5)
588
  plt.xlabel("Score (0-5)", fontsize=12, fontweight='bold')
589
- plt.title(f"{country}\\n{framework_name} Alignment Evaluation", fontsize=16, fontweight='bold', pad=20)
590
  plt.axvline(x=3, color='gray', linestyle='--', alpha=0.5)
591
 
592
  for i, (bar, score) in enumerate(zip(bars, values)):
@@ -613,7 +353,7 @@ def generate_charts(df, country, framework_name):
613
  ax.set_ylim(0, 5)
614
  ax.set_yticks([1, 2, 3, 4, 5])
615
  ax.set_yticklabels(['1', '2', '3', '4', '5'], color="grey", size=8)
616
- ax.set_title(f"{country}\\nAlignment Profile", size=16, fontweight='bold', pad=20)
617
  radar_buf = BytesIO()
618
  plt.savefig(radar_buf, format='png', dpi=150, bbox_inches='tight')
619
  radar_buf.seek(0)
@@ -645,7 +385,7 @@ def generate_charts(df, country, framework_name):
645
  ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=11)
646
  ax.set_yticks([])
647
  ax.set_xlabel(f"Overall Alignment Score", fontsize=14, fontweight='bold')
648
- ax.set_title(f"{country}\\n{percent:.1f}% Aligned with {framework_name}", fontsize=18, fontweight='bold', pad=15)
649
  ax.text(50, 0, f"{percent:.1f}%", ha='center', va='center', fontsize=20, fontweight='bold', color='white' if percent > 50 else 'black')
650
 
651
  gauge_buf = BytesIO()
@@ -677,17 +417,17 @@ def generate_pdf_report(df, country, framework_name, percent, bar_img_data, rada
677
  weak = df[df['Score'] <= 2]['Label'].tolist()
678
  moderate = df[df['Score'] == 3]['Label'].tolist()
679
 
680
- summary_text.append(f"\\nStrengths (Score 4-5): {len(strong)} criteria")
681
  if strong:
682
  for s in strong:
683
  summary_text.append(f" ✓ {s}")
684
 
685
- summary_text.append(f"\\nModerate Alignment (Score 3): {len(moderate)} criteria")
686
  if moderate:
687
  for m in moderate:
688
  summary_text.append(f" ~ {m}")
689
 
690
- summary_text.append(f"\\nPriority Areas (Score 0-2): {len(weak)} criteria")
691
  if weak:
692
  for w in weak:
693
  summary_text.append(f" ✗ {w}")
@@ -872,136 +612,66 @@ ATQF_DISPLAY = get_criteria_display(ATQF_CRITERIA, "ATQF/CTQF (Qualifications)")
872
  # Initialize folders and get initial choices
873
  os.makedirs(AFSCTP_FOLDER, exist_ok=True)
874
  os.makedirs(ATQF_FOLDER, exist_ok=True)
875
-
876
- # Build knowledge base on startup (if API key available)
877
- if NVIDIA_API_KEY:
878
- print("Building knowledge base from existing documents...")
879
- build_knowledge_base_from_documents()
880
-
881
  INITIAL_DROPDOWN_CHOICES = get_frameworks_for_type("Standards and Competencies (AFSCTP)")
882
- INITIAL_GLOSSARY = get_knowledge_glossary()
883
- INITIAL_COMPARISON_CHART = generate_comparison_charts()
884
 
885
  # Gradio UI
886
- with gr.Blocks(title="African Union Teacher Frameworks Knowledge & Assessment Platform") as demo:
887
- gr.Markdown("# 🌍 African Union Teacher Frameworks Knowledge & Assessment Platform")
888
- gr.Markdown("Explore existing evaluations in the **Knowledge Base** or evaluate new documents against the **African Framework of Standards and Competences for Teachers (AFSCTP)** or the **Continental Teacher Qualification Framework (ATQF/CTQF)**.")
889
-
890
- # Create tabs for Knowledge Base and Evaluation
891
- with gr.Tabs() as tabs:
892
-
893
- # ========== KNOWLEDGE BASE TAB ==========
894
- with gr.TabItem("📚 Knowledge Base", id="knowledge"):
895
- with gr.Row():
896
- with gr.Column(scale=1):
897
- gr.Markdown("### Knowledge Base Controls")
898
- refresh_kb_btn = gr.Button("🔄 Refresh Knowledge Base", variant="primary")
899
- kb_status = gr.Textbox(label="Status", value="Ready", interactive=False)
900
-
901
- gr.Markdown("---")
902
- gr.Markdown("### 📊 Statistics")
903
- kb_stats = gr.Markdown("Click 'Refresh' to see statistics")
904
-
905
- with gr.Column(scale=3):
906
- gr.Markdown("### Country Framework Scores Glossary")
907
- glossary_output = gr.Markdown(INITIAL_GLOSSARY)
908
 
909
- with gr.Row():
910
- with gr.Column():
911
- gr.Markdown("### Country Comparison")
912
- if INITIAL_COMPARISON_CHART:
913
- comparison_html = gr.HTML(f'<img src="data:image/png;base64,{INITIAL_COMPARISON_CHART}" style="width:100%; max-width:1200px;">')
914
- else:
915
- comparison_html = gr.HTML("No data available for comparison. Add documents and refresh.")
916
-
917
- # ========== EVALUATION TAB ==========
918
- with gr.TabItem("🔍 Evaluate Document", id="evaluate"):
919
- with gr.Row():
920
- with gr.Column(scale=1):
921
- # Dynamic criteria display based on selection
922
- with gr.Accordion("View Evaluation Criteria & Scoring Descriptors", open=False):
923
- criteria_markdown = gr.Markdown(AFSCTP_DISPLAY)
924
-
925
- gr.Markdown("### Framework Selection")
926
-
927
- # Framework type selector (default to Standards)
928
- framework_selector = gr.Dropdown(
929
- label="Select Evaluation Framework",
930
- choices=["Standards and Competencies (AFSCTP)", "Qualifications Framework (ATQF/CTQF)"],
931
- value="Standards and Competencies (AFSCTP)",
932
- interactive=True
933
- )
934
-
935
- gr.Markdown("### Input Selection")
936
-
937
- # Dropdown for pre-loaded frameworks - now dynamic based on type
938
- framework_dropdown = gr.Dropdown(
939
- label="Select Pre-loaded Framework",
940
- choices=INITIAL_DROPDOWN_CHOICES,
941
- value=INITIAL_DROPDOWN_CHOICES[0] if INITIAL_DROPDOWN_CHOICES else None,
942
- interactive=True
943
- )
944
-
945
- gr.Markdown("**OR Upload New Document**")
946
-
947
- # File upload with framework category selection
948
- with gr.Group():
949
- file_input = gr.File(label="Upload Document", file_types=[".pdf", ".docx", ".txt"])
950
- upload_framework_selector = gr.Dropdown(
951
- label="Select Framework Category for Upload",
952
- choices=get_available_framework_categories(),
953
- value=get_available_framework_categories()[0] if get_available_framework_categories() else None,
954
- interactive=True
955
- )
956
-
957
- evaluate_btn = gr.Button("Evaluate Framework", variant="primary")
958
- status_text = gr.Textbox(label="Status", value="Waiting", interactive=False)
959
- country_text = gr.Textbox(label="Detected Country", value="Unknown", interactive=False)
960
-
961
- pdf_output = gr.File(label="Download PDF Report", visible=False)
962
-
963
- with gr.Column(scale=2):
964
- summary_output = gr.Markdown("")
965
- gauge_output = gr.HTML("")
966
-
967
- with gr.Row():
968
- with gr.Column():
969
- gr.Markdown("### Detailed Scores by Criterion")
970
- bar_output = gr.HTML()
971
- with gr.Column():
972
- gr.Markdown("### Alignment Profile")
973
- radar_output = gr.HTML()
974
-
975
- with gr.Row():
976
- table_output = gr.Dataframe(label="Detailed Evaluation Results")
977
-
978
- # ========== EVENT HANDLERS ==========
979
-
980
- # Refresh knowledge base
981
- def refresh_knowledge_base():
982
- if not NVIDIA_API_KEY:
983
- return "❌ NVIDIA_API_KEY not set. Cannot refresh.", "No data available", "Please configure NVIDIA_API_KEY"
984
- try:
985
- kb = build_knowledge_base_from_documents()
986
- glossary = get_knowledge_glossary()
987
- chart = generate_comparison_charts()
988
 
989
- # Update statistics
990
- total_countries = len(kb.get("countries", {}))
991
- total_docs = len(kb.get("processed_docs", {}))
992
- stats_text = f"**{total_countries}** countries evaluated\\n**{total_docs}** documents processed"
 
 
 
993
 
994
- chart_html = f'<img src="data:image/png;base64,{chart}" style="width:100%; max-width:1200px;">' if chart else "No comparison data available"
995
 
996
- return "✅ Knowledge base refreshed", stats_text, glossary, chart_html
997
- except Exception as e:
998
- return f" Error: {str(e)}", "Error occurred", f"Error: {str(e)}"
999
-
1000
- refresh_kb_btn.click(
1001
- fn=refresh_knowledge_base,
1002
- outputs=[kb_status, kb_stats, glossary_output, comparison_html]
1003
- )
1004
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1005
  # Update dropdown and criteria when framework type changes
1006
  def on_framework_change(framework_type):
1007
  choices = get_frameworks_for_type(framework_type)
@@ -1018,20 +688,9 @@ with gr.Blocks(title="African Union Teacher Frameworks Knowledge & Assessment Pl
1018
  )
1019
 
1020
  # Main evaluation handler
1021
- def handle_evaluation(file, framework_type, dropdown_selection, upload_framework):
1022
- """Handle evaluation with proper framework type selection"""
1023
- # If file is uploaded, use the upload framework selector
1024
- if file is not None:
1025
- # Clean up the framework type (remove [No documents yet] suffix if present)
1026
- clean_framework = upload_framework.split(" [")[0] if upload_framework else framework_type
1027
- return process_inputs(file, clean_framework, None)
1028
- else:
1029
- # Use the main framework selector for dropdown selection
1030
- return process_inputs(None, framework_type, dropdown_selection)
1031
-
1032
  evaluate_btn.click(
1033
- fn=handle_evaluation,
1034
- inputs=[file_input, framework_selector, framework_dropdown, upload_framework_selector],
1035
  outputs=[table_output, summary_output, bar_output, radar_output, gauge_output,
1036
  gr.Number(visible=False), status_text, country_text, pdf_output]
1037
  ).then(
 
22
  AFSCTP_FOLDER = "./frameworks/standards"
23
  ATQF_FOLDER = "./frameworks/qualifications"
24
 
 
 
 
25
  # ---------- AFSCTP SCORECARD (Standards and Competencies) ----------
26
  AFSCTP_CRITERIA = {
27
  "S1": {
 
174
  }
175
  }
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  def get_criteria_display(criteria_dict, framework_name):
178
  """Generate markdown display of criteria for a specific framework"""
179
+ md = f"### 📋 {framework_name} Evaluation Criteria\n\n"
180
 
181
  sections = {}
182
  for cid, data in criteria_dict.items():
 
186
  sections[section].append((cid, data))
187
 
188
  for section, items in sections.items():
189
+ md += f"\n#### {section}\n\n"
190
  for cid, data in items:
191
+ md += f"**{cid} - {data['short_label']}**\n"
192
+ md += "<details><summary>View Scoring Descriptors (0-5)</summary>\n\n"
193
  for level, desc in data['levels'].items():
194
  emoji = "🔴" if level <= 1 else "🟡" if level <= 3 else "🟢"
195
+ md += f"{emoji} **{level}:** {desc}\n\n"
196
+ md += "</details>\n\n"
197
 
198
  return md
199
 
 
225
  else:
226
  return ATQF_FOLDER
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  def extract_text_from_file(file_path):
229
  if file_path.endswith('.pdf'):
230
  with open(file_path, 'rb') as f:
231
  reader = PyPDF2.PdfReader(f)
232
  text = "".join(page.extract_text() for page in reader.pages)
233
+ return re.sub(r'\s+', ' ', text)
234
  elif file_path.endswith('.docx'):
235
  doc = Document(file_path)
236
+ return "\n".join([para.text for para in doc.paragraphs])
237
  else:
238
  with open(file_path, 'r', encoding='utf-8') as f:
239
  return f.read()
 
263
  response = requests.post(NVIDIA_URL, headers=headers, json=payload)
264
  if response.status_code == 200:
265
  country = response.json()["choices"][0]["message"]["content"].strip()
266
+ country = country.replace("The ", "").replace("the ", "").replace(".", "").replace("\n", " ").strip()
267
  return country if country else "Unknown Country"
268
  except:
269
  pass
 
274
  prompt_parts = []
275
  prompt_parts.append(f"You are evaluating a national teacher framework against the {framework_name}. ")
276
  prompt_parts.append("For each criterion below, select the SINGLE best description (0-5) that matches the document content. ")
277
+ prompt_parts.append("Return ONLY a JSON object where keys are criterion IDs (e.g., 'S1' or 'Q1') and values are integers 0-5. No explanations.\n")
278
 
279
  for cid, data in criteria_dict.items():
280
+ prompt_parts.append(f"\n{cid} - {data['short_label']}:")
281
  for level, desc in data['levels'].items():
282
  prompt_parts.append(f" [{level}] {desc}")
283
 
284
+ prompt_parts.append(f"\n\nDOCUMENT TEXT:\n{document_text[:20000]}\n")
285
+ prompt_parts.append("\nRespond with valid JSON only: {\"S1\": 4, \"S2\": 3, ...} or {\"Q1\": 5, \"Q2\": 4, ...}")
286
 
287
+ return "\n".join(prompt_parts)
288
 
289
  def call_nvidia_llm(prompt):
290
  headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"}
 
309
  try:
310
  return json.loads(content)
311
  except json.JSONDecodeError:
312
+ match = re.search(r'\{.*\}', content, re.DOTALL)
313
  if match:
314
  return json.loads(match.group())
315
  raise Exception(f"Could not parse JSON from: {content[:200]}")
 
326
  bars = plt.barh(labels, values, color=colors, edgecolor='black', linewidth=0.5)
327
  plt.xlim(0, 5)
328
  plt.xlabel("Score (0-5)", fontsize=12, fontweight='bold')
329
+ plt.title(f"{country}\n{framework_name} Alignment Evaluation", fontsize=16, fontweight='bold', pad=20)
330
  plt.axvline(x=3, color='gray', linestyle='--', alpha=0.5)
331
 
332
  for i, (bar, score) in enumerate(zip(bars, values)):
 
353
  ax.set_ylim(0, 5)
354
  ax.set_yticks([1, 2, 3, 4, 5])
355
  ax.set_yticklabels(['1', '2', '3', '4', '5'], color="grey", size=8)
356
+ ax.set_title(f"{country}\nAlignment Profile", size=16, fontweight='bold', pad=20)
357
  radar_buf = BytesIO()
358
  plt.savefig(radar_buf, format='png', dpi=150, bbox_inches='tight')
359
  radar_buf.seek(0)
 
385
  ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=11)
386
  ax.set_yticks([])
387
  ax.set_xlabel(f"Overall Alignment Score", fontsize=14, fontweight='bold')
388
+ ax.set_title(f"{country}\n{percent:.1f}% Aligned with {framework_name}", fontsize=18, fontweight='bold', pad=15)
389
  ax.text(50, 0, f"{percent:.1f}%", ha='center', va='center', fontsize=20, fontweight='bold', color='white' if percent > 50 else 'black')
390
 
391
  gauge_buf = BytesIO()
 
417
  weak = df[df['Score'] <= 2]['Label'].tolist()
418
  moderate = df[df['Score'] == 3]['Label'].tolist()
419
 
420
+ summary_text.append(f"\nStrengths (Score 4-5): {len(strong)} criteria")
421
  if strong:
422
  for s in strong:
423
  summary_text.append(f" ✓ {s}")
424
 
425
+ summary_text.append(f"\nModerate Alignment (Score 3): {len(moderate)} criteria")
426
  if moderate:
427
  for m in moderate:
428
  summary_text.append(f" ~ {m}")
429
 
430
+ summary_text.append(f"\nPriority Areas (Score 0-2): {len(weak)} criteria")
431
  if weak:
432
  for w in weak:
433
  summary_text.append(f" ✗ {w}")
 
612
  # Initialize folders and get initial choices
613
  os.makedirs(AFSCTP_FOLDER, exist_ok=True)
614
  os.makedirs(ATQF_FOLDER, exist_ok=True)
 
 
 
 
 
 
615
  INITIAL_DROPDOWN_CHOICES = get_frameworks_for_type("Standards and Competencies (AFSCTP)")
 
 
616
 
617
  # Gradio UI
618
+ with gr.Blocks(title="African Union Teacher Frameworks Self Assessment Tool") as demo:
619
+ gr.Markdown("# African Union Teacher Frameworks Self Assessment Tool")
620
+ gr.Markdown("Evaluate national teacher frameworks against the **African Framework of Standards and Competences for Teachers (AFSCTP)** or the **Continental Teacher Qualification Framework (ATQF/CTQF)**.")
621
+
622
+ with gr.Row():
623
+ with gr.Column(scale=1):
624
+ # Dynamic criteria display based on selection
625
+ with gr.Accordion("View Evaluation Criteria & Scoring Descriptors", open=False):
626
+ criteria_markdown = gr.Markdown(AFSCTP_DISPLAY)
 
 
 
 
 
 
 
 
 
 
 
 
 
627
 
628
+ gr.Markdown("### Framework Selection")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
 
630
+ # Framework type selector (default to Standards)
631
+ framework_selector = gr.Dropdown(
632
+ label="Select Evaluation Framework",
633
+ choices=["Standards and Competencies (AFSCTP)", "Qualifications Framework (ATQF/CTQF)"],
634
+ value="Standards and Competencies (AFSCTP)",
635
+ interactive=True
636
+ )
637
 
638
+ gr.Markdown("### Input Selection")
639
 
640
+ # Dropdown for pre-loaded frameworks - now dynamic based on type
641
+ framework_dropdown = gr.Dropdown(
642
+ label="Select Pre-loaded Framework",
643
+ choices=INITIAL_DROPDOWN_CHOICES,
644
+ value=INITIAL_DROPDOWN_CHOICES[0] if INITIAL_DROPDOWN_CHOICES else None,
645
+ interactive=True
646
+ )
647
+
648
+ gr.Markdown("**OR**")
649
+
650
+ # File upload
651
+ file_input = gr.File(label="Upload Custom Document", file_types=[".pdf", ".docx", ".txt"])
652
+
653
+ evaluate_btn = gr.Button("Evaluate Framework", variant="primary")
654
+ status_text = gr.Textbox(label="Status", value="Waiting", interactive=False)
655
+ country_text = gr.Textbox(label="Detected Country", value="Unknown", interactive=False)
656
+
657
+
658
+ pdf_output = gr.File(label="Download PDF Report", visible=False)
659
+
660
+ with gr.Column(scale=2):
661
+ summary_output = gr.Markdown("")
662
+ gauge_output = gr.HTML("")
663
+
664
+ with gr.Row():
665
+ with gr.Column():
666
+ gr.Markdown("### Detailed Scores by Criterion")
667
+ bar_output = gr.HTML()
668
+ with gr.Column():
669
+ gr.Markdown("### Alignment Profile")
670
+ radar_output = gr.HTML()
671
+
672
+ with gr.Row():
673
+ table_output = gr.Dataframe(label="Detailed Evaluation Results")
674
+
675
  # Update dropdown and criteria when framework type changes
676
  def on_framework_change(framework_type):
677
  choices = get_frameworks_for_type(framework_type)
 
688
  )
689
 
690
  # Main evaluation handler
 
 
 
 
 
 
 
 
 
 
 
691
  evaluate_btn.click(
692
+ fn=process_inputs,
693
+ inputs=[file_input, framework_selector, framework_dropdown],
694
  outputs=[table_output, summary_output, bar_output, radar_output, gauge_output,
695
  gr.Number(visible=False), status_text, country_text, pdf_output]
696
  ).then(