"""Blueprint that handles advisor relationships in both directions.""" from __future__ import annotations from typing import Any, Dict, List, Optional from .base import AnalysisContext, Blueprint, BlueprintResult, Fact from ..data.utils import canonicalize_name class AdvisorshipBlueprint(Blueprint): """Return advisor relationships - works for both students and faculty. Automatically detects whether the input is a student or faculty member: - If student: returns their faculty advisor(s) - If faculty: returns their advisees (students they advise) """ name = "advisorship" def run(self, context: AnalysisContext, **kwargs: Any) -> BlueprintResult: target_name = ( kwargs.get("name") or kwargs.get("student") or kwargs.get("faculty") or kwargs.get("person") or "" ).strip() if not target_name: return BlueprintResult( self.name, kwargs, facts=[], notes=["Please provide a student or faculty name."] ) facts: List[Fact] = [] notes: List[str] = [] # Check if it's a student student_record = self._find_student(context, target_name) # Check if it's a faculty member faculty_record = self._find_faculty(context, target_name) # If both match (same name in both), return both relationships if student_record and faculty_record: # Get student's advisors advisor_facts, advisor_notes = self._get_student_advisors(context, student_record) facts.extend(advisor_facts) notes.extend(advisor_notes) # Get faculty's advisees advisee_facts, advisee_notes = self._get_faculty_advisees(context, faculty_record) facts.extend(advisee_facts) notes.extend(advisee_notes) elif student_record: # Only found as student - get their advisors advisor_facts, advisor_notes = self._get_student_advisors(context, student_record) facts.extend(advisor_facts) notes.extend(advisor_notes) elif faculty_record: # Only found as faculty - get their advisees advisee_facts, advisee_notes = self._get_faculty_advisees(context, faculty_record) facts.extend(advisee_facts) notes.extend(advisee_notes) else: # Not found in either - check if staff can help with advising staff_facts, staff_notes = self._get_advising_staff(context, target_name) if staff_facts: facts.extend(staff_facts) notes.extend(staff_notes) else: notes.append(f"'{target_name}' not found in student or faculty records.") return BlueprintResult(self.name, kwargs, facts=facts, notes=notes) def _find_student(self, context: AnalysisContext, name: str) -> Optional[Dict[str, Any]]: """Find a student record by name.""" students = context.catalog.try_get("students") if not students: return None canonical = canonicalize_name(name) for row in students.records: if canonicalize_name(row.get("Name")) == canonical: return row return None def _find_faculty(self, context: AnalysisContext, name: str) -> Optional[Dict[str, Any]]: """Find a faculty record by name.""" faculty = context.catalog.try_get("faculty") if not faculty: return None canonical = canonicalize_name(name) for row in faculty.records: if canonicalize_name(row.get("Name")) == canonical: return row return None def _get_student_advisors( self, context: AnalysisContext, student_record: Dict[str, Any] ) -> tuple[List[Fact], List[str]]: """Get the advisors for a student.""" advisors = context.catalog.resolve_relationship("student_to_advisors", student_record) faculty_entity = context.catalog.try_get("faculty") faculty_origin = faculty_entity.origin if faculty_entity else None facts: List[Fact] = [] for advisor in advisors: facts.append( Fact( subject=student_record.get("Name"), predicate="advisor", value=advisor.get("Name"), source=faculty_origin, confidence=0.9, ) ) notes: List[str] = [] if not facts: advisor_text = student_record.get("Advisor(s)") if advisor_text and advisor_text.lower() not in {"none", "n/a"}: notes.append(f"Advisor listed as '{advisor_text}' but not found in faculty roster.") else: notes.append(f"No advisor listed for {student_record['Name']}.") return facts, notes def _get_faculty_advisees( self, context: AnalysisContext, faculty_record: Dict[str, Any] ) -> tuple[List[Fact], List[str]]: """Get the students advised by a faculty member.""" advisees = context.catalog.resolve_relationship("faculty_to_advisees", faculty_record) students_entity = context.catalog.try_get("students") students_origin = students_entity.origin if students_entity else None facts: List[Fact] = [] for student in advisees: facts.append( Fact( subject=faculty_record.get("Name"), predicate="advisee", value=student.get("Name"), source=students_origin, confidence=0.8, ) ) notes: List[str] = [] if not facts: notes.append(f"No advisees found for {faculty_record['Name']} in the current roster.") return facts, notes def _get_advising_staff( self, context: AnalysisContext, target_name: str ) -> tuple[List[Fact], List[str]]: """Fallback: return staff members who handle advising.""" staff = context.catalog.try_get("staff") if not staff: return [], [] matches = [] for row in staff.records: haystack = " ".join(str(row.get(field, "")).lower() for field in ("Title", "Role")) if "advisor" in haystack: matches.append(row) if not matches: return [], [] facts = [ Fact( subject=row.get("Name", "Staff"), predicate="staff_support", value={ "title": row.get("Title"), "role": row.get("Role"), "location": row.get("Room Location"), }, source=staff.origin, confidence=0.8, ) for row in matches ] notes = [f"'{target_name}' not found, but here are advising staff contacts."] return facts, notes