93 lines
4.3 KiB
Python
93 lines
4.3 KiB
Python
"""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 []}
|