dp-zp-agent/tools/knowledge/roster.py
jp170na fcfed6a818 Stabilizácia odpovedí
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 16:00:48 +01:00

93 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 []}