dp-zp-agent/tools/answer_evaluation/presentation.py
2026-11-03 03:45:13 +01:00

173 lines
8.8 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 .context_policy import normalized, person_token_pattern, text_urls
from .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:
return question[found[0].start():found[-1].end()]
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}**.')
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 owner and title is not None and year is not None:
parts.append(f'Rok práce je **{year}**.')
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:
parts.append(f'Pri osobe {person} je uvedené: {label} **{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ť',
}
def goal_text(unit: str) -> tuple[str, bool]:
"""Only bounded grammatical substitutions; unknown prose stays verbatim."""
if text_urls(unit) or '`' in unit:
# A word inside a URL/code literal must never be conjugated. Preserve
# technical excerpts exactly rather than changing their references.
return unit.strip(), False
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(['', 'Ďalšie podklady:', *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:
return (f'Pre osobu {people[0]} sa v dostupných podkladoch nepodarilo spoľahlivo nájsť '
'údaje zodpovedajúce tejto otázke.')
return 'K tejto otázke sa v dostupných podkladoch nepodarilo spoľahlivo nájsť zodpovedajúce údaje.'