199 lines
8.5 KiB
Python
199 lines
8.5 KiB
Python
"""Explicit answer aspects; these select evidence, never supply facts."""
|
|
|
|
import re
|
|
from tools.answers.context_policy import normalized
|
|
|
|
FACET_PATTERNS = {
|
|
"assignment": r"\b(?:zadani\w*|ciel\w*)\b|\bco\s+(?:ma|maju|mal[aoiy]?)\b[^?;!.]*\b(?:vytvorit|vypracovat|implementovat|navrhnut|zostavit)\b",
|
|
"plans": r"\b(?:planovan\w*|dalsi\w*\s+uloh\w*|uloh\w*|co\s+(?:ma|maju|mal[aoiy]?)\s+(?:urobit|spravit)(?:\s+dalej)?)\b",
|
|
"problems": r"\b(?:problem\w*|tazkost\w*|prekazk\w*|chyb\w*)\b",
|
|
"methods": r"\b(?:metod\w*|technolog\w*|nastroj\w*|pouziv\w*|pouzil\w*|pristup\w*|frontend\w*|backend\w*)\b",
|
|
"experiments": r"\b(?:experiment\w*|merani\w*|testovani\w*|vyhodnocov\w*|vyhodnot\w*)\b",
|
|
"progress": r"\b(?:priebezn\w*\s+)?stav\w*\b",
|
|
"results": r"\b(?:vysledk\w*|dosiahn\w*|funkcnost\w*)\b",
|
|
}
|
|
FACET_LABELS = {
|
|
"assignment": "Zadanie a ciele",
|
|
"plans": "Plánované úlohy",
|
|
"problems": "Riešené problémy",
|
|
"methods": "Metódy a technológie",
|
|
"experiments": "Experimenty a vyhodnotenie",
|
|
"progress": "Priebežný stav",
|
|
"results": "Zaznamenané výsledky",
|
|
}
|
|
|
|
|
|
def relative_years(question: str, today=None) -> str:
|
|
"""Replace the user's explicit relative year with the server's calendar year.
|
|
|
|
This is the user's own time expression ("tento rok", "minulý rok"), not a
|
|
guess of a missing date year in source data.
|
|
"""
|
|
from datetime import date
|
|
year = (today or date.today()).year
|
|
text = re.sub(r'\b(?:v\s+)?(?:tento|tohto|tomto|tohoto)\s+rok(?:u)?\b|\btohtoročn\w*',
|
|
f'v roku {year}', question, flags=re.I)
|
|
return re.sub(r'\b(?:v\s+)?(?:minul[ýy]|minul[ée]ho|minulom)\s+rok(?:u)?\b|\bvlani\b',
|
|
f'v roku {year - 1}', text, flags=re.I)
|
|
|
|
|
|
def normalize_question(question: str) -> str:
|
|
"""Only a narrow phrase typo; identities, dates and conditions stay literal."""
|
|
text=re.sub(r'^ma\s+(?:čom|com)\b','na čom',relative_years(question.strip()),flags=re.I)
|
|
text=re.sub(r'(?<!\d)(\d{1,2})\.\s*(\d{1,2})\.\s*([12]\d{3})(?!\d)',
|
|
lambda m:f'{m[1]}.{m[2]}.{m[3]}',text)
|
|
return re.sub(r'((?:stretnut[iíe]|dňa|dna)\s+)([12]\d{3})-(\d{2})-(\d{2})\b',
|
|
lambda m:f'{m[1]}{int(m[4])}.{int(m[3])}.{m[2]}',text,flags=re.I)
|
|
|
|
|
|
def requested_facets(question: str) -> list[str]:
|
|
text = normalized(re.sub(r'[„“"«][^„“”"»]+[“”"»]', " ", question))
|
|
text = re.sub(r'\bnezamienaj\s+(?:ich|to|ciele|zadanie|plany|ulohy)\s+za\s+(?:hotove\s+)?vysled\w*|\bbez domyslania vysled\w*',' ',text)
|
|
matches = [
|
|
(m.start(), key)
|
|
for key, pattern in FACET_PATTERNS.items()
|
|
if (m := re.search(pattern, text))
|
|
]
|
|
return [key for _, key in sorted(matches)]
|
|
|
|
|
|
def content_question(question: str) -> bool:
|
|
text = normalized(question)
|
|
return bool(
|
|
requested_facets(question)
|
|
or re.search(
|
|
r"\b(?:comu|o com|obsah\w*|opis\w*|popis\w*|zamer\w*|zhrn\w*|implement\w*)\b", text
|
|
)
|
|
)
|
|
|
|
|
|
def discovery_question(question: str) -> bool:
|
|
"""An unknown student is the requested result, not a missing input name.
|
|
|
|
Identify the requested subject rather than listing every activity verb.
|
|
Explicit property/list questions retain the metadata route; named roles and
|
|
academic lifecycle intents take precedence in the shared planner.
|
|
"""
|
|
text = normalized(question)
|
|
asks_people = bool(re.search(
|
|
r'\bkto\b|\b(?:ktor|ak)[iy]\w*\s+(?:zo\s+|z\s+)?student\w*\b|\bu koho\b'
|
|
r'|\b(?:vypis|vymenuj|uved|daj|najdi|zisti|ukaz)\b[^?.;!]*\bstudent\w*'
|
|
r'|\bmena\s+student\w*\b|\bstudent\w*,?\s+(?:ktor\w*|co)\b'
|
|
r'|\b(?:ktore|ake)\s+(?:prace|projekty)\b[^?.;!]*\b(?:venoval\w*|venuj\w*|zaober\w*|tyk\w*|riesil\w*)\b',
|
|
text))
|
|
if not asks_people:
|
|
return False
|
|
# "Aké témy sa riešili v roku 2024 a akí študenti?" lists works by period.
|
|
if re.search(r'\b(?:ake|ktore)\s+tem\w*\b', text):
|
|
return False
|
|
metadata = bool(re.search(r'\b(?:tag\w*|klucov\w*|kategori\w*|autor\w*|rok\w*|nazov\w*|pocet|kolko)\b|'
|
|
r'\b(?:ma|maju|mal|mali)\b[^?.;]*\b(?:prac\w*|projekt\w*)\b', text))
|
|
activity = content_question(question) or bool(re.search(
|
|
r'\b(?:venov\w*|zaober\w*|pracov\w*|robil\w*|vyskus\w*|vytvor\w*|pouz\w*|ries\w*|implement\w*)\b', text))
|
|
return activity or not metadata
|
|
|
|
|
|
TOPIC_VERBS = (
|
|
r'\b(?:pracoval\w*|robil\w*|venoval\w*|venuje|venuju|zaoberal\w*|zaobera\w*|riesil\w*|'
|
|
r'vytvoril\w*|vytvaral\w*|pouzival\w*|pouzil\w*|skusal\w*|vyskusal\w*|implementoval\w*|'
|
|
r'programoval\w*|trenoval\w*|dotrenoval\w*|testoval\w*|skumal\w*|experimentoval\w*|'
|
|
r'navrhol\w*|spracoval\w*|zameral\w*)\b'
|
|
)
|
|
TOPIC_STOP = set(
|
|
"a aj ale ako aky aka ake aku akom akej akych ci co com coho cim nieco niecim nejaky nejaku nejake "
|
|
"s so na v vo o z zo za pre pri do od k ku po sa si je su bol bola bolo boli ma mal mala mali "
|
|
"tom tej teme temu tema temy temou temach oblast oblasti praca prace praci pracu pracou projekt projekty "
|
|
"projektu projekte projektom student studenti studenta tento tato toto rok roku rokoch ktory ktora "
|
|
"ktore ktoru ktorom ktorej svojej svoju svoj svojom jeho jej uz este vobec niekedy tiez".split()
|
|
)
|
|
|
|
|
|
def person_topic(question: str, names) -> bool:
|
|
"""A named person plus a concrete topic: "Pracoval X na téme Y?", "Aký projekt s Y robil X?".
|
|
|
|
Only the topic words select evidence; the person filter stays mandatory.
|
|
"""
|
|
text = normalized(question)
|
|
if not (re.search(TOPIC_VERBS, text) or re.search(r'\b(?:projekt\w*|prac\w*)\s+(?:s|so|o|na|z)\s+\w', text)):
|
|
return False
|
|
for name in names:
|
|
for part in normalized(name).split():
|
|
text = re.sub(r'\b' + re.escape(part) + r'\w*\b', ' ', text)
|
|
text = re.sub(TOPIC_VERBS, ' ', text)
|
|
text = re.sub(r'\b\d+\b', ' ', text)
|
|
return any(len(word) >= 2 and word not in TOPIC_STOP for word in text.split())
|
|
|
|
|
|
def discovery_topic(question: str) -> str:
|
|
"""Seed retrieval with the activity; keep subsequent requirements for selection.
|
|
|
|
This is a retrieval query only. The plan, constraints, tool query and final
|
|
fulfilment guard retain the complete original question.
|
|
"""
|
|
return question.split('?',1)[0].strip() or question
|
|
|
|
|
|
def facet_mode(question: str) -> bool:
|
|
facets = requested_facets(question)
|
|
return any(
|
|
facet in facets
|
|
for facet in ["plans", "progress", "problems", "methods", "experiments", "results"]
|
|
)
|
|
|
|
|
|
def actual_methods_requested(question: str) -> bool:
|
|
return bool(
|
|
re.search(
|
|
r"\b(?:skutocne|pouziva\w*|pouzil\w*|vyuzival\w*|implementoval\w*|vyskus(?:al|ala|ali)\w*)\b|"
|
|
r"\bake technolog\w* ma\b|"
|
|
r"\b(?:pouzit\w*|aplikovan\w*|vyuzit\w*)\s+(?:metod\w*|technolog\w*|nastroj\w*)\b",
|
|
normalized(question),
|
|
)
|
|
)
|
|
|
|
|
|
def unresolved_followup(question: str) -> bool:
|
|
return bool(
|
|
re.fullmatch(r'(?:(?:a|ale)(?: co)?\s+)?v roku [12]\d{3}[.?]?',normalized(question)) or
|
|
re.search(r'\b(?:vtedy|(?:v|na|o|z|pri) (?:danom|tomto|tom|tomto istom) stretnuti|(?:tej|pri) druhej praci)\b',normalized(question)) or
|
|
re.fullmatch(
|
|
r"(?:(?:daj|uved|ukaz|vypis|porovnaj|chcem|prosim)\s+)?"
|
|
r"(?:obe|oba|obidv\w*|vsetky|prvy|druhy)(?:\s+(?:prace|projekty))?[.!?]?",
|
|
normalized(question),
|
|
)
|
|
)
|
|
|
|
|
|
def output_year_requested(question: str) -> bool:
|
|
# A supplied year scopes the evidence; it does not itself ask for a year claim.
|
|
text = normalized(question)
|
|
without_filters = re.sub(r"\b(?:v|z|pre|na)\s+rok(?:u)?\s+\d+\b", " ", text)
|
|
return bool(re.search(r"\brok\w*\b", without_filters))
|
|
|
|
|
|
def graph_needs_text(question: str) -> bool:
|
|
text = normalized(re.sub(r'[„“"«][^„“”"»]+[“”"»]', " ", question))
|
|
# Explicit graph properties stay on their existing path.
|
|
if re.search(r"\b(?:tag\w*|klucov\w*|kategor\w*|tem\w*|nazov\w*|autor\w*)\b", text):
|
|
return False
|
|
return bool(
|
|
set(requested_facets(question))
|
|
& {"assignment", "plans", "problems", "methods", "experiments", "progress", "results"}
|
|
)
|
|
|
|
|
|
def planned_experiments(question: str) -> bool:
|
|
text = normalized(question)
|
|
if re.search(
|
|
r"\b(?:vykonal\w*|vykonan\w*|uskutocnil\w*|uskutocnen\w*|realizoval\w*|realizovan\w*|otestoval\w*|vyhodnotil\w*)\b",
|
|
text,
|
|
):
|
|
return False
|
|
return bool(
|
|
re.search(
|
|
r"\b(?:plan\w*|pripravit|navrhnut)|"
|
|
r"\b(?:mal|mali|mala|ma|maju)\s+(?:priprav\w*|navrhn\w*|otest\w*|vyhodnot\w*)",
|
|
text,
|
|
)
|
|
)
|