"""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", "
") for v in values) + " |" return [ row(headers), row(["---"] * len(headers)), *(row([label, *values]) for label, values in rows), ]