396 lines
22 KiB
Python
396 lines
22 KiB
Python
"""Question -> CrzpQuery: who, in which role, which work, topic, year, type, school.
|
||
|
||
CRZP full-text search joins words with AND and does not inflect, so names and
|
||
topics must be sent in their base form ("Jána Ptáka" -> "Ján Pták",
|
||
"chatbotoch" -> "chatbot"). The configured model proposes the base forms; every
|
||
proposed value must still be present in the question (same word stem), so the
|
||
model cannot add a person, topic or year. Without the model, rules and the
|
||
Slovak lemmatizer extract the same fields.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from typing import Literal
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
from tools.crzp.answering.terms import alternatives
|
||
from tools.crzp.answering.units import UNITS, unit_in_question
|
||
from tools.crzp.source.parse import fold
|
||
|
||
Role = Literal["author", "supervisor", "opponent", "any"]
|
||
Intent = Literal["list", "count", "about", "keywords", "relation", "similar", "schools", "refused"]
|
||
|
||
SCHOOLS = {
|
||
# abbreviation -> (name sent to search, distinctive part of the CRZP school name)
|
||
"tuke": ("Technická univerzita v Košiciach", "technicka univerzita v kosiciach"),
|
||
"stu": ("Slovenská technická univerzita v Bratislave", "slovenska technicka univerzita"),
|
||
"uniba": ("Univerzita Komenského v Bratislave", "univerzita komenskeho"),
|
||
"uk": ("Univerzita Komenského v Bratislave", "univerzita komenskeho"),
|
||
"upjs": ("Univerzita Pavla Jozefa Šafárika v Košiciach", "univerzita pavla jozefa safarika"),
|
||
"zu": ("Žilinská univerzita v Žiline", "zilinska univerzita"),
|
||
"uniza": ("Žilinská univerzita v Žiline", "zilinska univerzita"),
|
||
"umb": ("Univerzita Mateja Bela v Banskej Bystrici", "univerzita mateja bela"),
|
||
"ukf": ("Univerzita Konštantína Filozofa v Nitre", "univerzita konstantina filozofa"),
|
||
"spu": ("Slovenská poľnohospodárska univerzita v Nitre", "slovenska polnohospodarska univerzita"),
|
||
"euba": ("Ekonomická univerzita v Bratislave", "ekonomicka univerzita v bratislave"),
|
||
"unipo": ("Prešovská univerzita v Prešove", "presovska univerzita"),
|
||
"tnuad": ("Trenčianska univerzita Alexandra Dubčeka v Trenčíne", "trencianska univerzita"),
|
||
"tuzvo": ("Technická univerzita vo Zvolene", "technicka univerzita vo zvolene"),
|
||
"ucm": ("Univerzita sv. Cyrila a Metoda v Trnave", "univerzita sv. cyrila a metoda"),
|
||
"truni": ("Trnavská univerzita v Trnave", "trnavska univerzita"),
|
||
"ku": ("Katolícka univerzita v Ružomberku", "katolicka univerzita"),
|
||
"uvlf": ("Univerzita veterinárneho lekárstva a farmácie v Košiciach", "univerzita veterinarneho lekarstva"),
|
||
"vsmu": ("Vysoká škola múzických umení v Bratislave", "vysoka skola muzickych umeni"),
|
||
}
|
||
WORK_CUES = {
|
||
"bachelor_thesis": r"bakal[aá]r|\bbachelor", "diploma_thesis": r"diplom|\bmaster",
|
||
"dissertation": r"dizerta|doktorandsk|\bphd\b|dissertation", "habilitation_thesis": r"habilita",
|
||
"doctoral_thesis": r"rigor[oó]z",
|
||
}
|
||
# Words that name the kind of work, never a topic ("dizertačné práce", "bachelor thesis").
|
||
WORK_WORDS = {"praca", "prace", "prac", "bakalarska", "bakalarske", "bakalarsku", "bakalarskej", "bakalarka", "bakalarke",
|
||
"diplomova", "diplomove", "diplomovu", "diplomovej", "diplomovka", "dizertacna", "dizertacne",
|
||
"habilitacna", "rigorozna", "zaverecna", "zaverecne", "thesis", "theses", "bachelor", "master",
|
||
"dissertation", "work", "projekt"}
|
||
# Rule overrides and bulk dumps are refused, never searched.
|
||
OUT_OF_SCOPE = re.compile(
|
||
r"\b(?:ignoruj|zabudni|obid|prepis|zrus)\w*\b[^?.!]*\b(?:pravidl|pokyn|instrukci|obmedzen|prompt)\w*"
|
||
r"|\b(?:vypis|exportuj|stiahni|ukaz|daj|dump)\w*\b[^?.!]*\b(?:vsetk|cel|kompletn)\w*\s+(?:\w+\s+)?"
|
||
r"(?:zaznam|databaz|register|crzp|prac)\w*"
|
||
r"|\bsystem\w* prompt|\bhesl\w*|\bapi kluc|\.env\b")
|
||
ROLE_CUES = {
|
||
"supervisor": r"\b(?:ved[uú]c|viedol|viedla|vedie|veden|pod veden|[sš]kolite[lľ])",
|
||
"opponent": r"\b(?:oponent|oponoval|oponovala|oponuje|oponentúr|posudzoval)",
|
||
"author": r"\b(?:autor|nap[ií]sal|vypracoval|[sš]tudent|pracoval|robil)",
|
||
}
|
||
STOP = set("""a aj ako ale alebo ani bez bol bola boli by co čo čom do ešte ho i ich je jej jeho k kde kedy kto ktorý ktorá ktoré
|
||
ktorí ktorých ku ma má mal mala mali na nad naj nájdi nie o od on ona po pod podľa pre pri práca práce prácu prácach prác
|
||
práci prácou s sa si so svoje svojej svojom ta tak tam téma témy temu to tom tu u v vo vy z za zo že aký aká aké akú akých
|
||
akej akom koľko počet daj ukáž vypíš zoznam všetky všetkých crzp registri register slovensku slovensko rok roku roky rokoch
|
||
bakalárska bakalárskej bakalársku bakalárske diplomová diplomovej diplomovú diplomové dizertačná dizertačnej habilitačná
|
||
vedúci vedúceho vedúcim viedol viedla vedie oponent oponenta oponentom oponoval oponovala školiteľ školiteľa študent
|
||
študenti študentov autor autora autorom práce kľúčové slová tagy tag písal napísal pracoval pracovala robil robila
|
||
zaoberal zaoberala zaoberá týka týkala týkali súvisí podobné súvisiace najčastejšie často kým kom
|
||
bolo boli bude budú mali majú mal mala existuje existovali koľkokrát ktorými ktorou ktorej
|
||
škola školy škôl škole školách univerzita univerzity univerzít fakulta fakulty fakúlt študoval študovala študovali
|
||
tí tie tých tieto tejto tento nich toho prvá prvej prvú prvý druhá druhej druhú druhý tretia tretej tretiu
|
||
posledná poslednej poslednú čo tak
|
||
zaoberali venoval venovala venovali venuje venujú písali robili skúmal skúmala skúmali riešil riešila riešili
|
||
""".split())
|
||
STOP_FOLDED = {fold(w) for w in STOP}
|
||
|
||
|
||
class Person(BaseModel):
|
||
name: str = Field(min_length=2, max_length=80)
|
||
role: Role = "any"
|
||
typed: str | None = None # the name as written in the question (decides whether diacritics matter)
|
||
|
||
|
||
class CrzpQuery(BaseModel):
|
||
intent: Intent = "list"
|
||
people: list[Person] = Field(default_factory=list, max_length=2)
|
||
title: str | None = Field(default=None, max_length=200)
|
||
topics: list[str] = Field(default_factory=list, max_length=4)
|
||
surface: list[str] = Field(default_factory=list, max_length=2) # topic words as written (rules)
|
||
phrases: list[str] = Field(default_factory=list, max_length=3) # rules: the topic as CRZP likely writes it
|
||
years: list[int] = Field(default_factory=list, max_length=6)
|
||
work_types: list[str] = Field(default_factory=list, max_length=5)
|
||
school: str | None = None # key of SCHOOLS
|
||
unit: str | None = None # key of units.UNITS (verified workplace codes)
|
||
alternatives: list[str] = Field(default_factory=list, max_length=2) # other search terms of topics[0]
|
||
target_role: Role | None = None # relation: which role to aggregate
|
||
from_model: bool = False # base forms proposed by the model (else rules)
|
||
relaxed_author: bool = False # asked as author, found only in another role
|
||
sids: list[str] = Field(default_factory=list, max_length=30) # follow-up: only these CRZP records
|
||
school_name: str | None = None # follow-up: the chosen namesake's university (answer.university)
|
||
searched: list[str] = Field(default_factory=list) # texts actually sent to CRZP (set by answer.find)
|
||
unverified_only: list[str] = Field(default_factory=list) # found only under unverified proposed terms
|
||
|
||
def topic_key(self) -> str:
|
||
"""The topic the alternative terms belong to (the rule phrase without the model)."""
|
||
return (self.phrases or self.topics or [""])[0]
|
||
|
||
def search_text(self) -> str:
|
||
if self.people:
|
||
return " ".join(p.name for p in self.people[:1]) + (" " + self.title if self.title else "")
|
||
if self.title:
|
||
return self.title
|
||
if self.topics:
|
||
return self.topics[0]
|
||
if self.school:
|
||
return SCHOOLS[self.school][0]
|
||
return ""
|
||
|
||
|
||
def stem_match(word: str, other: str, *, diacritics: bool = False) -> bool:
|
||
"""Same Slovak word up to inflection: shared prefix of len-2 (at least 3 letters).
|
||
With diacritics=True, "Jána" matches "Ján" but not "Jan"."""
|
||
a, b = (word.lower(), other.lower()) if diacritics else (fold(word), fold(other))
|
||
if a == b:
|
||
return True
|
||
n = max(3, min(len(a), len(b)) - 2)
|
||
return len(a) >= 3 and len(b) >= 3 and a[:n] == b[:n]
|
||
|
||
|
||
def _words(text: str) -> list[str]:
|
||
return re.findall(r"[^\W\d_]+", text)
|
||
|
||
|
||
def in_question(value: str, question: str) -> bool:
|
||
words = _words(question)
|
||
return all(any(stem_match(v, w) for w in words) for v in _words(value)) and bool(_words(value))
|
||
|
||
|
||
YEAR_RANGE = re.compile(r"\b(?:od\s+|medzi\s+)?((?:19|20)\d{2})\s*(?:až|az|-|–|do|a)\s*((?:19|20)\d{2})\b")
|
||
|
||
|
||
def years_in(question: str) -> list[int]:
|
||
"""Years; "2022 až 2024", "od 2022 do 2024", "medzi 2022 a 2024" and "2022-2024" are ranges,
|
||
"2022 a 2024" (without "medzi") are the two years."""
|
||
years = {int(y) for y in re.findall(r"\b(?:19|20)\d{2}\b", question)}
|
||
for match in YEAR_RANGE.finditer(question.lower()):
|
||
first, last = int(match.group(1)), int(match.group(2))
|
||
joiner = match.group(0)
|
||
is_range = not re.search(r"\sa\s", joiner) or joiner.startswith("medzi")
|
||
if is_range and 0 < last - first <= 30:
|
||
years |= set(range(first, last + 1))
|
||
return sorted(years)
|
||
|
||
|
||
# "z akých škôl boli", "ktoré univerzity", "kde študovali" (not "práce na Žilinskej univerzite").
|
||
SCHOOLS_CUE = re.compile(r"\b(?:z|zo|na) (?:akych|akej|akom|ktorych|ktorej|ktorom) (?:skol|univerzit|fakult)\w*"
|
||
r"|\b(?:ktor|ak)\w* (?:skol|univerzit|fakult)\w*"
|
||
r"|\bkde (?:studoval|studovali|studuj)\w*|\b(?:which|what) (?:schools?|universit)")
|
||
|
||
|
||
def asks_for_works(low: str) -> bool:
|
||
""""Ktoré práce sa zaoberali X?" asks for a list, not for a summary."""
|
||
return bool(re.search(r"^\W*(?:ktor|ak|vypis|ukaz|daj|najdi|nájdi)\w*\s+(?:\w+\s+){0,2}pr[aá]c", low))
|
||
|
||
|
||
def _intent(low: str) -> Intent:
|
||
if OUT_OF_SCOPE.search(fold(low)):
|
||
return "refused"
|
||
if re.search(r"\bkoľk\w*|\bkolk\w*|\bpočet\b|\bpocet\b|\bhow many\b", low):
|
||
return "count"
|
||
if re.search(r"kľúčov|klucov|\btag|keyword", low):
|
||
return "keywords"
|
||
if re.search(r"podobn|súvisiac|suvisiac|príbuzn|similar|related", low):
|
||
return "similar"
|
||
if not asks_for_works(low) and re.search(r"o čom|o com|čo robil|co robil|na čom|na com|čím sa zaobera|cim sa zaobera|zaoberal|zhrň|zhrn|"
|
||
r"abstrakt|abstract|cieľ|ciel|čo skúmal|co skumal|obsah|what did .* work|what is .* about|summar", low):
|
||
return "about"
|
||
if SCHOOLS_CUE.search(fold(low)):
|
||
return "schools"
|
||
if re.search(r"najčastejš|najcastejs|\bs kým\b|\bs kym\b|ktor[íi] oponent|kto (?:mu |jej )?oponoval|kto bol|"
|
||
r"who (?:was|were) the", low):
|
||
return "relation"
|
||
return "list"
|
||
|
||
|
||
def _rules(question: str) -> CrzpQuery:
|
||
low = question.lower()
|
||
years = years_in(question)
|
||
work_types = [t for t, cue in WORK_CUES.items() if re.search(cue, low)]
|
||
school = next((key for key in SCHOOLS if re.search(r"\b" + key + r"\b", fold(question))), None)
|
||
if school is None:
|
||
school = next((key for key, (_, part) in SCHOOLS.items() if part in fold(question)), None)
|
||
intent = _intent(low)
|
||
quoted = re.search(r"[\"„“']([^\"„“']{4,200})[\"“”']", question)
|
||
unit = unit_in_question(question)
|
||
if unit:
|
||
school = UNITS[unit]["school"]
|
||
query = CrzpQuery(intent=intent, years=years, work_types=work_types, school=school, unit=unit,
|
||
title=quoted.group(1).strip() if quoted else None)
|
||
asked = re.search(r"\bkto (?:bol|bola|je|boli|sú|su)\b[^?]*?\b(ved[uú]c|[sš]kolite|oponent|konzultant)", low)
|
||
if asked and not query.target_role:
|
||
query.target_role = {"v": "supervisor", "š": "supervisor", "s": "supervisor", "o": "opponent"}.get(
|
||
asked.group(1)[0], None)
|
||
return query
|
||
|
||
|
||
def _role_near(question: str, name: str, asked: Role | None = None) -> Role:
|
||
"""Role cue nearest before (or right after) the name; the asked role
|
||
("Kto bol vedúci práce X?") belongs to somebody else, X is the author."""
|
||
if asked:
|
||
return "author"
|
||
low, first = question.lower(), fold(name).split()[0][:3]
|
||
position = next((m.start() for m in re.finditer(r"[^\W\d_]+", low) if fold(m.group())[:3] == first), -1)
|
||
best, distance = "any", 10**6
|
||
for role, cue in ROLE_CUES.items():
|
||
for match in re.finditer(cue, low):
|
||
d = position - match.start() if match.start() <= position else (match.start() - position) * 2
|
||
if d < distance:
|
||
best, distance = role, d
|
||
return best
|
||
|
||
|
||
def _rule_people_topics(question: str, query: CrzpQuery) -> None:
|
||
from tools.retrieval import lemmatize
|
||
unquoted = question.replace(query.title, " ") if query.title else question
|
||
tokens = re.findall(r"[^\W\d_]+", unquoted)
|
||
names, current = [], []
|
||
for i, token in enumerate(tokens):
|
||
is_name = (token[:1].isupper() and not token.isupper() # "LLM", "IoT"-like acronyms are topics
|
||
and fold(token) not in STOP_FOLDED and (i > 0 or len(tokens) == 1))
|
||
if is_name and fold(token) not in SCHOOLS and fold(token) not in UNITS:
|
||
current.append(token)
|
||
elif current:
|
||
names.append(current); current = []
|
||
if current:
|
||
names.append(current)
|
||
for parts in names[:2]:
|
||
query.people.append(Person(name=" ".join(parts), role=_role_near(question, parts[0], query.target_role),
|
||
typed=" ".join(parts)))
|
||
named = {fold(t) for parts in names for t in parts}
|
||
topics, surface, runs, previous = [], [], [], None
|
||
# Topic words may contain digits ("5G", "ns-3"); plain numbers (years) are not topics.
|
||
for position, token in enumerate(t for t in re.findall(r"[^\W_][\w\-]*", unquoted) if not t.isdigit()):
|
||
if (fold(token) in STOP_FOLDED or fold(token) in named or fold(token) in SCHOOLS or fold(token) in UNITS
|
||
or len(token) < 2 or len(token) == 2 and not re.search(r"\d", token)):
|
||
continue
|
||
lemma = (lemmatize.lemmas(token) or [token])[0].lower()
|
||
if (fold(lemma) not in STOP_FOLDED and fold(lemma) not in WORK_WORDS
|
||
and not any(re.search(cue, token.lower()) for cue in WORK_CUES.values())):
|
||
topics.append(lemma)
|
||
surface.append(token)
|
||
if previous is not None and position == previous + 1 and runs:
|
||
runs[-1].append((token, lemma)) # adjacent words form one phrase ("rozpoznávaním reči")
|
||
else:
|
||
runs.append([(token, lemma)])
|
||
previous = position
|
||
query.topics = list(dict.fromkeys(topics))[:4]
|
||
query.surface = [" ".join(surface[:2])] if surface else []
|
||
query.phrases = topic_phrases(runs[0])[:3] if runs else []
|
||
|
||
|
||
PLURAL_NOUN = [("iach", "ie"), ("ach", "e"), ("och", "y"), ("ami", "e"), ("mi", "y"), ("ov", "y"), ("í", "e")]
|
||
|
||
|
||
def _adjective(lemma: str) -> bool:
|
||
return len(fold(lemma)) > 3 and fold(lemma).endswith("y") # adjective lemmas end in -ý
|
||
|
||
|
||
def _gender(lemma: str) -> str:
|
||
folded = fold(lemma)
|
||
if folded.endswith(("a", "ost")) or lemma.endswith(("ť", "ď", "ň", "ľ", "c")):
|
||
return "f"
|
||
return "n" if folded.endswith(("o", "ie", "e", "um")) else "m"
|
||
|
||
|
||
def topic_phrases(run: list[tuple[str, str]]) -> list[str]:
|
||
"""How CRZP titles likely write an inflected topic, without the model. Words before
|
||
the first noun agree with it, the noun takes its base form and the words after it
|
||
(genitive modifiers) stay as written: "rozpoznávaním reči" -> "rozpoznavanie reči",
|
||
"detekcii toxického jazyka" -> "detekcia toxického jazyka"; plural adjectives also
|
||
give the plural phrase: "neurónových sieťach" -> "neuronové siete"."""
|
||
adjectives = []
|
||
for token, lemma in run:
|
||
if not _adjective(lemma):
|
||
break
|
||
adjectives.append((token, lemma))
|
||
if len(adjectives) == len(run):
|
||
return []
|
||
head_token, head = run[len(adjectives)]
|
||
tail = [token for token, _ in run[len(adjectives) + 1:]]
|
||
ending = {"m": "ý", "f": "á", "n": "é"}[_gender(head)]
|
||
phrases = [" ".join([lemma[:-1] + ending for _, lemma in adjectives] + [head] + tail)]
|
||
if adjectives and all(re.search(r"(?:ych|ymi|ym)$", fold(t)) for t, _ in adjectives):
|
||
plural = next((head_token[: -len(e)] + r for e, r in PLURAL_NOUN if head_token.lower().endswith(e)), None)
|
||
if plural:
|
||
plural = re.sub(r"([ťďňľ])([ei])", lambda m: "tdnl"["ťďňľ".index(m.group(1))] + m.group(2), plural) # sieťe -> siete
|
||
phrases.append(" ".join([lemma[:-1] + "é" for _, lemma in adjectives] + [plural.lower()] + tail))
|
||
return list(dict.fromkeys(p for p in phrases if len(p.split()) > 1 or p != head))
|
||
|
||
|
||
PROMPT = """Z otázky o záverečných prácach v CRZP vytiahni parametre vyhľadávania. Otázka môže byť aj v inom
|
||
jazyku; mená a témy uveď tak, ako sa píšu v slovenských záznamoch. Odpovedz iba JSON:
|
||
{"intent":"list|count|about|keywords|relation|similar|schools (about = o čom je práca / čo v nej robil; schools = z akých škôl)",
|
||
"people":[{"name":"meno v 1. páde, napr. Ján Pták","role":"author|supervisor|opponent|any"}],
|
||
"title":"presný názov práce, ak je v otázke, inak null",
|
||
"topics":["téma alebo pojem v základnom tvare, najdôležitejší prvý"],
|
||
"alternatives":["0 až 2 ustálené odborné výrazy, ktorými sa PRVÁ téma naozaj píše v názvoch a kľúčových slovách prác: rozpísaná skratka, bežné slovenské synonymum alebo anglický termín (napr. LLM -> jazykový model, GPT; blockchain -> distributed ledger). Nevymýšľaj doslovné preklady; ak si nie si istý, daj []"],
|
||
"target_role":"rola, na ktorú sa pýta pri vzťahu (napr. kto oponoval prácam vedeným X -> opponent), inak null"}
|
||
Pravidlá: meno uveď len ak je v otázke; vedúci/školiteľ/viedol = supervisor; oponent/oponoval = opponent;
|
||
študent/autor/napísal/jeho práca = author.
|
||
"Kto bol vedúci práce X" -> X je author a target_role je supervisor. Témy len z otázky, bez slov ako práca, téma, študent, škola, rok.
|
||
Názov práce nie je meno osoby. Nepridávaj nič, čo v otázke nie je."""
|
||
|
||
|
||
def _model_fields(question: str, complete=None) -> dict | None:
|
||
from tools.answers.model_client import complete_with_configured_model
|
||
try:
|
||
response = (complete or complete_with_configured_model)(
|
||
[{"role": "system", "content": PROMPT}, {"role": "user", "content": question}], max_tokens=300)
|
||
text = response["choices"][0]["message"]["content"].strip()
|
||
match = re.search(r"\{[\s\S]*\}", text)
|
||
return json.loads(match.group(0)) if match else None
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def understand(question: str, *, complete=None, use_model: bool = True) -> CrzpQuery:
|
||
query = _rules(question)
|
||
if query.intent == "refused":
|
||
return query # refused before any model call or search
|
||
proposed = _model_fields(question, complete) if use_model else None
|
||
if isinstance(proposed, dict):
|
||
for person in proposed.get("people") or []:
|
||
if not isinstance(person, dict) or not isinstance(person.get("name"), str):
|
||
continue
|
||
name = " ".join(person["name"].split())
|
||
title = proposed.get("title") if isinstance(proposed.get("title"), str) else query.title or ""
|
||
if (not 2 <= len(name) <= 80 or len(name.split()) > 4 or not in_question(name, question)
|
||
or (title and fold(name) in fold(title))):
|
||
continue # a work title or an invented name is not a person
|
||
role = person.get("role") if person.get("role") in ("author", "supervisor", "opponent", "any") else "any"
|
||
query.people.append(Person(name=name, role=role, typed=typed_form(name, question)))
|
||
query.people = query.people[:2]
|
||
query.topics = [t.strip() for t in proposed.get("topics") or []
|
||
if isinstance(t, str) and t.strip() and in_question(t, question) and not work_words_only(t)
|
||
and not names_a_school(t) and not any(in_question(t, p.name) for p in query.people)][:4]
|
||
if (query.intent == "list" and proposed.get("intent") in ("count", "about", "keywords", "relation", "similar", "schools")
|
||
and not (proposed["intent"] == "about" and asks_for_works(question.lower()))):
|
||
query.intent = proposed["intent"] # rules found no cue (e.g. another language)
|
||
title = proposed.get("title")
|
||
if not query.title and isinstance(title, str) and len(title) >= 4 and fold(title) in fold(question):
|
||
query.title = title.strip()
|
||
if proposed.get("target_role") in ("author", "supervisor", "opponent"):
|
||
query.target_role = proposed["target_role"]
|
||
query.from_model = True
|
||
offered = proposed.get("alternatives") if isinstance(proposed.get("alternatives"), list) else []
|
||
else:
|
||
_rule_people_topics(question, query)
|
||
offered = []
|
||
if query.topics and not query.people and not query.title:
|
||
query.alternatives = alternatives(query.topic_key(), offered)
|
||
if query.target_role is None and query.intent == "relation":
|
||
asked = [r for r, cue in ROLE_CUES.items() if re.search(cue, question.lower())
|
||
and all(p.role != r for p in query.people)]
|
||
query.target_role = asked[0] if asked else None
|
||
if query.people and query.target_role == query.people[0].role:
|
||
query.target_role = None # "Koľkokrát oponoval X" asks about X's own role, not about another person
|
||
if query.intent == "relation" and query.target_role is None:
|
||
query.intent = "list"
|
||
return query
|
||
|
||
|
||
def names_a_school(term: str) -> bool:
|
||
""""KEMT", "TUKE", "FEI" or a school name is a filter, not a topic to search."""
|
||
folded = " ".join(fold(term).split())
|
||
return (folded in SCHOOLS or folded in UNITS or unit_in_question(term) is not None
|
||
or any(part in folded or folded in part for _, part in SCHOOLS.values() if len(folded) > 5))
|
||
|
||
|
||
def work_words_only(term: str) -> bool:
|
||
words = [fold(w) for w in _words(term)]
|
||
return bool(words) and all(w in WORK_WORDS or w in STOP_FOLDED for w in words)
|
||
|
||
|
||
def typed_form(name: str, question: str) -> str:
|
||
"""The question's own spelling of the name words ("jan ptak" stays without diacritics)."""
|
||
words = _words(question)
|
||
return " ".join(next((w for w in words if stem_match(part, w)), part) for part in _words(name))
|