afbench / app.py
michsethowusu's picture
Update app.py
a12ac8a verified
import gradio as gr
import os
import requests
import json
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from io import BytesIO
import base64
import PyPDF2
from docx import Document
import re
from matplotlib.backends.backend_pdf import PdfPages
from datetime import datetime
# ---------- Configuration ----------
NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY")
NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
MODEL = "meta/llama-4-maverick-17b-128e-instruct"
# Two separate folders for the two framework types
AFSCTP_FOLDER = "./frameworks/standards"
ATQF_FOLDER = "./frameworks/qualifications"
# ---------- AFSCTP SCORECARD (Standards and Competencies) ----------
AFSCTP_CRITERIA = {
"S1": {
"short_label": "Career Path Structure",
"section": "Career & CPD",
"levels": {
0: "No career stages or progression pathway defined for teachers",
1: "Generic promotion levels exist but not based on professional competence",
2: "Some progression levels defined but not aligned to the 4-stage continental model",
3: "Four stages mentioned (Beginner, Proficient, Expert, Distinguished) but lack clear descriptors or requirements",
4: "Four stages defined with competence descriptors for each stage but missing progression criteria",
5: "Fully defined 4-stage career path (Beginner→Proficient→Expert→Distinguished) with detailed competences, CPD requirements, and progression criteria for each stage"
}
},
"S2": {
"short_label": "Teacher Professional Standards (5 Domains)",
"section": "Professional Standards",
"levels": {
0: "No professional standards or competences defined for teachers",
1: "Generic teaching guidelines exist but not structured as professional standards",
2: "Some professional domains covered but incomplete (missing 2-3 domains from the 5-domain framework)",
3: "All 5 domains mentioned (Knowledge, Skills, Values, Partnerships, Leadership) but lack detailed competences/descriptors",
4: "All 5 domains with substantial competences defined but partial alignment to AFSCTP",
5: "Full alignment: All 5 domains (Professional Knowledge, Skills, Values/Attributes, Partnerships, Leadership) with comprehensive competences matching AFSCTP"
}
},
"S3": {
"short_label": "School Leadership Standards",
"section": "School Leadership",
"levels": {
0: "No standards defined for school leaders/principals",
1: "Generic management standards only (administrative focus)",
2: "Some leadership standards exist but cover fewer than 4 of the 7 key areas",
3: "Leadership standards exist but not aligned to the 7-standard continental framework",
4: "Substantial alignment with 7 standards but missing detailed competences for career stages",
5: "Full alignment with all 7 leadership standards: Developing Self/Others, Leading Knowledge/Practice, Managing Resources, Promoting Improvement, Generating Resources, Supporting Learners, and Community Engagement"
}
},
"S4": {
"short_label": "Competency Assessment Framework",
"section": "Professional Regulation",
"levels": {
0: "No competency assessment or testing mentioned",
1: "Informal/self-assessment only with no standardized testing",
2: "Assessment exists but not explicitly linked to career stage progression",
3: "Competency testing mentioned for some transitions but not standardized across all stages",
4: "Structured competency tests for progression but lacking technology-based implementation (CBT)",
5: "Comprehensive competency testing for all career transitions (Beginner→Proficient→Expert→Distinguished) using standardized computer-based testing aligned to AFSCTP"
}
},
"S5": {
"short_label": "Continental Strategy Alignment",
"section": "Policy Context",
"levels": {
0: "No reference to African Union education strategies or international frameworks",
1: "Generic reference to African education without specific strategy alignment",
2: "Mentions CESA 16-25 but not integrated into the standards framework",
3: "References CESA 16-25 and Agenda 2063 but lacks explicit implementation pathways",
4: "Substantial alignment with CESA 16-25 Strategic Objective 1 and continental teacher policies",
5: "Explicit alignment with CESA 16-25, Agenda 2063, SDG4c, and Continental Teacher Mobility Protocol with clear implementation strategies"
}
}
}
# ---------- ATQF/CTQF SCORECARD (Qualifications Framework) ----------
ATQF_CRITERIA = {
"Q1": {
"short_label": "Minimum Entry Qualifications",
"section": "Entry Requirements",
"levels": {
0: "No minimum entry standards defined; certificates or lower qualifications accepted",
1: "Diploma or sub-degree qualifications accepted as minimum entry",
2: "Degree required but not specific to education (any degree accepted without pedagogy)",
3: "Education degree required but not aligned to B.Ed./PGDE model or allows many exceptions",
4: "B.Ed. or Degree+PGDE required as primary pathways with minor administrative exceptions",
5: "Strict requirement: Only B.Ed. (5-year integrated or 3+2 model) or Bachelor's degree + PGDE accepted as entry pathways"
}
},
"Q2": {
"short_label": "B.Ed. Curriculum Structure",
"section": "Initial Teacher Education",
"levels": {
0: "No B.Ed. standards defined; unstructured teacher preparation",
1: "B.Ed. exists but without specific credit requirements or professional module percentages",
2: "B.Ed. defined but <150 credits OR <40% professional modules (missing key elements)",
3: "B.Ed. meets duration requirement but either credits <150 OR professional modules <40%",
4: "Meets 150 credits and ≥40% professional modules but teaching practice <1 year or not structured",
5: "Full CTQF compliance: 150 credits minimum, ≥40% professional modules, 1-year teaching practice (split or continuous), ISCED Level 6 alignment"
}
},
"Q3": {
"short_label": "PGDE Curriculum Structure",
"section": "Initial Teacher Education",
"levels": {
0: "No postgraduate diploma pathway defined for degree holders",
1: "Generic postgraduate certificate without specific structure",
2: "PGDE exists but <50 credits OR <3 months teaching practice",
3: "PGDE with 50 credits but teaching practice <3 months or not supervised",
4: "PGDE with 50 credits and 3 months practice but missing ISCED alignment or quality framework",
5: "Full compliance: 50-credit PGDE with 3-month teaching practice, ISCED Level 6 professional orientation, for graduates entering teaching"
}
},
"Q4": {
"short_label": "Accreditation System (Dual)",
"section": "Quality Assurance",
"levels": {
0: "No accreditation system for teacher education programs",
1: "Voluntary or informal accreditation only",
2: "Generic institutional accreditation only (no professional teaching accreditation)",
3: "Professional accreditation mentioned but not conducted by Teaching Regulatory Authority",
4: "Dual accreditation exists (Generic HE Council + Teaching Council) but gaps in implementation or coverage",
5: "Mandatory dual accreditation: Generic (Higher Education Council/Commission) + Professional (Teaching Regulatory Authority) with AQRM/ASG-QA alignment"
}
},
"Q5": {
"short_label": "CPD Credit System",
"section": "Career & CPD",
"levels": {
0: "No CPD requirements or continuous education defined",
1: "CPD encouraged but voluntary/not structured",
2: "CPD required but no credit system or accumulation mechanism defined",
3: "CPD credit system exists but <55 credits per 5-year cycle OR not linked to career progression",
4: "55-credit system per 5-year cycle defined but missing endorsement mechanisms for providers",
5: "Full system: 55 CPD credits per 5-year cycle covering 7 modules, endorsed providers, linked to career stage advancement (Beginner→Proficient→Expert→Distinguished)"
}
},
"Q6": {
"short_label": "School Leadership Certification",
"section": "School Leadership",
"levels": {
0: "No specific qualification requirements for school leaders",
1: "Generic leadership training accepted without specific diploma requirements",
2: "Leadership qualification required but <60 credits or not mandatory",
3: "60-credit Diploma in School Leadership required but not linked to teacher career stages",
4: "60-credit Diploma required + must have reached Proficient stage but missing competency assessment",
5: "Full compliance: 60-credit Diploma in School Leadership + Proficient teacher status + competency assessment + workplace project for headteacher/principal appointment"
}
},
"Q7": {
"short_label": "Induction & Mentoring System",
"section": "Professional Entry",
"levels": {
0: "No induction or mentoring program for newly qualified teachers",
1: "Brief orientation (days/weeks) only without structured mentoring",
2: "Induction exists but <2 years duration or not mandatory",
3: "Mandatory induction exists but lacks formal mentoring structure or assessment",
4: "2-year induction/mentoring specified but not fully integrated with CPD portfolio",
5: "Mandatory structured 2-year induction and mentoring program with assigned mentors, assessment, and integration into CPD credits for Beginner teachers"
}
}
}
def get_criteria_display(criteria_dict, framework_name):
"""Generate markdown display of criteria for a specific framework"""
md = f"### 📋 {framework_name} Evaluation Criteria\n\n"
sections = {}
for cid, data in criteria_dict.items():
section = data['section']
if section not in sections:
sections[section] = []
sections[section].append((cid, data))
for section, items in sections.items():
md += f"\n#### {section}\n\n"
for cid, data in items:
md += f"**{cid} - {data['short_label']}**\n"
md += "<details><summary>View Scoring Descriptors (0-5)</summary>\n\n"
for level, desc in data['levels'].items():
emoji = "🔴" if level <= 1 else "🟡" if level <= 3 else "🟢"
md += f"{emoji} **{level}:** {desc}\n\n"
md += "</details>\n\n"
return md
def get_frameworks_for_type(framework_type):
"""Get available frameworks based on selected type"""
if framework_type == "Standards and Competencies (AFSCTP)":
folder = AFSCTP_FOLDER
default_msg = "No standards frameworks available"
else:
folder = ATQF_FOLDER
default_msg = "No qualifications frameworks available"
# Create folder if it doesn't exist
if not os.path.exists(folder):
os.makedirs(folder)
return [default_msg + f" - Add PDFs to {folder}"]
pdf_files = [f for f in os.listdir(folder) if f.lower().endswith('.pdf')]
if not pdf_files:
return [default_msg + f" - Add PDFs to {folder}"]
return ["Select a framework..."] + sorted([f[:-4] for f in pdf_files])
def get_folder_for_framework(framework_type):
"""Return the appropriate folder path based on framework type"""
if framework_type == "Standards and Competencies (AFSCTP)":
return AFSCTP_FOLDER
else:
return ATQF_FOLDER
def extract_text_from_file(file_path):
if file_path.endswith('.pdf'):
with open(file_path, 'rb') as f:
reader = PyPDF2.PdfReader(f)
text = "".join(page.extract_text() for page in reader.pages)
return re.sub(r'\s+', ' ', text)
elif file_path.endswith('.docx'):
doc = Document(file_path)
return "\n".join([para.text for para in doc.paragraphs])
else:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def detect_country(document_text):
"""Detect country from document"""
if not NVIDIA_API_KEY:
return "Unknown Country"
prompt = f"""Identify which African country this teacher education framework document belongs to.
Look for country names, national bodies (e.g., "Kenya National Qualifications Framework", "Ghana Education Service").
Return ONLY the country name (e.g., "Kenya", "Ghana", "Nigeria"). If uncertain, return "Unknown".
DOCUMENT TEXT (first 3000 characters):
{document_text[:3000]}
Country:"""
try:
headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"max_tokens": 50
}
response = requests.post(NVIDIA_URL, headers=headers, json=payload)
if response.status_code == 200:
country = response.json()["choices"][0]["message"]["content"].strip()
country = country.replace("The ", "").replace("the ", "").replace(".", "").replace("\n", " ").strip()
return country if country else "Unknown Country"
except:
pass
return "Unknown Country"
def build_evaluation_prompt(document_text, criteria_dict, framework_name):
"""Build prompt with anchored descriptors"""
prompt_parts = []
prompt_parts.append(f"You are evaluating a national teacher framework against the {framework_name}. ")
prompt_parts.append("For each criterion below, select the SINGLE best description (0-5) that matches the document content. ")
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")
for cid, data in criteria_dict.items():
prompt_parts.append(f"\n{cid} - {data['short_label']}:")
for level, desc in data['levels'].items():
prompt_parts.append(f" [{level}] {desc}")
prompt_parts.append(f"\n\nDOCUMENT TEXT:\n{document_text[:20000]}\n")
prompt_parts.append("\nRespond with valid JSON only: {\"S1\": 4, \"S2\": 3, ...} or {\"Q1\": 5, \"Q2\": 4, ...}")
return "\n".join(prompt_parts)
def call_nvidia_llm(prompt):
headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"}
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.1,
"max_tokens": 2000
}
response = requests.post(NVIDIA_URL, headers=headers, json=payload)
if response.status_code != 200:
raise Exception(f"NVIDIA API error: {response.status_code} - {response.text}")
content = response.json()["choices"][0]["message"]["content"]
content = content.strip()
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
content = content.strip()
try:
return json.loads(content)
except json.JSONDecodeError:
match = re.search(r'\{.*\}', content, re.DOTALL)
if match:
return json.loads(match.group())
raise Exception(f"Could not parse JSON from: {content[:200]}")
def generate_charts(df, country, framework_name):
"""Generate visualization charts"""
labels = df["Label"].tolist()
scores = df["Score"].tolist()
values = np.array(scores)
# Bar chart
plt.figure(figsize=(14, 9))
colors = ['#b71c1c' if v == 0 else '#d32f2f' if v == 1 else '#f57c00' if v == 2 else '#fbc02d' if v == 3 else '#689f38' if v == 4 else '#2e7d32' for v in values]
bars = plt.barh(labels, values, color=colors, edgecolor='black', linewidth=0.5)
plt.xlim(0, 5)
plt.xlabel("Score (0-5)", fontsize=12, fontweight='bold')
plt.title(f"{country}\n{framework_name} Alignment Evaluation", fontsize=16, fontweight='bold', pad=20)
plt.axvline(x=3, color='gray', linestyle='--', alpha=0.5)
for i, (bar, score) in enumerate(zip(bars, values)):
plt.text(score + 0.1, bar.get_y() + bar.get_height()/2, f"{score}", va='center', fontweight='bold', fontsize=11)
plt.tight_layout()
bar_buf = BytesIO()
plt.savefig(bar_buf, format='png', dpi=150, bbox_inches='tight')
bar_buf.seek(0)
bar_img = base64.b64encode(bar_buf.read()).decode()
plt.close()
# Radar chart
N = len(labels)
angles = [n / float(N) * 2 * np.pi for n in range(N)]
angles += angles[:1]
values_radar = list(values) + [values[0]]
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(polar=True))
ax.plot(angles, values_radar, 'o-', linewidth=2.5, color='#1565c0')
ax.fill(angles, values_radar, alpha=0.25, color='#1565c0')
ax.set_xticks(angles[:-1])
ax.set_xticklabels(labels, size=10)
ax.set_ylim(0, 5)
ax.set_yticks([1, 2, 3, 4, 5])
ax.set_yticklabels(['1', '2', '3', '4', '5'], color="grey", size=8)
ax.set_title(f"{country}\nAlignment Profile", size=16, fontweight='bold', pad=20)
radar_buf = BytesIO()
plt.savefig(radar_buf, format='png', dpi=150, bbox_inches='tight')
radar_buf.seek(0)
radar_img = base64.b64encode(radar_buf.read()).decode()
plt.close()
# Gauge
total_score = sum(values)
max_score = len(values) * 5
percent = (total_score / max_score) * 100
fig, ax = plt.subplots(figsize=(10, 4))
ax.barh([0], [100], color='#e0e0e0', height=0.5, edgecolor='black')
if percent < 40:
color = '#d32f2f'
elif percent < 60:
color = '#f57c00'
elif percent < 75:
color = '#fbc02d'
elif percent < 90:
color = '#689f38'
else:
color = '#2e7d32'
ax.barh([0], [percent], color=color, height=0.5, edgecolor='black')
ax.set_xlim(0, 100)
ax.set_xticks([0, 25, 50, 75, 100])
ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=11)
ax.set_yticks([])
ax.set_xlabel(f"Overall Alignment Score", fontsize=14, fontweight='bold')
ax.set_title(f"{country}\n{percent:.1f}% Aligned with {framework_name}", fontsize=18, fontweight='bold', pad=15)
ax.text(50, 0, f"{percent:.1f}%", ha='center', va='center', fontsize=20, fontweight='bold', color='white' if percent > 50 else 'black')
gauge_buf = BytesIO()
plt.savefig(gauge_buf, format='png', dpi=150, bbox_inches='tight')
gauge_buf.seek(0)
gauge_img = base64.b64encode(gauge_buf.read()).decode()
plt.close()
return bar_img, radar_img, gauge_img, percent
def generate_pdf_report(df, country, framework_name, percent, bar_img_data, radar_img_data, gauge_img_data):
"""Generate PDF report"""
pdf_path = "/tmp/framework_evaluation_report.pdf"
with PdfPages(pdf_path) as pdf:
# Page 1: Cover
fig = plt.figure(figsize=(8.5, 11))
fig.text(0.5, 0.9, f"{country}", ha='center', fontsize=24, fontweight='bold')
fig.text(0.5, 0.85, f"{framework_name} Evaluation Report", ha='center', fontsize=18, fontweight='bold')
fig.text(0.5, 0.80, f"Overall Alignment: {percent:.1f}%", ha='center', fontsize=16,
color='#2e7d32' if percent >= 75 else '#f57c00' if percent >= 50 else '#d32f2f')
fig.text(0.5, 0.75, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", ha='center', fontsize=10, style='italic')
# Summary
summary_text = []
summary_text.append("EXECUTIVE SUMMARY")
summary_text.append("-" * 50)
strong = df[df['Score'] >= 4]['Label'].tolist()
weak = df[df['Score'] <= 2]['Label'].tolist()
moderate = df[df['Score'] == 3]['Label'].tolist()
summary_text.append(f"\nStrengths (Score 4-5): {len(strong)} criteria")
if strong:
for s in strong:
summary_text.append(f" ✓ {s}")
summary_text.append(f"\nModerate Alignment (Score 3): {len(moderate)} criteria")
if moderate:
for m in moderate:
summary_text.append(f" ~ {m}")
summary_text.append(f"\nPriority Areas (Score 0-2): {len(weak)} criteria")
if weak:
for w in weak:
summary_text.append(f" ✗ {w}")
y_pos = 0.65
for line in summary_text:
weight = 'bold' if line.isupper() or line.startswith('-') else 'normal'
size = 11 if line.startswith(' ') else 12
fig.text(0.1, y_pos, line, fontsize=size, fontweight=weight)
y_pos -= 0.025
plt.axis('off')
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 2: Bar Chart
if bar_img_data:
fig, ax = plt.subplots(figsize=(11, 8.5))
img_data = base64.b64decode(bar_img_data)
img = plt.imread(BytesIO(img_data))
ax.imshow(img)
ax.axis('off')
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 3: Radar Chart
if radar_img_data:
fig, ax = plt.subplots(figsize=(11, 8.5))
img_data = base64.b64decode(radar_img_data)
img = plt.imread(BytesIO(img_data))
ax.imshow(img)
ax.axis('off')
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 4: Detailed Table
fig, ax = plt.subplots(figsize=(11, 8.5))
ax.axis('tight')
ax.axis('off')
table_data = [['Criterion', 'Section', 'Score', 'Assessment Level']]
for _, row in df.iterrows():
table_data.append([
row['Label'],
row['Section'],
str(row['Score']),
row['Level'][:80] + '...' if len(row['Level']) > 80 else row['Level']
])
table = ax.table(cellText=table_data, cellLoc='left', loc='center',
colWidths=[0.25, 0.15, 0.08, 0.52])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 2)
# Style header
for i in range(4):
table[(0, i)].set_facecolor('#1565c0')
table[(0, i)].set_text_props(weight='bold', color='white')
# Color code scores
for i in range(1, len(table_data)):
score = int(table_data[i][2])
color = '#b71c1c' if score == 0 else '#d32f2f' if score == 1 else '#f57c00' if score == 2 else '#fbc02d' if score == 3 else '#689f38' if score == 4 else '#2e7d32'
table[(i, 2)].set_facecolor(color)
table[(i, 2)].set_text_props(weight='bold', color='white')
pdf.savefig(fig, bbox_inches='tight')
plt.close()
return pdf_path
def evaluate_framework(file_path, framework_type):
"""Main evaluation function"""
if not NVIDIA_API_KEY:
return pd.DataFrame(), "❌ NVIDIA_API_KEY not set.", "", "", "", 0, "Error", None, "Unknown Country"
# Select criteria based on framework type
if framework_type == "Standards and Competencies (AFSCTP)":
criteria_dict = AFSCTP_CRITERIA
framework_name = "AFSCTP"
else:
criteria_dict = ATQF_CRITERIA
framework_name = "ATQF/CTQF"
try:
text = extract_text_from_file(file_path)
if not text.strip():
return pd.DataFrame(), "❌ File contains no extractable text.", "", "", "", 0, "Error", None, "Unknown Country"
if len(text) < 100:
return pd.DataFrame(), "❌ Extracted text too short.", "", "", "", 0, "Error", None, "Unknown Country"
except Exception as e:
return pd.DataFrame(), f"❌ Text extraction error: {e}", "", "", "", 0, "Error", None, "Unknown Country"
country = detect_country(text)
try:
prompt = build_evaluation_prompt(text, criteria_dict, framework_name)
results = call_nvidia_llm(prompt)
except Exception as e:
return pd.DataFrame(), f"❌ AI evaluation error: {e}", "", "", "", 0, "Error", None, country
# Process results
rows = []
for cid, data in criteria_dict.items():
score = results.get(cid, 0)
try:
score = int(score)
if score < 0 or score > 5:
score = 0
except:
score = 0
rows.append({
"ID": cid,
"Label": data['short_label'],
"Section": data['section'],
"Score": score,
"Level": data['levels'][score]
})
if not rows:
return pd.DataFrame(), "❌ No valid results returned.", "", "", "", 0, "Error", None, country
df = pd.DataFrame(rows)
# Generate charts
bar_img, radar_img, gauge_img, percent = generate_charts(df, country, framework_name)
# Generate PDF
pdf_path = generate_pdf_report(df, country, framework_name, percent, bar_img, radar_img, gauge_img)
# Create summary
strong_criteria = df[df['Score'] >= 4]['Label'].tolist()
weak_criteria = df[df['Score'] <= 2]['Label'].tolist()
summary_md = f"""
### **{country} - {framework_name} Evaluation Report**
**Overall Alignment: {percent:.1f}%**
**Framework:** {framework_type}
**Scale:** 0 = Non-existent | 1 = Weak | 2 = Developing | 3 = Moderate | 4 = Strong | 5 = Excellent
**🟢 Strengths ({len(strong_criteria)}):** {', '.join(strong_criteria) if strong_criteria else 'None identified'}
**🔴 Priority Areas ({len(weak_criteria)}):** {', '.join(weak_criteria) if weak_criteria else 'None - all criteria adequate'}
"""
# Convert images to HTML
bar_html = f'<img src="data:image/png;base64,{bar_img}" style="width:100%; max-width:900px;">'
radar_html = f'<img src="data:image/png;base64,{radar_img}" style="width:100%; max-width:600px;">'
gauge_html = f'<img src="data:image/png;base64,{gauge_img}" style="width:100%; max-width:800px;">'
return df, summary_md, bar_html, radar_html, gauge_html, percent, "Success", pdf_path, country
def process_inputs(file, framework_type, dropdown_selection):
"""Process either uploaded file or selected framework"""
# Determine which input to use
if dropdown_selection and not dropdown_selection.startswith("Select") and not dropdown_selection.startswith("No"):
folder = get_folder_for_framework(framework_type)
file_path = os.path.join(folder, dropdown_selection + ".pdf")
if not os.path.exists(file_path):
return pd.DataFrame(), f"❌ File not found: {file_path}", "", "", "", 0, "Error", None, "Unknown Country", gr.update(visible=False)
elif file is not None:
file_path = file.name
else:
return pd.DataFrame(), "Please upload a document or select a framework from the dropdown.", "", "", "", 0, "Waiting", None, "Unknown Country", gr.update(visible=False)
df, summary, bar_html, radar_html, gauge_html, percent, status, pdf_path, country = evaluate_framework(file_path, framework_type)
if status != "Success":
return df, summary, "", "", "", percent, status, country, gr.update(visible=False)
return df, summary, bar_html, radar_html, gauge_html, percent, status, country, gr.update(value=pdf_path, visible=True)
# Generate displays for both frameworks
AFSCTP_DISPLAY = get_criteria_display(AFSCTP_CRITERIA, "AFSCTP (Standards & Competencies)")
ATQF_DISPLAY = get_criteria_display(ATQF_CRITERIA, "ATQF/CTQF (Qualifications)")
# Initialize folders and get initial choices
os.makedirs(AFSCTP_FOLDER, exist_ok=True)
os.makedirs(ATQF_FOLDER, exist_ok=True)
INITIAL_DROPDOWN_CHOICES = get_frameworks_for_type("Standards and Competencies (AFSCTP)")
# Gradio UI
with gr.Blocks(title="African Union Teacher Frameworks Self Assessment Tool") as demo:
gr.Markdown("# African Union Teacher Frameworks Self Assessment Tool")
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)**.")
with gr.Row():
with gr.Column(scale=1):
# Dynamic criteria display based on selection
with gr.Accordion("View Evaluation Criteria & Scoring Descriptors", open=False):
criteria_markdown = gr.Markdown(AFSCTP_DISPLAY)
gr.Markdown("### Framework Selection")
# Framework type selector (default to Standards)
framework_selector = gr.Dropdown(
label="Select Evaluation Framework",
choices=["Standards and Competencies (AFSCTP)", "Qualifications Framework (ATQF/CTQF)"],
value="Standards and Competencies (AFSCTP)",
interactive=True
)
gr.Markdown("### Input Selection")
# Dropdown for pre-loaded frameworks - now dynamic based on type
framework_dropdown = gr.Dropdown(
label="Select Pre-loaded Framework",
choices=INITIAL_DROPDOWN_CHOICES,
value=INITIAL_DROPDOWN_CHOICES[0] if INITIAL_DROPDOWN_CHOICES else None,
interactive=True
)
gr.Markdown("**OR**")
# File upload
file_input = gr.File(label="Upload Custom Document", file_types=[".pdf", ".docx", ".txt"])
evaluate_btn = gr.Button("Evaluate Framework", variant="primary")
status_text = gr.Textbox(label="Status", value="Waiting", interactive=False)
country_text = gr.Textbox(label="Detected Country", value="Unknown", interactive=False)
pdf_output = gr.File(label="Download PDF Report", visible=False)
with gr.Column(scale=2):
summary_output = gr.Markdown("")
gauge_output = gr.HTML("")
with gr.Row():
with gr.Column():
gr.Markdown("### Detailed Scores by Criterion")
bar_output = gr.HTML()
with gr.Column():
gr.Markdown("### Alignment Profile")
radar_output = gr.HTML()
with gr.Row():
table_output = gr.Dataframe(label="Detailed Evaluation Results")
# Update dropdown and criteria when framework type changes
def on_framework_change(framework_type):
choices = get_frameworks_for_type(framework_type)
criteria = AFSCTP_DISPLAY if framework_type == "Standards and Competencies (AFSCTP)" else ATQF_DISPLAY
return {
framework_dropdown: gr.update(choices=choices, value=choices[0] if choices else None),
criteria_markdown: criteria
}
framework_selector.change(
fn=on_framework_change,
inputs=framework_selector,
outputs=[framework_dropdown, criteria_markdown]
)
# Main evaluation handler
evaluate_btn.click(
fn=process_inputs,
inputs=[file_input, framework_selector, framework_dropdown],
outputs=[table_output, summary_output, bar_output, radar_output, gauge_output,
gr.Number(visible=False), status_text, country_text, pdf_output]
).then(
lambda ft: gr.update(value=get_frameworks_for_type(ft)[0] if get_frameworks_for_type(ft) else None),
inputs=[framework_selector],
outputs=[framework_dropdown]
)
demo.launch()