"""Deterministic roster: which works (topics) and students belong to given years. "Aké témy sa riešili v roku 2024 a akí študenti?" asks for a list, not for prose. Every listed work is one unchanged source row (its title/topic when available) of the same student, work and year; no model call is needed. """ from __future__ import annotations from collections import defaultdict import re from tools.answers.context_policy import normalized from tools.knowledge.evidence import claim_eligible from tools.answers.content_selection import administrative_only MAX_WORKS = 40 _RANK = {"topic": 0, "assignment": 1} def roster_question(spec) -> bool: n = normalized(spec.question) years = spec.years or (spec.period and spec.period.field == "work_year") return bool( spec.intent == "metadata" and not spec.people and years and re.search(r"\b(?:tem\w*|prac\w*|projekt\w*|na com sa|co sa|ries\w*)\b", n) and not re.search(r"\b(?:tag\w*|klucov\w*|kategor\w*|kolko|pocet|nazov\w*|autor\w*|vedu\w*|vedie)\b", n) ) def _year_label(spec): if spec.period and spec.period.field == "work_year": first, last = spec.period.start[:4], spec.period.end[:4] return f"z rokov {first} – {last}" if first != last else f"z roku {first}" years = sorted(set(spec.years)) return ("z roku " + str(years[0])) if len(years) == 1 else "z rokov " + ", ".join(map(str, years)) def answer_roster(spec, index, *, engine: str) -> dict: best = {} for row in index["units"]: if (not claim_eligible(row) or not row.get("work_label") or not row.get("work_year") or not spec.matches_evidence(row) or administrative_only(row["text"])): continue if spec.years and row["work_year"] not in spec.years: continue key = (row["person"], row["source_path"], row["work_label"]) # A bare heading ("## Diplomový projekt 2024") names the work, not its topic. heading = bool(re.fullmatch(r"\s*#+\s.*", row["text"])) or normalized(row["text"]) == normalized(row["work_label"]) rank = 9 if heading else _RANK.get(row["kind"], 2) if key not in best or rank < best[key][0]: best[key] = (rank, row) works = sorted((row for _, row in best.values()), key=lambda r: (r["work_year"], normalized(r["person"]), r["work_label"])) truncated = len(works) > MAX_WORKS shown = works[:MAX_WORKS] base = { "engine": engine, "index_version": index["index_version"], "knowledge_version": index["version"], "accepted_for_delivery": True, "claims": [], } if not shown: return {**base, "status": "no_answer", "reason": "no_content_evidence", "answer": f"V dostupných podkladoch sa nenašli práce {_year_label(spec)} zodpovedajúce otázke.", "evidence": [], "source_urls": [], "presentation_notes": [], "complete": False, "truncated": False, "truncation_reasons": []} by_person = defaultdict(list) for row in shown: by_person[row["person"]].append(row) students = len({r["person"] for r in works}) lines = [f"Práce {_year_label(spec)} v ZP Wiki: {len(works)} prác, {students} študentov."] urls = [] for person, rows in by_person.items(): lines.append(f"**{person}**") for row in rows: if re.fullmatch(r"\s*#+\s.*", row["text"]) or normalized(row["text"]) == normalized(row["work_label"]): lines.append(f"- {row['work_label']} (téma v profile neuvedená)") continue label = "téma alebo názov" if row["kind"] == "topic" else "zadanie" if row["kind"] == "assignment" else "podklad" lines.append(f"- {row['work_label']}; {label}: {row['text'].strip()}") url = rows[0].get("source_url") if url: lines.append(f"Zdroj: {url}") urls.append(url) notes = [] if truncated: notes.append(f"Zobrazených je prvých {MAX_WORKS} prác; zúž otázku typom práce alebo rokom.") lines.append(notes[-1]) return {**base, "status": "ok", "reason": None, "answer": "\n\n".join(lines), "evidence": shown, "source_urls": list(dict.fromkeys(urls)), "presentation_notes": notes, "complete": not truncated, "truncated": truncated, "truncation_reasons": ["result_limit"] if truncated else []}