Spaces:
Sleeping
Sleeping
Monish Vijay Kumar
Deploy Northwestern CS Kiosk Docker Space with checkpoint and inference runtime
7056be6 | """Blueprint for office hours: list slots by course, course+day, or person.""" | |
| from __future__ import annotations | |
| from typing import Any, Dict, List, Tuple | |
| import difflib | |
| from .base import AnalysisContext, Blueprint, BlueprintResult, Fact | |
| from ..data.utils import canonicalize_name | |
| from ..mcp.context_resolver import expand_day_abbreviation | |
| def _day_matches(row: Dict[str, Any], day: str) -> bool: | |
| """Check if row's Day matches the given day (handles W vs wednesday, etc.).""" | |
| val = (row.get("Day") or "").strip().lower() | |
| if not day or not val: | |
| return True | |
| return (day in val) or (val in day) | |
| def _resolve_day(day_raw: str) -> str: | |
| """Resolve day abbreviation or pass through (today/tomorrow resolved in executor).""" | |
| expanded = expand_day_abbreviation(day_raw) | |
| return (expanded or day_raw).strip().lower() | |
| def _find_course_rows(context: AnalysisContext, course: str) -> Tuple[List[Dict[str, Any]], List[str]]: | |
| """Return all office-hours rows matching the course. Returns (rows, notes).""" | |
| notes: List[str] = [] | |
| dataset = context.catalog.try_get("office_hours") | |
| if not dataset: | |
| return [], ["Office hours dataset not found."] | |
| canonical = canonicalize_name(course) | |
| matches: List[Dict[str, Any]] = [] | |
| seen: set[int] = set() | |
| for row in dataset.records: | |
| val = row.get("Course Name") | |
| if not val: | |
| continue | |
| val_canon = canonicalize_name(val) | |
| if canonical == val_canon or canonical in val_canon: | |
| if id(row) not in seen: | |
| matches.append(row) | |
| seen.add(id(row)) | |
| if not matches: | |
| names = [r.get("Course Name") for r in dataset.records if r.get("Course Name")] | |
| names = list(dict.fromkeys(names)) | |
| closest = difflib.get_close_matches(course, names, n=1, cutoff=0.6) | |
| if closest: | |
| for row in dataset.records: | |
| if row.get("Course Name") == closest[0] and id(row) not in seen: | |
| matches.append(row) | |
| seen.add(id(row)) | |
| notes.append(f"Showing results for '{closest[0]}' (closest match).") | |
| return matches, notes | |
| def _find_person_rows(context: AnalysisContext, person: str) -> Tuple[List[Dict[str, Any]], List[str]]: | |
| """Return all rows where the person appears in Course Name or Description of Use.""" | |
| notes: List[str] = [] | |
| dataset = context.catalog.try_get("office_hours") | |
| if not dataset: | |
| return [], ["Office hours dataset not found."] | |
| canonical = canonicalize_name(person) | |
| tokens = [canonicalize_name(t) for t in person.strip().split() if len(t) > 1] | |
| cols = ["Course Name", "Description of Use"] | |
| matches: List[Dict[str, Any]] = [] | |
| seen: set[int] = set() | |
| for row in dataset.records: | |
| if id(row) in seen: | |
| continue | |
| for col in cols: | |
| val = row.get(col) | |
| if not val or not isinstance(val, str): | |
| continue | |
| val_canon = canonicalize_name(val) | |
| if canonical in val_canon: | |
| matches.append(row) | |
| seen.add(id(row)) | |
| break | |
| for tok in tokens: | |
| if tok in val_canon: | |
| matches.append(row) | |
| seen.add(id(row)) | |
| break | |
| else: | |
| continue | |
| break | |
| if not matches and tokens: | |
| first = tokens[0] | |
| for row in dataset.records: | |
| if id(row) in seen: | |
| continue | |
| for col in cols: | |
| val = row.get(col) or "" | |
| if first in canonicalize_name(str(val)): | |
| matches.append(row) | |
| seen.add(id(row)) | |
| notes.append(f"Showing office hours matching '{person.strip().split()[0]}'.") | |
| break | |
| return matches, notes | |
| def _rows_to_facts(rows: List[Dict[str, Any]], subject: str, source: str | None) -> List[Fact]: | |
| """Convert office-hours rows to Fact objects.""" | |
| field_map = [ | |
| ("Course Name", "course_name"), | |
| ("Description of Use", "description"), | |
| ("Day", "day"), | |
| ("Time", "time"), | |
| ("Room", "room"), | |
| ("Capacity", "capacity"), | |
| ("Availability", "availability"), | |
| ] | |
| facts: List[Fact] = [] | |
| for row in rows: | |
| value: Dict[str, Any] = {} | |
| for col, key in field_map: | |
| if row.get(col): | |
| value[key] = row[col] | |
| facts.append( | |
| Fact( | |
| subject=subject, | |
| predicate="office_hours", | |
| value=value, | |
| source=source, | |
| confidence=0.9, | |
| ) | |
| ) | |
| return facts | |
| class OfficeHoursBlueprint(Blueprint): | |
| """ | |
| Office hours lookup: by course, course+day, or person. | |
| 1. course only: all office hours for that course (time + location) | |
| 2. course + day: slots for that course on that day | |
| 3. person only: all slots where the person holds office hours (course names + times) | |
| """ | |
| name = "office_hours" | |
| def run(self, context: AnalysisContext, **kwargs: Any) -> BlueprintResult: | |
| params = dict(kwargs) | |
| course = (params.get("course") or params.get("class_name") or "").strip() | |
| day_raw = (params.get("day") or "").strip() | |
| person = (params.get("person") or params.get("name") or "").strip() | |
| office = context.catalog.try_get("office_hours") | |
| source = office.origin if office else None | |
| # 3. Person only: "When are Prof Hammond's office hours?" | |
| if person and not course: | |
| rows, notes = _find_person_rows(context, person) | |
| if not rows: | |
| return BlueprintResult( | |
| self.name, | |
| params, | |
| facts=[], | |
| notes=[f"No office hours found for '{person}'."] + notes, | |
| ) | |
| if day_raw: | |
| day = _resolve_day(day_raw) | |
| rows = [r for r in rows if _day_matches(r, day)] | |
| if not rows: | |
| return BlueprintResult( | |
| self.name, | |
| params, | |
| facts=[], | |
| notes=[f"No office hours found for {person} on {day_raw}."], | |
| ) | |
| facts = _rows_to_facts(rows, person, source) | |
| return BlueprintResult(self.name, params, facts=facts, notes=notes) | |
| # 1 & 2. Course (with optional day) | |
| if course: | |
| rows, notes = _find_course_rows(context, course) | |
| if not rows: | |
| return BlueprintResult( | |
| self.name, | |
| params, | |
| facts=[], | |
| notes=[f"Course '{course}' not found."] + notes, | |
| ) | |
| if day_raw: | |
| day = _resolve_day(day_raw) | |
| rows = [r for r in rows if _day_matches(r, day)] | |
| if not rows: | |
| return BlueprintResult( | |
| self.name, | |
| params, | |
| facts=[], | |
| notes=[f"No office hours for '{course}' on {day_raw}."], | |
| ) | |
| facts = _rows_to_facts(rows, course, source) | |
| return BlueprintResult(self.name, params, facts=facts, notes=notes) | |
| return BlueprintResult( | |
| self.name, | |
| params, | |
| facts=[], | |
| notes=["Specify a course (e.g. CS 336) or person (e.g. Prof Hammond) to look up office hours."], | |
| ) | |