654 lines
32 KiB
Python
654 lines
32 KiB
Python
"""Source-bound academic roles, meeting records and explicit completion events.
|
|
|
|
Neither authorship nor an academic heading proves supervision or graduation.
|
|
This path needs no generative model: every delivered statement cites literal
|
|
evidence from the same profile and work. Unknown is never treated as ongoing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
import re
|
|
|
|
from tools.answers.context_policy import normalized
|
|
from tools.knowledge.sources import advisor_name
|
|
|
|
|
|
def with_profile_metadata(result,plan,db_file):
|
|
"""Keep indexed document authorship and literal study start separate from work events."""
|
|
import sqlite3
|
|
from tools.sync.index_state import index_snapshot,read_version
|
|
from tools.rag.search_core import make_source_url
|
|
if result.get('status')!='ok' or not result.get('accepted_for_delivery'):return result
|
|
with index_snapshot(db_file.parent,db_file=db_file):
|
|
if read_version(db_file.parent)!=result['index_version']:raise RuntimeError('Authorship snapshot changed')
|
|
with sqlite3.connect(db_file.resolve().as_uri()+'?mode=ro',uri=True) as conn:
|
|
rows=[]
|
|
for person in plan.spec.people:
|
|
if person.role!='student' or not person.source_document:continue
|
|
row=conn.execute('SELECT author FROM documents WHERE path=? AND COALESCE(published,1)=1',
|
|
(person.source_document,)).fetchone()
|
|
if 'document_author' in plan.spec.fields and row and row[0]:rows.append({'person':person.name,'document_author':row[0],
|
|
'source_path':person.source_document,'source_url':make_source_url(person.source_document),'metadata_field':'author'})
|
|
if 'start_year' in plan.spec.fields:
|
|
starts={}
|
|
for chunk_id,text in conn.execute('SELECT chunk_id,text FROM chunks WHERE document_path=? AND COALESCE(published,1)=1',(person.source_document,)):
|
|
for line in text.splitlines():
|
|
match=re.fullmatch(r'rok zaciatku studia ([12]\d{3})',normalized(line))
|
|
if match:starts[int(match[1])]=(chunk_id,line)
|
|
if len(starts)==1:
|
|
year=next(iter(starts));chunk_id,quote=starts[year]
|
|
rows.append({'person':person.name,'start_year':year,'source_path':person.source_document,
|
|
'source_url':make_source_url(person.source_document),'metadata_field':'literal_start_year','chunk_id':chunk_id,'quote':quote})
|
|
if not rows:return result
|
|
body,separator,sources=result['answer'].rpartition('\n\nZdroje:')
|
|
if not separator:body=result['answer']
|
|
lines=[('Autor dokumentu profilu '+r['person']+': '+r['document_author']+'.') if 'document_author' in r else
|
|
('Explicitný rok začiatku štúdia — '+r['person']+': '+str(r['start_year'])+'.') for r in rows]
|
|
if 'start_year' in plan.spec.fields and 'year' in plan.spec.fields:
|
|
lines.append('Rok práce, dátum záznamu a dátum vykonania činnosti sú odlišné údaje.')
|
|
urls=list(dict.fromkeys([*result['source_urls'],*(r['source_url'] for r in rows)]))
|
|
answer=body+'\n\n'+'\n'.join(lines)+'\n\nZdroje: '+', '.join(urls)
|
|
return {**result,'answer':answer,'metadata_evidence':rows,
|
|
'source_urls':urls}
|
|
from tools.retrieval.person_names import resolve_person_names
|
|
from tools.retrieval.work_selector import parse_work_selector
|
|
|
|
|
|
def academic_intent(question):
|
|
text = normalized(question)
|
|
completion = re.search(
|
|
r"\b(?:ukonc\w*|skoncil\w*|dokonc\w*|obhaj\w*|neobhaj\w*|absolvov\w*|"
|
|
r"prerusil\w*|studuje|studium|stale prebieha)\b",
|
|
text,
|
|
)
|
|
subtask = re.search(r"\b(?:uloh\w*|experiment\w*|implement\w*|trenovan\w*)\b", text)
|
|
academic_object = re.search(r"\b(?:obhaj\w*|studium|studia|studuje|ukoncenie prace)\b", text)
|
|
if completion and (not subtask or academic_object):
|
|
return "completion"
|
|
if re.search(r"\b(?:stretnut\w*|konzultaci\w*)\b", text) and re.search(
|
|
r"\b(?:naposledy|posledn\w*|najnov\w*|kedy|o com)\b", text
|
|
):
|
|
return "meeting"
|
|
if re.search(r"\bkto\b.*\b(?:vedie|viedol|viedla)\b", text):
|
|
return "advisors"
|
|
if re.search(r"\b(?:veduc\w*|skolitel\w*|konzult\w*)\b", text):
|
|
return "advisors"
|
|
return None
|
|
|
|
|
|
def completion_event(unit):
|
|
"""Recognize narrow, explicit outcomes, never completed subtasks or plans."""
|
|
from tools.knowledge.evidence import claim_eligible, STRUCTURAL_KINDS
|
|
if unit.get("kind") in STRUCTURAL_KINDS or not claim_eligible(unit):
|
|
return None
|
|
text = normalized(unit["text"])
|
|
if (
|
|
unit.get("phase") in {"assignment", "planned"}
|
|
or unit.get("completion_basis") == "checked_task"
|
|
):
|
|
return None
|
|
if re.search(
|
|
r"\b(?:planuj\w*|plan |ciel\w*|mal by|ma byt|bude|budu|treba|chce|chcel|"
|
|
r"nepotvrd\w*|nie je potvrden\w*|nie je dolozen\w*|zatial neukoncil\w*)\b",
|
|
text,
|
|
):
|
|
return None
|
|
# Avoid interpreting a nested quote about somebody else as this student's outcome.
|
|
if re.search(r"\b(?:iny student|inych studentov|kolega|kolegy\w*)\b", text):
|
|
return None
|
|
study = bool(re.search(r"\b(?:studium|studia|studuje|absolvov\w*)\b", text))
|
|
work = bool(re.search(r"\b(?:prac\w*|projekt\w*|obhaj\w*|neobhaj\w*)\b", text))
|
|
if study and work:
|
|
return None # Two different event scopes in one sentence need separate evidence.
|
|
status = None
|
|
if re.search(r"\b(?:ukoncil\w*|skoncil\w*) predcasne\b|\bprerusil\w* studium\b", text):
|
|
status = "interrupted" if "prerusil" in text else "premature"
|
|
elif re.search(r"\bneobhajil\w*\b|\bneuspesne (?:ukoncil\w*|obhajil\w*)\b", text):
|
|
status = "unsuccessful"
|
|
elif re.search(
|
|
r"\b(?:uspesne (?:ukoncil\w*|obhajil\w*|absolvoval\w*)|obhajil\w*|uspesne obhajen[ay]|absolvoval\w*)\b", text
|
|
):
|
|
if not re.search(r"\b(?:neobhajil\w*|nie|neuspesne)\b", text):
|
|
status = "successful"
|
|
elif re.search(r"\b(?:dokonc(?:il|ila|ene|ena)\w*|ukoncil\w*|odovzdal\w*)\b", text) and (
|
|
study or work
|
|
):
|
|
status = "submitted" if "odovzdal" in text else "completed_unspecified"
|
|
elif study and re.search(r"\b(?:stale studuje|studium pokracuje|studium prebieha)\b", text):
|
|
status = "ongoing"
|
|
elif work and re.search(r"\b(?:praca|projekt) (?:stale )?(?:prebieha|pokracuje)\b", text):
|
|
status = "ongoing"
|
|
if not status or not (study or work or status in {"premature", "interrupted"}):
|
|
return None
|
|
# A model, database, experiment or text being finished isn't a defended thesis.
|
|
if re.search(
|
|
r"\b(?:text prace|pisanie prace|minimov\w*|pracu na|ukoncil\w* trenovan\w*|dokonc\w* implement\w*)\b",
|
|
text,
|
|
):
|
|
return None
|
|
scope = "studies" if study else "work" if work else "unspecified"
|
|
if scope == "work":
|
|
from tools.answers.rag_evidence import academic_scope
|
|
|
|
mentioned = academic_scope(unit["text"])
|
|
if mentioned and unit.get("work_type") and mentioned[0] != unit["work_type"]:
|
|
return None
|
|
# Event timing comes only from the statement, never its record/work heading.
|
|
from tools.knowledge.evidence import temporal_fields
|
|
timing = temporal_fields(unit["text"], unit.get("phase", "unknown"))
|
|
return {"status":status,"scope":scope,"event_year":timing["event_year"],
|
|
"event_date":timing["event_date"]}
|
|
|
|
|
|
def answer_academic(question, index, *, engine, limit=5, units=None, mode=None, plan=None):
|
|
mode = mode or academic_intent(question)
|
|
from tools.knowledge.evidence import claim_eligible
|
|
source_rows = units if units is not None else index["units"]
|
|
rows = [r for r in source_rows if claim_eligible(r)]
|
|
catalog = [
|
|
{"name": name, "kind": "Person", "source_document": path}
|
|
for name, path in sorted({(r["person"], r["source_path"]) for r in source_rows})
|
|
]
|
|
role_names = {
|
|
normalized(name): name
|
|
for r in rows
|
|
for role in ("supervisor", "consultant")
|
|
if (name := advisor_name(r, role))
|
|
}
|
|
known = {normalized(p["name"]) for p in catalog}
|
|
catalog += [
|
|
{"name": name, "kind": "Person"} for key, name in role_names.items() if key not in known
|
|
]
|
|
resolution = plan.resolution if plan else resolve_person_names(question, catalog)
|
|
base = {
|
|
"engine": engine,
|
|
"index_version": index["index_version"],
|
|
"knowledge_version": index["version"],
|
|
"accepted_for_delivery": True,
|
|
"claims": [],
|
|
"complete": False,
|
|
"truncated": False,
|
|
"truncation_reasons": [],
|
|
"name_resolution": resolution,
|
|
}
|
|
presentation_notes=[]
|
|
|
|
def respond(text, evidence=(), *, status="ok", reason=None, truncated=False):
|
|
unique = list({r["id"]: r for r in evidence}.values())
|
|
urls = list(dict.fromkeys(r["source_url"] for r in unique))
|
|
text = "\n\n".join([*resolution.get("notices", []), text])
|
|
if urls:
|
|
text += "\n\nZdroje: " + ", ".join(urls)
|
|
return {
|
|
**base,
|
|
"status": status,
|
|
"reason": reason,
|
|
"answer": text,
|
|
"source_urls": urls,
|
|
"truncated": truncated,
|
|
"truncation_reasons": ["academic_result_limit"] if truncated else [],
|
|
"evidence": unique,
|
|
'presentation_notes':list(dict.fromkeys(presentation_notes)),
|
|
"answer_coverage": {
|
|
"scope": "list" if mode == "advisors" else "overview",
|
|
"status": (
|
|
"not_found"
|
|
if status == "no_answer"
|
|
else (
|
|
"unavailable"
|
|
if status == "error"
|
|
else "partial" if reason or truncated or presentation_notes else "supported"
|
|
)
|
|
),
|
|
"warnings": [],
|
|
"limitations": [reason] if reason else [],
|
|
},
|
|
}
|
|
|
|
if resolution["status"] != "ok":
|
|
return respond(
|
|
resolution["message"], status=resolution["status"], reason=resolution["reason"]
|
|
)
|
|
if index.get("academic_schema") != 1:
|
|
return respond(
|
|
"Index rolí, stretnutí a ukončení ešte nie je pripravený pre túto verziu dát. "
|
|
"Je potrebná obnova obsahového indexu.",
|
|
status="error",
|
|
reason="content_index_unavailable",
|
|
)
|
|
resolved = resolution.get("resolved_query", question)
|
|
text = normalized(resolved)
|
|
selection = parse_work_selector(resolved)
|
|
from tools.retrieval.question_constraints import unsupported_constraint, constraint_message
|
|
|
|
unsupported = unsupported_constraint(resolved)
|
|
if not plan and unsupported not in {None, "work_event_date", "year_range"}:
|
|
return respond(
|
|
constraint_message(unsupported), status="ambiguous", reason="unsupported_constraint"
|
|
)
|
|
if not plan and re.search(r"\b(?:tag\w*|klucov\w* slov\w*|autor dokumentu|semest\w*)\b", text):
|
|
return respond(
|
|
"Túto kombinovanú podmienku neviem spoľahlivo overiť. Uveď osobu, konkrétnu prácu "
|
|
"a požadovanú rolu, stretnutie alebo ukončenie.",
|
|
status="ambiguous",
|
|
reason="unsupported_academic_join",
|
|
)
|
|
matches = resolution.get("matches", [])
|
|
staff = {normalized(m["name"]) for m in matches if normalized(m["name"]) in role_names}
|
|
students = [m for m in matches if m.get("source_document")]
|
|
if plan:
|
|
staff={normalized(p.name) for p in plan.spec.people if p.role in {'supervisor','consultant'}}
|
|
student_names={p.name for p in plan.spec.people if p.role=='student'}
|
|
students=[m for m in matches if m['name'] in student_names and m.get('source_document')]
|
|
# A known mentor can also have a student profile: only treat it as mentor
|
|
# when the question explicitly filters by leadership/consultation.
|
|
role_filter = bool(
|
|
staff
|
|
and re.search(
|
|
r"\b(?:pod vedenim|vedie|viedol|veden\w*|veduc\w*|skolitel\w*|konzult\w*)\b", text
|
|
)
|
|
)
|
|
if role_filter:
|
|
students = [m for m in students if normalized(m["name"]) not in staff]
|
|
global_question = bool(re.search(r"\b(?:kto|ktori|ktore)\b", text) and mode == "completion")
|
|
if not students and not role_filter and not global_question:
|
|
return respond(
|
|
"Uveď celé meno študenta alebo meno vedúceho, podľa ktorého mám práce vybrať.",
|
|
status="ambiguous",
|
|
reason="missing_subject",
|
|
)
|
|
if len(staff) > 1 and role_filter:
|
|
return respond(
|
|
"Uveď jedného vedúceho alebo konzultanta pre tento výber prác.",
|
|
status="ambiguous",
|
|
reason="ambiguous_advisor_filter",
|
|
)
|
|
from tools.answers.rag_evidence import requested_types
|
|
|
|
types = requested_types(resolved, [m["name"] for m in students])
|
|
# Ordinary wording of the same work-type constraint must not silently
|
|
# broaden "diplomovku" to every bachelor thesis and project on a profile.
|
|
informal = {
|
|
kind
|
|
for pattern, kind in [
|
|
(r"\bdiplomovk\w*\b", "diploma_thesis"),
|
|
(r"\bbakalark\w*\b", "bachelor_thesis"),
|
|
]
|
|
if re.search(pattern, text)
|
|
}
|
|
if len(informal) > 1 or informal and types and not set(types.values()) <= informal:
|
|
return respond(
|
|
"Upresni, či sa pýtaš na bakalársku alebo diplomovú prácu.",
|
|
status="ambiguous",
|
|
reason="ambiguous_work_type",
|
|
)
|
|
if informal:
|
|
types = {m["name"]: next(iter(informal)) for m in students}
|
|
global_types = set(types.values())
|
|
if not students:
|
|
from tools.answers.rag_evidence import work_type
|
|
|
|
kind = work_type(resolved)
|
|
if kind:
|
|
global_types = {kind}
|
|
elif informal:
|
|
global_types = informal
|
|
# Completion-year filters apply to the EVENT, not the work heading.
|
|
from tools.knowledge.query import _period
|
|
|
|
try:
|
|
period = ((plan.spec.period.start,plan.spec.period.end) if plan.spec.period else None) if plan else _period(resolved)
|
|
except ValueError:
|
|
return respond(
|
|
"Uveď platný rok alebo interval dátumov.", status="ambiguous", reason="invalid_period"
|
|
)
|
|
work_year_match = re.search(
|
|
r"\b(?:diplomov\w*|bakalarsk\w*|timov\w*)\s+(?:prac\w*|projekt\w*)"
|
|
r"(?:\s+\d{1,2})?\s+(?:z\s+roku\s+)?([12]\d{3})\b",
|
|
text,
|
|
)
|
|
work_year = int(work_year_match[1]) if work_year_match and not plan else None
|
|
if mode == "completion" and period and work_year:
|
|
return respond(
|
|
"Máš na mysli rok evidovaný pri práci, alebo rok jej skutočného ukončenia? "
|
|
"Uveď výslovne požadovaný dátum ukončenia; tieto roky nemusia byť rovnaké.",
|
|
status="ambiguous",
|
|
reason="completion_year_scope",
|
|
)
|
|
selected_paths = {m["source_document"] for m in students}
|
|
profile_roles=defaultdict(list)
|
|
for row in rows:
|
|
if row['kind'] in {'supervisor','consultant'} and not row.get('work_id'):
|
|
profile_roles[row['source_path']].append(row)
|
|
profile_work_ids=defaultdict(set)
|
|
for row in index['units']:
|
|
if row['kind']=='work_record' and row.get('work_id'):profile_work_ids[row['source_path']].add(row['work_id'])
|
|
groups = defaultdict(list)
|
|
for r in rows:
|
|
if plan and not plan.spec.matches_work(r['person'],r.get('work_label'),r.get('work_type'),r.get('work_year')):continue
|
|
if selected_paths and r["source_path"] not in selected_paths:
|
|
continue
|
|
if selection.specific and not selection.matches(r.get("work_label") or ""):
|
|
continue
|
|
kind = types.get(r["person"])
|
|
if kind and r.get("work_type") != kind:
|
|
continue
|
|
if not students and global_types and r.get("work_type") not in global_types:
|
|
continue
|
|
if (
|
|
mode == "advisors"
|
|
and period
|
|
and not (
|
|
r.get("work_year") and int(period[0][:4]) <= r["work_year"] <= int(period[1][:4])
|
|
)
|
|
):
|
|
continue
|
|
if mode == "meeting" and work_year and r.get("work_year") != work_year:
|
|
continue
|
|
groups[(r["source_path"], r.get("work_label"))].append(r)
|
|
requested_role = (
|
|
"consultant"
|
|
if re.search(r"\bkonzult\w*\b", text) and not re.search(r"\bveduc\w*|\bveden\w*", text)
|
|
else "supervisor"
|
|
)
|
|
if role_filter:
|
|
groups = {
|
|
key: part
|
|
for key, part in groups.items()
|
|
if key[1]
|
|
and any(normalized(advisor_name(r, requested_role) or "") in staff for r in part)
|
|
}
|
|
evidence = []
|
|
lines = []
|
|
omitted = False
|
|
status_labels = {
|
|
"successful": "úspešné ukončenie / obhajoba",
|
|
"unsuccessful": "neúspešné ukončenie / obhajoba",
|
|
"premature": "predčasné ukončenie",
|
|
"interrupted": "prerušenie štúdia",
|
|
"completed_unspecified": "ukončenie bez doloženého výsledku obhajoby",
|
|
"submitted": "odovzdanie; úspešná obhajoba nepotvrdená",
|
|
"ongoing": "výslovne zaznamenané pokračovanie",
|
|
}
|
|
study_question = bool(re.search(r"\bstudium\b|\bstudia\b|\bstuduje\b", text))
|
|
wants_success = bool(re.search(r"\buspesn\w*\b", text))
|
|
wants_failure = bool(re.search(r"\bneuspesn\w*\b|\bneobhaj\w*\b", text))
|
|
if wants_success and wants_failure:
|
|
wants_success = wants_failure = False
|
|
outcomes = defaultdict(list)
|
|
conflicts = set()
|
|
if mode == "completion":
|
|
for key, part in groups.items():
|
|
all_events = [(r, event) for r in part if (event := completion_event(r))]
|
|
# Unresolved contradictory outcomes of the same work must not
|
|
# produce an unqualified success list. A later dated re-defense
|
|
# can be queried explicitly by date instead.
|
|
statuses = {event["status"] for r, event in all_events if event["scope"] == "work"}
|
|
if {"successful", "unsuccessful"} <= statuses and any(
|
|
not event["event_date"] for r, event in all_events
|
|
):
|
|
conflicts.add(key)
|
|
continue
|
|
for r, event in all_events:
|
|
if study_question and event["scope"] != "studies":
|
|
continue
|
|
if not study_question and key[1] and event["scope"] == "studies":
|
|
continue
|
|
if role_filter and event["scope"] != "work":
|
|
continue
|
|
if wants_success and event["status"] != "successful":
|
|
continue
|
|
if wants_failure and event["status"] != "unsuccessful":
|
|
continue
|
|
if period:
|
|
if event["event_date"]:
|
|
if not period[0] <= event["event_date"] <= period[1]:
|
|
continue
|
|
elif not (
|
|
period[0].endswith("-01-01")
|
|
and period[1].endswith("-12-31")
|
|
and event["event_year"]
|
|
and int(period[0][:4]) <= event["event_year"] <= int(period[1][:4])
|
|
):
|
|
continue
|
|
outcomes[key].append((r, event))
|
|
# Unscoped introductory metadata must not become a fictional extra work.
|
|
keys = sorted(
|
|
(
|
|
key
|
|
for key, part in groups.items()
|
|
if key[1]
|
|
or any(
|
|
completion_event(r)
|
|
or advisor_name(r, "supervisor")
|
|
or advisor_name(r, "consultant")
|
|
or r["kind"] == "meeting"
|
|
for r in part
|
|
)
|
|
),
|
|
key=lambda k: (groups[k][0]["person"], k[0], k[1] or ""),
|
|
)
|
|
if mode == "completion" and (role_filter or not students):
|
|
keys = [k for k in keys if outcomes[k] or k in conflicts]
|
|
if mode == "meeting":
|
|
by_person = defaultdict(list)
|
|
for key in keys:
|
|
by_person[key[0]].extend(groups[key])
|
|
for path, part in by_person.items():
|
|
markers = [
|
|
r
|
|
for r in part
|
|
if r["kind"] == "meeting"
|
|
and (
|
|
not period or work_year or r.get("date") and period[0] <= r["date"] <= period[1]
|
|
)
|
|
]
|
|
dated = [r for r in markers if r.get("date")]
|
|
if not markers:
|
|
lines.append(
|
|
f'**{part[0]["person"]}**: v tomto výbere nie je doložený záznam stretnutia.'
|
|
)
|
|
continue
|
|
latest = max((r["date"] for r in dated), default=None)
|
|
meeting_selection=plan.spec.meeting if plan else None
|
|
exact=bool(meeting_selection and meeting_selection.selection in {'exact','all'})
|
|
if exact:
|
|
chosen=[r for r in markers if meeting_selection.matches(r)]
|
|
if meeting_selection.selection=='exact' and len(chosen)>1 and len({r.get('work_id') for r in chosen})>1:
|
|
return respond('Rovnakému dátumu zodpovedá viac prác. Uveď presné označenie práce.',status='ambiguous',reason='ambiguous_meeting_work')
|
|
else:
|
|
chosen = meeting_selection.select(markers) if meeting_selection else ([r for r in markers if r.get("date") == latest] if latest else markers)
|
|
chosen.sort(key=lambda r:(r.get('date') or '',r['line']))
|
|
if meeting_selection and meeting_selection.selection=='latest' and len({r.get('date') for r in chosen if r.get('date')})<meeting_selection.count:
|
|
note=part[0]['person']+': zdroj obsahuje menej úplne datovaných stretnutí, než požaduje otázka; chýbajúce stretnutie nemožno domyslieť.'
|
|
lines.append(note);presentation_notes.append(note)
|
|
for marker in chosen[:limit]:
|
|
heading='Dátum požadovaného stretnutia' if exact or meeting_selection and meeting_selection.count>1 else 'Posledný záznam s úplným dátumom'
|
|
displayed_date=marker.get('date')
|
|
lines.append(
|
|
f'**{marker["person"]} — {marker.get("work_label") or "profil"}**\n'
|
|
f"{heading}: {displayed_date}."
|
|
if displayed_date
|
|
else f'**{marker["person"]}** — záznam: {marker["date_label"]}; rok nie je uvedený.'
|
|
)
|
|
evidence.append(marker)
|
|
if (plan and {'start_year','date'}<=set(plan.spec.fields) and not plan.spec.facets
|
|
and not re.search(r'\bo com\b|\bco sa\b',text)):
|
|
continue # A comparison of time roles does not ask for the meeting agenda.
|
|
notes = [
|
|
r
|
|
for r in part
|
|
if r.get("meeting_id") == marker["meeting_id"]
|
|
and r["kind"]
|
|
not in {"meeting", "work_record", "reference", "supervisor", "consultant"}
|
|
]
|
|
from tools.answers.content_selection import unit_score
|
|
|
|
notes = [r for r in notes if not r["text"].rstrip().endswith(":")]
|
|
if plan and plan.spec.facets and plan.spec.answer_mode!='comparison':
|
|
from tools.answers.content_selection import facets_for
|
|
notes=[r for r in notes if set(plan.spec.facets)&set(facets_for(r['text'],
|
|
{'assignment':'assignment','planned':'task','result':'progress','progress':'progress'}.get(r['phase'],'other'),
|
|
plan.spec.question,r['phase']))]
|
|
notes.sort(
|
|
key=lambda r: (
|
|
-unit_score(
|
|
r["text"],
|
|
"progress" if r["phase"] in {"result", "progress"} else "task",
|
|
),
|
|
r["phase"] != "result",
|
|
r["line"],
|
|
)
|
|
)
|
|
actual = [r for r in notes if r["phase"] in {"result", "progress"}]
|
|
planned = [r for r in notes if r["phase"] == "planned"]
|
|
selected = actual[:4] + planned[:4]
|
|
selected.extend(r for r in notes if r not in selected)
|
|
notes = selected
|
|
if plan and plan.spec.facets:
|
|
from tools.answers.content_selection import facets_for
|
|
from tools.answers.question_intent import FACET_LABELS
|
|
found={f for r in notes for f in facets_for(r['text'],
|
|
{'assignment':'assignment','planned':'task','result':'progress','progress':'progress'}.get(r['phase'],'other'),
|
|
plan.spec.question,r['phase'])}
|
|
for facet in plan.spec.facets:
|
|
if facet not in found:
|
|
note=marker['person']+' — záznam '+str(marker.get('date') or marker.get('date_label'))+': '+FACET_LABELS[facet].lower()+': požadovaný údaj nie je v tomto zápise doložený.'
|
|
lines.append(note);presentation_notes.append(note)
|
|
# A meeting is a finite source scope. Keep its whole agenda
|
|
# within a shared budget instead of discarding evaluation tasks
|
|
# after eight arbitrarily ranked excerpts.
|
|
kept=[];size=0
|
|
for note in notes:
|
|
if len(kept)>=24 or size+len(note['text'])>15000//max(1,len(chosen[:limit])):
|
|
omitted=True;continue
|
|
kept.append(note);size+=len(note['text'])
|
|
for note in kept:
|
|
phase = {
|
|
"result": "Zaznamenaný výsledok",
|
|
"progress": "Priebežný stav",
|
|
"planned": "Plán",
|
|
"assignment": "Zadanie",
|
|
}.get(note["phase"], "Záznam")
|
|
quote = re.sub(r"^\s*(?:[-*+]|\d+[.)])\s+", "", note["text"])
|
|
lines.append(f"- {phase}: {quote}")
|
|
evidence.append(note)
|
|
if not notes:
|
|
note='Požadované údaje k tomuto stretnutiu nie sú v dostupnom výbere doložené.'
|
|
lines.append(note);presentation_notes.append(note)
|
|
if len(chosen) > limit:
|
|
omitted = True
|
|
partial = [r for r in part if r["kind"] == "meeting" and not r.get("date")]
|
|
if partial and not exact:
|
|
note='Profil obsahuje aj záznamy bez úplného dátumu; nemožno spoľahlivo určiť, či sú novšie. Rok nedopĺňam z roku práce.'
|
|
lines.append(note);presentation_notes.append(note)
|
|
evidence.extend(partial)
|
|
else:
|
|
for key in keys[:limit]:
|
|
part = groups[key]
|
|
person = part[0]["person"]
|
|
label = key[1] or "profil (bez priradenia ku konkrétnej práci)"
|
|
lines.append(f"**{person} — {label}**")
|
|
header = next((r for r in part if r["kind"] == "work_record"), None)
|
|
if header:
|
|
evidence.append(header)
|
|
title = next(
|
|
(r for r in part if r["kind"] == "topic" and not r["text"].rstrip().endswith(":")),
|
|
None,
|
|
)
|
|
if title:
|
|
lines.append("Názov / téma zo zdroja: " + title["text"])
|
|
evidence.append(title)
|
|
if mode == "advisors" or role_filter:
|
|
used_profile_author = False
|
|
for role, name_label in [
|
|
("supervisor", "Vedúci práce"),
|
|
("consultant", "Konzultant"),
|
|
]:
|
|
requested=set(plan.spec.fields)&{'supervisor','consultant'} if plan else set()
|
|
if requested and role not in requested:continue
|
|
roles = [r for r in part if advisor_name(r, role)]
|
|
if not roles and len(profile_work_ids[key[0]])==1:
|
|
roles=[r for r in profile_roles[key[0]] if advisor_name(r,role)]
|
|
if roles:name_label='Vedúci v hlavičke profilu' if role=='supervisor' else 'Konzultant v hlavičke profilu'
|
|
names = list(dict.fromkeys(advisor_name(r, role) for r in roles))
|
|
if role == "supervisor" and not names:
|
|
from tools.knowledge.sources import profile_authors
|
|
author = profile_authors().get(key[0])
|
|
if author:
|
|
names = [author]
|
|
name_label = "Vedúci práce (autor profilu študenta)"
|
|
used_profile_author = True
|
|
lines.append(
|
|
name_label
|
|
+ ": "
|
|
+ (", ".join(names) if names else "v dostupných podkladoch nie je uvedený")
|
|
+ "."
|
|
)
|
|
evidence.extend(roles)
|
|
if mode == "advisors" and used_profile_author:
|
|
from tools.knowledge.sources import PROFILE_AUTHOR_NOTE
|
|
lines.append(PROFILE_AUTHOR_NOTE)
|
|
if mode == "completion":
|
|
if key in conflicts:
|
|
lines.append(
|
|
"Zdroj obsahuje rozporné záznamy o výsledku tejto práce; bez doloženého poradia "
|
|
"obhajob nemožno potvrdiť úspešné ukončenie."
|
|
)
|
|
evidence.extend(r for r in part if completion_event(r))
|
|
continue
|
|
found = outcomes[key]
|
|
for r, event in found:
|
|
scope = {
|
|
"studies": "štúdium",
|
|
"work": "práca",
|
|
"unspecified": "rozsah ukončenia nie je výslovne uvedený",
|
|
}[event["scope"]]
|
|
lines.append(f'- {status_labels[event["status"]]} ({scope}): „{r["text"]}“')
|
|
lines.append(
|
|
"Rok udalosti: " + str(event["event_year"]) + "."
|
|
if event["event_year"]
|
|
else "Dátum ukončenia nie je doložený; rok práce ho nenahrádza."
|
|
)
|
|
evidence.append(r)
|
|
if not found:
|
|
lines.append(
|
|
"Úspešné ani neúspešné ukončenie tejto práce"
|
|
+ (" či celého štúdia" if study_question else "")
|
|
+ " nie je v dostupných podkladoch doložené. Nemožno z toho usúdiť, že stále prebieha."
|
|
)
|
|
notes = [r for r in part if completion_event(r)]
|
|
for note in notes:
|
|
lines.append(
|
|
"Doplňujúci záznam s odlišným alebo neurčeným rozsahom: „"
|
|
+ note["text"]
|
|
+ "“."
|
|
)
|
|
evidence.append(note)
|
|
omitted = len(keys) > limit
|
|
if not lines:
|
|
return respond(
|
|
"V dostupných profiloch nie je doložená práca, ktorá by súčasne spĺňala všetky podmienky "
|
|
"otázky. Vedúci, konkrétna práca, výsledok ukončenia a rok udalosti musia byť doložené "
|
|
"pri tom istom zázname. Rok práce, autor dokumentu ani odovzdanie textu nepotvrdzujú "
|
|
"úspešnú obhajobu. To neznamená, že takí študenti neexistujú.",
|
|
status="no_answer",
|
|
reason="academic_conditions_not_documented",
|
|
)
|
|
if omitted:
|
|
note='Zobrazený je obmedzený výber. Zúž otázku na študenta, konkrétnu prácu alebo obdobie, prípadne zvýš limit.'
|
|
lines.append(note);presentation_notes.append(note)
|
|
unknown = mode == "completion" and not any(outcomes.values())
|
|
return respond(
|
|
"\n\n".join(lines),
|
|
evidence,
|
|
status="no_answer" if unknown else "ok",
|
|
reason=(
|
|
"conflicting_completion_evidence"
|
|
if conflicts
|
|
else "completion_not_documented" if unknown else None
|
|
),
|
|
truncated=omitted,
|
|
)
|