"""ContextPack assembler. Composes deterministic packs from retrieval results: - course_constitution: all course_meta records (always loaded) - topic: retrieval top-K - module: full topic content (intent-driven) assessment_safe is enforced upstream (catalog only contains student_access=True records by construction in sub-project #1's _student_visible_files). """ from __future__ import annotations from dataclasses import dataclass, field from scripts.lib.catalog_loader import Catalog from scripts.lib.ranker import RetrievalResult _ASSESSMENT_KINDS = {"question", "slide_answer"} _MODULE_INTENTS = {"summarize", "explain"} @dataclass class ContextPack: course_constitution: list[dict] = field(default_factory=list) topic: list[RetrievalResult] = field(default_factory=list) module: list[dict] = field(default_factory=list) has_assessment_content: bool = False has_redacted_tier: bool = False def assemble( *, intent: str, retrieved: list[RetrievalResult], catalog: Catalog, mode: str = "tutor", ) -> ContextPack: """Compose a ContextPack from an intent + retrieval results + mode. In review mode the module block is expanded whenever the dominant-topic threshold is met, regardless of intent — faculty queries about a concept consistently want every occurrence across the term, not the intent-driven subset that's right for student tutoring. """ by_doc_id = {r["doc_id"]: r for r in catalog.records} constitution = [r for r in catalog.records if r.get("kind") == "course_meta"] topic = list(retrieved) module: list[dict] = [] module_eligible = mode == "review" or intent in _MODULE_INTENTS if module_eligible and len(retrieved) >= 2: # Find the topic with the most retrieved records topic_counts: dict[str, int] = {} for r in retrieved: rec = by_doc_id.get(r.doc_id, {}) t = rec.get("topic", "") if t: topic_counts[t] = topic_counts.get(t, 0) + 1 if topic_counts: dominant_topic = max(topic_counts, key=lambda k: topic_counts[k]) if topic_counts[dominant_topic] >= 2: module = [ rec for rec in catalog.records if rec.get("topic") == dominant_topic ] has_assessment = any( by_doc_id.get(r.doc_id, {}).get("kind") in _ASSESSMENT_KINDS for r in retrieved ) has_redacted = any( by_doc_id.get(r.doc_id, {}).get("privacy_tier") == "redacted_classroom_speech" for r in retrieved ) return ContextPack( course_constitution=constitution, topic=topic, module=module, has_assessment_content=has_assessment, has_redacted_tier=has_redacted, )