| from __future__ import annotations |
|
|
| import json |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
| PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| if str(PROJECT_ROOT) not in sys.path: |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
| import pandas as pd |
|
|
| from src.intervention_engine import ( |
| InterventionRecommendation, |
| load_intervention_library, |
| recommend_interventions, |
| ) |
| from src.risk_engine import assess_student_risk |
| from src.root_cause import generate_root_causes |
|
|
| DATA_PATH = PROJECT_ROOT / "data" / "students.csv" |
| EVALUATION_DIR = PROJECT_ROOT / "evaluation" |
| EVALUATION_DATASET_PATH = EVALUATION_DIR / "evaluation_dataset.json" |
| ACTION_PLAN_PLACEHOLDER = "[PASTE ACTION PLAN HERE]" |
|
|
| |
| |
| STUDENT_NAMES = [ |
| "Olivia Moore", |
| "Ava Martinez", |
| "Noah Taylor", |
| "William Anderson", |
| "John Smith", |
| "Emma Brown", |
| "Sarah Johnson", |
| "Lily Green", |
| ] |
|
|
| PROFILE_FIELDS = [ |
| "student_id", |
| "student_name", |
| "gender", |
| "grade_level", |
| "homeroom", |
| "attendance_p1", |
| "attendance_p2", |
| "attendance_p3", |
| "grade_p1", |
| "grade_p2", |
| "grade_p3", |
| "homework_p1", |
| "homework_p2", |
| "homework_p3", |
| "behavior_incident_count", |
| "behavior_notes", |
| "teacher_notes", |
| ] |
|
|
| RISK_FACTOR_FIELDS = [ |
| "attendance_risk", |
| "academic_risk", |
| "homework_risk", |
| "behavior_risk", |
| "engagement_risk", |
| ] |
|
|
|
|
| def json_safe(value: Any) -> Any: |
| """Convert pandas/numpy scalar values into JSON-safe Python values.""" |
| if pd.isna(value): |
| return None |
| if hasattr(value, "item"): |
| return value.item() |
| return value |
|
|
|
|
| def load_students() -> pd.DataFrame: |
| """Load the demo student dataset.""" |
| return pd.read_csv(DATA_PATH) |
|
|
|
|
| def selected_student_names() -> list[str]: |
| """Return CLI-provided student names or the configured default list.""" |
| return sys.argv[1:] or STUDENT_NAMES |
|
|
|
|
| def find_student(students: pd.DataFrame, student_name: str) -> pd.Series: |
| """Find a student row by case-insensitive exact name.""" |
| matches = students[ |
| students["student_name"].astype(str).str.casefold() == student_name.casefold() |
| ] |
| if matches.empty: |
| raise ValueError(f"Student not found in dataset: {student_name}") |
| return matches.iloc[0] |
|
|
|
|
| def build_risk_assessment(student: pd.Series) -> dict[str, Any]: |
| """Calculate risk assessment values using the existing risk engine.""" |
| assessment = assess_student_risk( |
| attendance=float(student["attendance_p3"]), |
| grades=float(student["grade_p3"]), |
| homework=float(student["homework_p3"]), |
| behavior_incidents=int(student["behavior_incident_count"]), |
| ) |
| return {key: json_safe(value) for key, value in assessment.items()} |
|
|
|
|
| def build_root_causes(student: pd.Series) -> list[str]: |
| """Generate root-cause analysis using the existing root-cause engine.""" |
| return generate_root_causes( |
| attendance_p1=float(student["attendance_p1"]), |
| attendance_p2=float(student["attendance_p2"]), |
| attendance_p3=float(student["attendance_p3"]), |
| grade_p1=float(student["grade_p1"]), |
| grade_p2=float(student["grade_p2"]), |
| grade_p3=float(student["grade_p3"]), |
| homework_p1=float(student["homework_p1"]), |
| homework_p2=float(student["homework_p2"]), |
| homework_p3=float(student["homework_p3"]), |
| behavior_notes=str(student.get("behavior_notes", "") or ""), |
| teacher_notes=str(student.get("teacher_notes", "") or ""), |
| ) |
|
|
|
|
| def build_student_profile(student: pd.Series) -> dict[str, Any]: |
| """Extract student profile fields for an evaluation case.""" |
| return {field: json_safe(student.get(field)) for field in PROFILE_FIELDS} |
|
|
|
|
| def build_risk_factors(risk_assessment: dict[str, Any]) -> list[str]: |
| """Summarize non-low risk factors for evaluator review.""" |
| factors = [ |
| f"{field.replace('_', ' ').title()}: {risk_assessment[field]}" |
| for field in RISK_FACTOR_FIELDS |
| if risk_assessment.get(field) != "Low" |
| ] |
| if not factors: |
| factors.append("No elevated category risks identified.") |
| factors.append(f"Overall Risk: {risk_assessment['overall_risk']}") |
| return factors |
|
|
|
|
| def intervention_to_dict( |
| recommendation: InterventionRecommendation, |
| ) -> dict[str, Any]: |
| """Convert a recommendation into evaluation evidence.""" |
| return { |
| "intervention_name": recommendation.intervention_name, |
| "category": recommendation.category, |
| "summary": recommendation.summary, |
| "expected_benefits": recommendation.expected_benefits, |
| "evidence_level": recommendation.evidence_level, |
| "source": recommendation.source, |
| "reference_url": recommendation.reference_url, |
| "recommendation_reason": recommendation.recommendation_reason, |
| } |
|
|
|
|
| def source_to_dict(recommendation: InterventionRecommendation) -> dict[str, str]: |
| """Extract source label and URL for one recommendation.""" |
| return { |
| "label": recommendation.intervention_name, |
| "reference_url": recommendation.reference_url, |
| } |
|
|
|
|
| def build_case( |
| case_id: str, |
| student: pd.Series, |
| intervention_library: list[dict[str, Any]], |
| ) -> dict[str, Any]: |
| """Build one evaluation package without generating an AI action plan.""" |
| risk_assessment = build_risk_assessment(student) |
| root_causes = build_root_causes(student) |
| recommendations = recommend_interventions( |
| root_causes=root_causes, |
| risk_profile=risk_assessment, |
| intervention_library=intervention_library, |
| max_results=5, |
| ) |
| return { |
| "case_id": case_id, |
| "student_name": str(student["student_name"]), |
| "student_profile": build_student_profile(student), |
| "risk_level": risk_assessment["overall_risk"], |
| "risk_factors": build_risk_factors(risk_assessment), |
| "risk_assessment": risk_assessment, |
| "root_cause_analysis": root_causes, |
| "retrieved_evidence_based_interventions": [ |
| intervention_to_dict(recommendation) |
| for recommendation in recommendations |
| ], |
| "evidence_sources": [ |
| source_to_dict(recommendation) |
| for recommendation in recommendations |
| ], |
| "generated_ai_action_plan": ACTION_PLAN_PLACEHOLDER, |
| } |
|
|
|
|
| def markdown_mapping(mapping: dict[str, Any]) -> str: |
| """Render dictionary values as Markdown bullets.""" |
| return "\n".join(f"- **{key}:** {value}" for key, value in mapping.items()) |
|
|
|
|
| def markdown_list(items: list[str]) -> str: |
| """Render strings as Markdown bullets.""" |
| if not items: |
| return "- None" |
| return "\n".join(f"- {item}" for item in items) |
|
|
|
|
| def markdown_interventions(interventions: list[dict[str, Any]]) -> str: |
| """Render retrieved interventions as compact Markdown evidence.""" |
| if not interventions: |
| return "No interventions retrieved." |
|
|
| blocks = [] |
| for index, intervention in enumerate(interventions, start=1): |
| summary = "\n".join( |
| f" - {item}" for item in intervention.get("summary", []) |
| ) |
| benefits = "\n".join( |
| f" - {item}" for item in intervention.get("expected_benefits", []) |
| ) |
| blocks.append( |
| "\n".join( |
| [ |
| f"{index}. **{intervention['intervention_name']}**", |
| f" - Category: {intervention['category']}", |
| f" - Evidence Level: {intervention['evidence_level']}", |
| f" - Source: {intervention['source']}", |
| f" - Reference URL: {intervention['reference_url']}", |
| " - Summary:", |
| summary or " - Not provided", |
| " - Expected Benefits:", |
| benefits or " - Not provided", |
| ( |
| " - Recommendation Reason: " |
| f"{intervention['recommendation_reason']}" |
| ), |
| ] |
| ) |
| ) |
| return "\n\n".join(blocks) |
|
|
|
|
| def markdown_evidence_sources(sources: list[dict[str, str]]) -> str: |
| """Render source links as Markdown bullets.""" |
| if not sources: |
| return "No evidence sources available." |
| return "\n".join( |
| f"- [{source['label']}]({source['reference_url']})" |
| if source["reference_url"] |
| else f"- {source['label']}" |
| for source in sources |
| ) |
|
|
|
|
| def render_case_markdown(case: dict[str, Any]) -> str: |
| """Render one evaluation case using the required section structure.""" |
| return f"""# Student Profile |
| |
| ## Case Metadata |
| |
| - **case_id:** {case["case_id"]} |
| - **student_name:** {case["student_name"]} |
| |
| {markdown_mapping(case["student_profile"])} |
| |
| # Risk Assessment |
| |
| {markdown_mapping(case["risk_assessment"])} |
| |
| # Risk Factors |
| |
| {markdown_list(case["risk_factors"])} |
| |
| # Root Cause Analysis |
| |
| {markdown_list(case["root_cause_analysis"])} |
| |
| # Retrieved Evidence-Based Interventions |
| |
| {markdown_interventions(case["retrieved_evidence_based_interventions"])} |
| |
| # Evidence Sources |
| |
| {markdown_evidence_sources(case["evidence_sources"])} |
| |
| # Generated AI Action Plan |
| |
| {case["generated_ai_action_plan"]} |
| """ |
|
|
|
|
| def export_cases(student_names: list[str]) -> list[dict[str, Any]]: |
| """Export evaluation packages for the provided student names.""" |
| students = load_students() |
| intervention_library = load_intervention_library() |
| EVALUATION_DIR.mkdir(exist_ok=True) |
|
|
| cases = [] |
| for index, student_name in enumerate(student_names, start=1): |
| student = find_student(students, student_name) |
| case = build_case( |
| case_id=f"case_{index:03d}", |
| student=student, |
| intervention_library=intervention_library, |
| ) |
| (EVALUATION_DIR / f"{case['case_id']}.md").write_text( |
| render_case_markdown(case), |
| encoding="utf-8", |
| ) |
| cases.append(case) |
|
|
| EVALUATION_DATASET_PATH.write_text( |
| json.dumps(cases, indent=2, ensure_ascii=False), |
| encoding="utf-8", |
| ) |
| return cases |
|
|
|
|
| def main() -> None: |
| """Generate evaluation packages without calling the AI action-plan pipeline.""" |
| names = selected_student_names() |
| if not names: |
| raise ValueError("Add student names to STUDENT_NAMES or pass names as arguments.") |
|
|
| cases = export_cases(names) |
| print(f"Exported {len(cases)} evaluation cases to {EVALUATION_DIR}") |
| print(f"Wrote dataset JSON to {EVALUATION_DATASET_PATH}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|