369 lines
15 KiB
Python
369 lines
15 KiB
Python
"""Slovak presentation templates for validated facts and literal source excerpts.
|
|
|
|
No model, gold answers or extra retrieval is used here. Task wording can be
|
|
converted to a goal, never to a claim that the task has already been completed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from tools.answers.context_policy import normalized, person_token_pattern, text_urls
|
|
from tools.answers.rag_evidence import WORK_LABELS
|
|
|
|
ACCUSATIVE = {
|
|
"bachelor_thesis": "bakalársku prácu",
|
|
"diploma_thesis": "diplomovú prácu",
|
|
"doctoral_thesis": "dizertačnú prácu",
|
|
"bachelor_project": "bakalársky projekt",
|
|
"diploma_project": "diplomový projekt",
|
|
"team_project": "tímový projekt",
|
|
"research_project": "výskumný projekt",
|
|
}
|
|
|
|
|
|
def owner_in_question(person: str, question: str) -> str | None:
|
|
"""Reuse a bound genitive from the question; never guess a name's declension."""
|
|
words = list(re.finditer(r"[^\W\d_]+(?:[-'][^\W\d_]+)*", question))
|
|
tokens = person.split()
|
|
for index in range(len(words) - len(tokens) + 1):
|
|
found = words[index : index + len(tokens)]
|
|
if not re.search(
|
|
r"\b(?:práca|práce|prácu|prác|projekt|projektu|projektov)\s*$",
|
|
question[: found[0].start()],
|
|
re.I,
|
|
):
|
|
continue
|
|
valid = True
|
|
for token, match in zip(tokens, found):
|
|
valid &= bool(re.fullmatch(person_token_pattern(token), normalized(match[0])))
|
|
if valid:
|
|
owner = question[found[0].start() : found[-1].end()]
|
|
# Internal canonicalization supplies nominative catalog names. It
|
|
# is not a genitive and must use the natural person-first template.
|
|
return owner if normalized(owner) != normalized(person) else None
|
|
return None
|
|
|
|
|
|
def work_subject(person: str, kind: str | None, question: str = "") -> str:
|
|
label = WORK_LABELS.get(kind, "práca")
|
|
owner = owner_in_question(person, question)
|
|
return label.capitalize() + (" " + owner if owner else f" ({person})")
|
|
|
|
|
|
def fact_sentences(claim: dict, question: str = "") -> str:
|
|
"""Keep each person's academic tuple together, with explicit year semantics."""
|
|
person = claim["person"]
|
|
kind = claim.get("work_type")
|
|
title = claim.get("title")
|
|
year = claim.get("year")
|
|
displayed_title = title.rstrip(".") if title is not None else None
|
|
parts = []
|
|
if kind or title is not None or year is not None:
|
|
owner = owner_in_question(person, question)
|
|
if owner:
|
|
subject = work_subject(person, kind, question)
|
|
if title is not None:
|
|
parts.append(
|
|
f"{subject} má názov **{displayed_title}**"
|
|
+ (f" (rok práce: **{year}**)." if year is not None else ".")
|
|
)
|
|
elif year is not None:
|
|
parts.append(
|
|
f"{subject} je v podkladoch evidovaná s rokom **{year}**."
|
|
if not (kind or "").endswith("_project")
|
|
else f"{subject} je v podkladoch evidovaný s rokom **{year}**."
|
|
)
|
|
else:
|
|
parts.append(
|
|
f"V podkladoch je uvedená {subject[0].lower() + subject[1:]}."
|
|
if not (kind or "").endswith("_project")
|
|
else f"V podkladoch je uvedený {subject[0].lower() + subject[1:]}."
|
|
)
|
|
else:
|
|
sentence = f'{person} má {ACCUSATIVE.get(kind, "prácu")}'
|
|
if title is not None:
|
|
sentence += f" s názvom **{displayed_title}**"
|
|
if year is not None:
|
|
sentence += f" (rok práce: **{year}**)"
|
|
parts.append(sentence + ".")
|
|
if claim.get("start_year") is not None:
|
|
parts.append(f'{person} má uvedený začiatok štúdia v roku **{claim["start_year"]}**.')
|
|
for key, label in [
|
|
("keyword", "kľúčové slovo"),
|
|
("category", "kategória"),
|
|
("document_author", "autor dokumentu"),
|
|
]:
|
|
if claim.get(key) is not None:
|
|
if key == "keyword":
|
|
if parts and (kind or title is not None or year is not None):
|
|
parts[-1] = parts[-1].rstrip(".") + f" a tag **{claim[key]}**."
|
|
else:
|
|
parts.append(f"{person} má tag **{claim[key]}**.")
|
|
else:
|
|
parts.append(
|
|
f"{person}: "
|
|
+ ("autor dokumentu je" if key == "document_author" else "uvedená kategória je")
|
|
+ f" **{claim[key]}**."
|
|
)
|
|
return " ".join(parts) or person + "."
|
|
|
|
|
|
GOAL_NOUNS = (
|
|
("Vypracujte prehľad", "Vypracovanie prehľadu"),
|
|
("Vypracujte rešerš", "Vypracovanie rešerše"),
|
|
("Implementujte jednoduchý systém", "Implementácia jednoduchého systému"),
|
|
("Implementujte systém", "Implementácia systému"),
|
|
("Implementujte vyhľadávač", "Implementácia vyhľadávača"),
|
|
("Navrhnite a vypracujte experimenty", "Návrh a vypracovanie experimentov"),
|
|
("Navrhnite možné zlepšenia", "Návrh možných zlepšení"),
|
|
("Navrhnite zlepšenia", "Návrh zlepšení"),
|
|
("Preskúmajte metódy", "Preskúmanie metód"),
|
|
("Vyhodnoťte presnosť", "Vyhodnotenie presnosti"),
|
|
)
|
|
GOAL_VERBS = {
|
|
"vypracujte": "Vypracovať",
|
|
"implementujte": "Implementovať",
|
|
"navrhnite": "Navrhnúť",
|
|
"preskúmajte": "Preskúmať",
|
|
"vyhodnoťte": "Vyhodnotiť",
|
|
"porovnajte": "Porovnať",
|
|
"vytvorte": "Vytvoriť",
|
|
"analyzujte": "Analyzovať",
|
|
"otestujte": "Otestovať",
|
|
"overte": "Overiť",
|
|
"zistite": "Zistiť",
|
|
"naučte": "Naučiť",
|
|
"zostavte": "Zostaviť",
|
|
"využite": "Využiť",
|
|
"zlepšite": "Zlepšiť",
|
|
"použite": "Použiť",
|
|
"natrénujte": "Natrénovať",
|
|
"optimalizujte": "Optimalizovať",
|
|
"identifikujte": "Identifikovať",
|
|
"preštudujte": "Preštudovať",
|
|
"pripravte": "Pripraviť",
|
|
"vyhodnotte": "Vyhodnotiť",
|
|
"vyhodnnotte": "Vyhodnotiť",
|
|
"urobte": "Urobiť",
|
|
"naštudujte": "Naštudovať",
|
|
"pozrite": "Pozrieť",
|
|
"prečítajte": "Prečítať",
|
|
"pokračujte": "Pokračovať",
|
|
}
|
|
|
|
|
|
def goal_text(unit: str) -> tuple[str, bool]:
|
|
"""Format only known goal verbs, keeping URLs and code byte-for-byte intact."""
|
|
prefix = "_ZP_LITERAL_"
|
|
while prefix in unit:
|
|
prefix += "X"
|
|
literals = {}
|
|
|
|
def protect(value):
|
|
key = prefix + str(len(literals)) + "_"
|
|
literals[key] = value
|
|
return key
|
|
|
|
text = re.sub(r"(`+)([\s\S]*?)\1", lambda m: protect(m[0]), unit)
|
|
text = re.sub(r"<[^>]+>", lambda m: protect(m[0]), text)
|
|
for url in sorted(text_urls(text), key=len, reverse=True):
|
|
text = text.replace(url, protect(url))
|
|
result, is_goal = _goal_text(text)
|
|
for key, value in literals.items():
|
|
result = result.replace(key, value)
|
|
return result, is_goal
|
|
|
|
|
|
def _goal_text(unit: str) -> tuple[str, bool]:
|
|
"""Only bounded grammatical substitutions; unknown prose stays verbatim."""
|
|
text = re.sub(r"^\s*(?:\d+[.)]|[-+*])\s+", "", unit.strip())
|
|
for original, replacement in GOAL_NOUNS:
|
|
if re.match(re.escape(original) + r"\b", text, re.I):
|
|
tail = text[len(original) :]
|
|
# Infinitives retain the original object cases when a second
|
|
# command or coordinated object makes a noun substitution unsafe.
|
|
if re.search(
|
|
r"\b(?:" + "|".join(map(re.escape, GOAL_VERBS)) + r")\b", tail, re.I
|
|
) or re.search(
|
|
r"\ba\s+(?:spôsoby|metódy|aplikácie|systémy|experimenty|možnosti)\b", tail, re.I
|
|
):
|
|
break
|
|
result = replacement + text[len(original) :]
|
|
result = result.replace("v ktorom využijete", "v ktorom sa má využiť")
|
|
result = result.replace(", v ktorých vyhodnotíte", " s cieľom vyhodnotiť")
|
|
result = result.replace("v ktorých vyhodnotíte", "s cieľom vyhodnotiť")
|
|
return result, True
|
|
match = re.match(r"[^\W\d_]+", text)
|
|
first = match[0].casefold() if match else ""
|
|
if first in GOAL_VERBS or (
|
|
first == "na"
|
|
and text.casefold().startswith("na základe ")
|
|
and re.search(r"\b(?:" + "|".join(map(re.escape, GOAL_VERBS)) + r")\b", text, re.I)
|
|
):
|
|
|
|
def infinitive(match):
|
|
word = GOAL_VERBS[match[0].casefold()]
|
|
prefix = text[: match.start()].rstrip()
|
|
return word if not prefix or prefix[-1] in ".!?" else word.lower()
|
|
|
|
result = re.sub(
|
|
r"\b(" + "|".join(map(re.escape, GOAL_VERBS)) + r")\b", infinitive, text, flags=re.I
|
|
)
|
|
return result[0].upper() + result[1:], True
|
|
if first in {value.casefold() for value in GOAL_VERBS.values()}:
|
|
return text, True
|
|
return unit.strip(), False
|
|
|
|
|
|
def content_lines(
|
|
units: list[str], person: str | None, kind: str | None, question: str = ""
|
|
) -> list[str]:
|
|
"""Distinguish documented goals from factual descriptions or progress notes."""
|
|
# A task heading has no factual content of its own and otherwise prevents
|
|
# a whole assignment quote from being presented as individual goals.
|
|
units = [
|
|
unit
|
|
for unit in units
|
|
if normalized(unit)
|
|
not in {
|
|
"zadanie",
|
|
"zadanie diplomovej prace",
|
|
"zadanie bakalarskej prace",
|
|
"ciele prace",
|
|
"ciele na semester",
|
|
"navrh na zadanie",
|
|
"navrh zadania",
|
|
}
|
|
]
|
|
converted = [goal_text(unit) for unit in units]
|
|
subject = work_subject(person, kind, question) if person else "Práca"
|
|
goals = [text for text, is_goal in converted if is_goal]
|
|
other = [text for text, is_goal in converted if not is_goal]
|
|
if goals:
|
|
lines = [
|
|
subject + " má podľa dostupných podkladov tieto ciele:",
|
|
"",
|
|
*[f"{index}. {text}" for index, text in enumerate(goals, 1)],
|
|
]
|
|
if other:
|
|
lines.extend(["", *other])
|
|
return lines
|
|
return [
|
|
(
|
|
subject + " je v dostupných podkladoch opísaná takto:"
|
|
if not (kind or "").endswith("_project")
|
|
else subject + " je v dostupných podkladoch opísaný takto:"
|
|
),
|
|
"",
|
|
*units,
|
|
]
|
|
|
|
|
|
def no_answer_text(context: dict) -> str:
|
|
people = context.get("requested_people") or context.get("requirements", {}).get("people", [])
|
|
if len(people) == 1:
|
|
req = context.get("requirements", {})
|
|
years = context.get("requested_years") or req.get("years") or []
|
|
kinds = context.get("requested_work_types") or [req.get("work_types", {}).get(people[0])]
|
|
genitive = {
|
|
"bachelor_thesis": "bakalárskej práce",
|
|
"diploma_thesis": "diplomovej práce",
|
|
"doctoral_thesis": "dizertačnej práce",
|
|
"bachelor_project": "bakalárskeho projektu",
|
|
"diploma_project": "diplomového projektu",
|
|
"team_project": "tímového projektu",
|
|
"research_project": "výskumného projektu",
|
|
}
|
|
kind = (
|
|
genitive.get(kinds[0], "práce alebo projektu")
|
|
if len(kinds) == 1
|
|
else "práce alebo projektu"
|
|
)
|
|
if years:
|
|
date = (
|
|
" z roku **" + str(years[0]) + "**"
|
|
if len(years) == 1
|
|
else " z požadovaných rokov **" + ", ".join(map(str, years)) + "**"
|
|
)
|
|
return f"{people[0]}: v dostupných podkladoch sa nenašiel záznam {kind}{date}."
|
|
return f"{people[0]}: požadované údaje sa v dostupných podkladoch nenašli."
|
|
return "Požadované údaje sa v dostupných podkladoch nenašli."
|
|
|
|
|
|
def missing_aspects(facets: list[str], question: str = "") -> str:
|
|
"""Name only the missing requested fields, without empty helper sections."""
|
|
from tools.answers.question_intent import planned_experiments, actual_methods_requested
|
|
|
|
labels = {
|
|
"assignment": "zadaní a cieľoch",
|
|
"plans": "plánovaných úlohách",
|
|
"progress": "priebežnom stave",
|
|
"results": "výsledkoch",
|
|
"problems": "riešených problémoch",
|
|
"methods": (
|
|
"skutočne použitých metódach a technológiách"
|
|
if actual_methods_requested(question)
|
|
else "metódach a technológiách"
|
|
),
|
|
"experiments": (
|
|
"plánovaných experimentoch"
|
|
if planned_experiments(question)
|
|
else "experimentoch a ich vyhodnotení"
|
|
),
|
|
}
|
|
values = list(dict.fromkeys(labels.get(f, f) for f in facets))
|
|
if not values:
|
|
return ""
|
|
joined = values[0] if len(values) == 1 else ", ".join(values[:-1]) + " a " + values[-1]
|
|
return "V poskytnutých podkladoch chýbajú údaje o " + joined + "."
|
|
|
|
|
|
def clean_answer(lines: list[str]) -> str:
|
|
text = "\n".join(lines).strip()
|
|
return re.sub(r"\n{3,}", "\n\n", text)
|
|
|
|
|
|
def evidence_subject(person: str, context: dict) -> str:
|
|
"""Use only work identity actually present in the supplied passages."""
|
|
req = context.get("requirements", {})
|
|
passages = [p for p in context.get("content_passages", []) if p.get("person") == person]
|
|
scopes = {(p.get("work_type"), p.get("year")) for p in passages}
|
|
if not scopes:
|
|
return person
|
|
kind = req.get("work_types", {}).get(person) or (
|
|
next(iter(scopes))[0] if len(scopes) == 1 else None
|
|
)
|
|
subject = work_subject(person, kind, context.get("question", ""))
|
|
if not owner_in_question(person, context.get("question", "")):
|
|
subject = person + (" — " + WORK_LABELS[kind] if kind in WORK_LABELS else "")
|
|
if len(scopes) == 1 and next(iter(scopes))[1] is not None:
|
|
subject += " (" + str(next(iter(scopes))[1]) + ")"
|
|
labels = {p.get("work_label") for p in passages if p.get("work_label")}
|
|
if (req.get("project_number") is not None or req.get("exact_work_label")) and len(labels) == 1:
|
|
subject = person + " — " + " ".join(next(iter(labels)).split())
|
|
return subject
|
|
|
|
|
|
def comparison_table(headers: list[str], rows: list[tuple[str, list[str]]]) -> list[str] | None:
|
|
"""Lay out short literal evidence; keep complex text outside Markdown tables."""
|
|
cells = [*headers, *(cell for label, values in rows for cell in [label, *values])]
|
|
if len(headers) > 4 or any(
|
|
len(cell) > 900
|
|
or "|" in cell
|
|
or "`" in cell
|
|
or re.search(r"<[^>]+>", cell)
|
|
or text_urls(cell)
|
|
for cell in cells
|
|
):
|
|
return None
|
|
|
|
def row(values):
|
|
return "| " + " | ".join(v.replace("\n", "<br>") for v in values) + " |"
|
|
|
|
return [
|
|
row(headers),
|
|
row(["---"] * len(headers)),
|
|
*(row([label, *values]) for label, values in rows),
|
|
]
|