dp-zp-agent/tools/answers/finalize.py
2026-11-15 16:27:08 +01:00

114 lines
5.7 KiB
Python

"""Last presentation pass over every accepted ZP Wiki answer (RAG and GraphRAG).
It changes wording only, never facts:
- sources are written as full links, never as numbers ("[1]");
- notices that explain the system instead of answering ("Vedúci je určený
podľa autora profilovej stránky…", "Profil obsahuje aj záznamy bez úplného
dátumu…", "Rok nedopĺňam…") are dropped;
- "this detail is not recorded" lines stay only when the answer has nothing
else; then one plain sentence says so;
- technical labels ("Názov / téma zo zdroja:") become plain words.
"""
from __future__ import annotations
import re
DROP = [
r"Vedúci je určený podľa autora profilovej stránky[^\n]*",
r"Profil obsahuje aj záznamy bez úplného dátumu[^\n]*",
r"Rok nedopĺňam z roku práce\.?",
r"Údaje sú z indexovan[^\n]*",
r"Indexovan[éý][^\n]*",
r"Zobrazený je obmedzený výber[^\n]*",
]
PHASE_TAG = re.compile(r"\s+\((?:zaznamenaný výsledok|priebežný stav|plán|zadanie|podklad|navrhovaný cieľ|téma alebo názov|"
r"úloha označená ako splnená)\)(?=\s*$)", re.M)
# Students whose only match is an assignment/plan or reading: one line at the end instead of a block each.
SIDE = {"téma je v zadaní, téme alebo pláne práce; zaznamenaný výsledok k nej sa nenašiel.":
"Téma je len v zadaní alebo pláne, bez zaznamenaného výsledku",
"záznamy dokladajú len štúdium alebo poznámky k téme, nie vlastnú realizáciu.":
"Len štúdium alebo poznámky k téme"}
BULLET = re.compile(r"^- (?P<work>[^;\n:]+?); (?P<phase>[^;:\n]+?)(?:; záznam (?P<date>[^:\n]+?))?: (?P<text>.*)$", re.M)
LABEL_PREFIX = re.compile(r"^(?:-\s+|\*(?:Názov|Meno)[^*]*\*:\s*|(?:Názov|Téma)\s*:\s*)+")
def _bullet(match: re.Match) -> str:
text = LABEL_PREFIX.sub("", match.group("text").strip()).strip()
if not text or text.startswith("#"):
return "" # a bare heading ("## Diplomový projekt 2") says nothing
if re.fullmatch(r"(?:Vedúci\w*:\s*)?(?:(?:Ing|Mgr|doc|prof|Bc|RNDr)\.\s*)*[A-ZÁ-Ž]\w+\s+[A-ZÁ-Ž]\w+(?:,\s*PhD\.?)?\.?", text):
return "" # only the supervisor's name, already the subject of the answer
date = (match.group("date") or "").replace("(rok neuvedený)", "").strip(" :()")
return f"- {match.group('work').strip()}" + (f" ({date})" if date else "") + f": {text}"
def _collapse_side_people(blocks: list[str]) -> list[str]:
groups, current = [], []
for block in blocks:
if re.fullmatch(r"\*\*[^*\n]+\*\*", block) and current:
groups.append(current)
current = []
current.append(block)
if current:
groups.append(current)
kept, side = [], {}
for group in groups:
head = re.fullmatch(r"\*\*([^*\n]+)\*\*", group[0])
label = next((v for k, v in SIDE.items() for b in group if b.endswith(k)), None)
if head and label:
side.setdefault(label, []).append(head.group(1))
else:
kept += group
return kept + [f"{label}: {', '.join(names)}." for label, names in side.items()]
MISSING = re.compile(r"(?:požadovaný údaj nie je v tomto zápise doložený|v podkladoch chýbajú doložené údaje|"
r"Požadované údaje k tomuto stretnutiu nie sú v dostupnom výbere doložené)", re.I)
RENAME = [
(r"Názov / téma zo zdroja:\s*(?:\*Názov [^*]*\*:\s*)?", "Téma: "),
(r"Vedúci práce \(autor profilu študenta\):", "Vedúci práce:"),
(r"Konzultant práce \(zo zdroja\):", "Konzultant:"),
]
INLINE_CITE = re.compile(r"\s*(?:\[\d+\])+(?!\(|:)")
REFERENCE_LINE = re.compile(r"^\[\d+\]:\s*\S+\s*$", re.M)
SOURCES = re.compile(r"^Zdroj(?:e)?:\s*(.+)$", re.M)
def _links(line: str) -> list[str]:
urls = re.findall(r"\]\((https?://[^)\s]+)\)", line) or re.findall(r"https?://[^\s,)\]]+", line)
return list(dict.fromkeys(urls))
def finalize(text: str, source_urls: list[str] | None = None) -> str:
if not isinstance(text, str) or not text.strip():
return text
out = REFERENCE_LINE.sub("", text)
sources, label = [], None
for match in SOURCES.finditer(out):
sources += _links(match.group(1))
label = label or match.group(0).split(":", 1)[0]
out = SOURCES.sub("", out)
out = INLINE_CITE.sub("", out)
for pattern in DROP:
out = re.sub(pattern, "", out)
for pattern, replacement in RENAME:
out = re.sub(pattern, replacement, out)
out = PHASE_TAG.sub("", out)
out = BULLET.sub(_bullet, out)
out = re.sub(r"^- Téma alebo názov doložený pri tej istej práci:\s*(?:\*[^*]*\*:\s*)?", "- Téma: ", out, flags=re.M)
out = re.sub(r"(?m)^(\s*- [^:\n]+:)\s+-\s+", r"\1 ", out) # "Diplomový projekt: - Rozpoznávanie…'
blocks = _collapse_side_people([b.strip() for b in re.split(r"\n\s*\n", out) if b.strip()])
substantive = [b for b in blocks if not MISSING.search(b) and not re.fullmatch(r"\*\*[^*]+\*\*", b)
and not re.fullmatch(r"(?:\*\*[^*]+\*\*\s*)?(?:Posledný záznam[^\n]*|Zadanie a ciele:)", b)]
if substantive:
blocks = [b for b in blocks if not MISSING.search(b)]
blocks = [b for i, b in enumerate(blocks) # a heading left without its content
if not (re.fullmatch(r"[A-ZÁ-Ž][^\n]{0,40}:", b) and (i + 1 == len(blocks) or blocks[i + 1].startswith("**")))]
else:
heads = [b for b in blocks if re.fullmatch(r"\*\*[^*]+\*\*", b) or b.startswith("**")]
blocks = heads[:1] + ["ZP Wiki k tomu nemá záznam."]
sources = list(dict.fromkeys(sources)) # rewrite the answer's own source line, never add one
if sources:
blocks.append((label or "Zdroje") + ": " + ", ".join(sources))
return "\n\n".join(blocks)