homeroom-copilot / scripts /export_student_evaluation_data.py
chand2345's picture
Add risk-aware action plan prompting
36dd9b8
Raw
History Blame Contribute Delete
10.7 kB
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]"
# Edit this list when preparing a specific evaluation set. The defaults provide
# 2 Low, 2 Moderate, 2 High, and 2 Critical risk students from the demo dataset.
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()