"""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}**'+ (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ť', } 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(['', *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 .question_intent import planned_experiments 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':'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 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)]