EmpathRAG / src /data /curated_resources.py
MukulRay's picture
Add curated corpus integration scaffold
fadd796
Raw
History Blame
5.88 kB
"""
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())