""" Utilities for EmpathRAG curated resource corpora. The curated corpus is a JSONL file prepared from official/student-support resources. It intentionally stays separate from the Reddit research corpus. """ from __future__ import annotations import argparse import json from dataclasses import dataclass from pathlib import Path from typing import Iterable REQUIRED_FIELDS = ( "id", "source_id", "source_name", "source_type", "title", "url", "topic", "audience", "risk_level", "usage_mode", "text", "summary", "last_checked", "notes", ) SOURCE_TYPES = { "university_resource", "crisis_resource", "government_public_health", "student_support", "clinician_review_candidate", } TOPICS = { "crisis_immediate_help", "counseling_services", "after_hours_support", "academic_burnout", "advisor_conflict", "isolation_loneliness", "anxiety_stress", "depression_support", "accessibility_disability", "graduate_student_support", "help_seeking_script", "grounding_exercise", "campus_navigation", "therapy_expectations", "peer_support", "emergency_services", } AUDIENCES = { "umd_student", "graduate_student", "student_general", "crisis_support", "supporter_or_friend", } RISK_LEVELS = {"safe", "wellbeing", "crisis_resource", "exclude"} USAGE_MODES = {"retrieval", "wellbeing_only", "crisis_only", "metadata_only"} @dataclass(frozen=True) class ValidationIssue: line_no: int row_id: str message: str def load_jsonl(path: str | Path) -> list[dict]: rows = [] path = Path(path) for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): if not line.strip(): continue try: row = json.loads(line) except json.JSONDecodeError as exc: raise ValueError(f"Invalid JSON on line {line_no}: {exc}") from exc if not isinstance(row, dict): raise ValueError(f"Line {line_no} must be a JSON object.") row["_line_no"] = line_no rows.append(row) return rows def validate_rows(rows: Iterable[dict]) -> list[ValidationIssue]: issues: list[ValidationIssue] = [] seen_ids: set[str] = set() for row in rows: line_no = int(row.get("_line_no", 0)) row_id = str(row.get("id", "")).strip() for field in REQUIRED_FIELDS: if not str(row.get(field, "")).strip(): issues.append(ValidationIssue(line_no, row_id, f"missing field: {field}")) if row_id in seen_ids: issues.append(ValidationIssue(line_no, row_id, "duplicate id")) if row_id: seen_ids.add(row_id) _check_allowed(issues, row, line_no, row_id, "source_type", SOURCE_TYPES) _check_allowed(issues, row, line_no, row_id, "topic", TOPICS) _check_allowed(issues, row, line_no, row_id, "audience", AUDIENCES) _check_allowed(issues, row, line_no, row_id, "risk_level", RISK_LEVELS) _check_allowed(issues, row, line_no, row_id, "usage_mode", USAGE_MODES) text = str(row.get("text", "")).strip() word_count = len(text.split()) if text and not (40 <= word_count <= 300): issues.append( ValidationIssue( line_no, row_id, f"text length {word_count} words outside review band 40-300", ) ) if row.get("risk_level") == "exclude" and row.get("usage_mode") != "metadata_only": issues.append( ValidationIssue( line_no, row_id, "exclude rows must use usage_mode=metadata_only or be removed", ) ) return issues def ingestion_rows(rows: Iterable[dict]) -> list[dict]: """Rows safe to embed into the curated retrieval index.""" usable = [] for row in rows: if row.get("risk_level") == "exclude": continue if row.get("usage_mode") == "metadata_only": continue usable.append({k: v for k, v in row.items() if not k.startswith("_")}) return usable def validate_file(path: str | Path, strict: bool = True) -> tuple[list[dict], list[ValidationIssue]]: rows = load_jsonl(path) issues = validate_rows(rows) if strict and issues: messages = "\n".join( f"line {i.line_no} ({i.row_id or 'no id'}): {i.message}" for i in issues ) raise ValueError(f"Curated corpus validation failed:\n{messages}") return rows, issues def _check_allowed( issues: list[ValidationIssue], row: dict, line_no: int, row_id: str, field: str, allowed: set[str], ) -> None: value = row.get(field) if value and value not in allowed: issues.append( ValidationIssue( line_no, row_id, f"{field}={value!r} is not one of {sorted(allowed)}", ) ) def main() -> int: parser = argparse.ArgumentParser(description="Validate EmpathRAG curated JSONL corpus.") parser.add_argument("path", help="Path to resources_seed.jsonl") parser.add_argument("--non-strict", action="store_true", help="Print issues but exit 0.") args = parser.parse_args() rows, issues = validate_file(args.path, strict=False) usable = ingestion_rows(rows) print(f"Rows: {len(rows)}") print(f"Usable retrieval rows: {len(usable)}") if issues: print(f"Issues: {len(issues)}") for issue in issues: print(f"- line {issue.line_no} ({issue.row_id or 'no id'}): {issue.message}") return 0 if args.non_strict else 1 print("Validation passed.") return 0 if __name__ == "__main__": raise SystemExit(main())