"""Role-aware, bounded excerpt selection and presentation from actual evidence.""" from __future__ import annotations import re from tools.answers.context_policy import normalized from tools.answers.question_intent import ( FACET_LABELS, planned_experiments, requested_facets, actual_methods_requested, ) COMPLETED = ( r"\b(?:(?:implementovan|natrenovan|vytvoren|extrahovan|vykonan|vyhodnoten|nameran|" r"vyriesen|odstranen|otestovan|porovnan|dosiahnut|dokoncen)(?:y|a|e|u|ou|ych|ym|ymi|om)|" r"urobene vyhodnotenie|implementoval\w*|natrenoval\w*|vyhodnotil\w*|funguje|funguju)\b" ) PLANNED = r"\b(?:bude|budu|planuj\w*|planovan\w*|cielom|mal by|ma sa|ma (?:implementovat|vypracovat|vyhodnotit|vytvorit|navrhnut|natrenovat|otestovat)|treba|pripravte|navrhnite|otestujte|vyhodnotte)\b" ADMIN = r"\b(?:konzultac\w*|struktura prace|pisomn\w*|pisat|odovzda\w*\s+(?:text\w*|pisomn\w*|prac\w*)|wip|termin\w*)\b" NOT_COMPLETED = r"\b(?:nie je|nie su|nebol\w*|este nie|zatial nie|nebude|nebudu|nefung\w*|neimplementov\w*|nevytvoren\w*|nevyhodnoten\w*)\b" # Named tools in a record: acronyms (RAG, MCP), CamelCase (OpenWebUI, GraphRAG), names with digits # (Neo4J, Fast120, me5mini), domains, and common lower-case tool names. TOOL_NAMES = set("""python pytorch tensorflow keras docker kubernetes unsloth llamafactory qlora lora optuna whisper bert slovakbert mistral llama qwen olmo gemma huggingface transformers spacy stanza prodigy rasa langchain llamaindex elasticsearch opensearch neo4j cypher postgres postgresql sqlite mysql mongodb redis fastapi flask django react vue angular nodejs javascript typescript java kotlin matlab opencv yolo onnx ollama openwebui telegram discord grafana prometheus jupyter colab git gitea github gitlab slurm faiss qdrant chroma milvus pandas numpy scikit sklearn skills skill streamlit gradio langgraph cogdb""".split()) TOOL_PATTERN = re.compile(r"(? list[str]: """Tool and technology names the record mentions (not dates, not plain words).""" found = [m for m in TOOL_PATTERN.findall(text or "") if not re.fullmatch(r"\d+[.:]\d*", m) and len(m) >= 2] found += [w for w in re.findall(r"[A-Za-z][A-Za-z0-9]+", text or "") if w.lower() in TOOL_NAMES] return list(dict.fromkeys(found)) def has_qualification(text: str) -> bool: return bool( re.search( r"\b(?:iba|len|avsak|ale|zatial|podmien\w*|obmedzen\w*|nebol\w*|nie|nepotvrd\w*)\b", normalized(text), ) ) def administrative_only(text: str) -> bool: n = normalized(text) return bool(re.search(ADMIN, n) and not re.search(COMPLETED, n)) def reference_only(text: str) -> bool: """A link alone establishes a reference, never an implemented result.""" value = re.sub(r"^\s*(?:[-*+]|\d+[.)])\s+", "", text).strip() if re.fullmatch(r"\[[^]]+\]\([^\s]+\)", value): return True from tools.answers.context_policy import text_urls links = text_urls(value) if not links: return False for url in links: value = value.replace(url, "") return bool( re.fullmatch( r"(?:repozitar(?: s \w+(?: \w+)?)?|zdrojove kody|vystup je tu|vystupy su tu|odkaz|git)?", normalized(value), ) ) def split_units(text: str) -> list[str]: from tools.answers.rag_evidence import descriptive_text parts = re.split(r"\n\s*\n|(?=^[ \t]*(?:\d+[.)]|[-+*])\s+)", text, flags=re.M) units = [] for part in parts: part = part.strip() if not descriptive_text(part): continue # A source paragraph is kept literal, including its negations and qualifications. for unit in ( [part] if len(part) <= 900 or has_qualification(part) else re.split(r"(?<=[.!?])\s+(?=[A-ZÁČĎÉÍĽŇÓŠŤÚÝŽ])", part) ): if descriptive_text(unit): units.append(unit.strip()) return units def effective_role(text: str, role: str) -> str: n = normalized(re.sub(r"^\s*(?:\d+[.)]|[-+*])\s+", "", text)) if role in {"other", "progress"}: from tools.answers.presentation import GOAL_VERBS first = n.split()[0] if n else "" if ( first in {normalized(word) for word in [*GOAL_VERBS, *GOAL_VERBS.values()]} or n.startswith("cielom je") or re.search(PLANNED, n) ): return "task" if role == "other" and re.search(COMPLETED, n) and not re.search(PLANNED, n): return "progress" return role def evidence_phase(text: str, role: str, source_phase: str = "unknown") -> str: if reference_only(text): return "unknown" if role == "assignment": return "assignment" if role == "task": return "planned" if re.search(PLANNED, normalized(text)): return "planned" if administrative_only(text): return "administrative" if role == "progress": n = normalized(text) negative_finding = re.search(r"\b(?:nebol uspesny|nebola uspesna|neboli uspesne|neprekonal\w*|nezlepsil\w*)\b",n) if negative_finding and re.search(r"\b(?:test\w*|meran\w*|experiment\w*|vyhodnot\w*)\b",n): return "result" if re.search(NOT_COMPLETED, n): return "progress" if source_phase == "result" or ( not re.search(NOT_COMPLETED, normalized(text)) and re.search(COMPLETED, normalized(text)) ): return "result" return "progress" return "unknown" def facets_for(text: str, role: str, question: str, phase: str | None = None) -> list[str]: if reference_only(text): return [] n = normalized(text) facets = [] if role == "assignment": facets.append("assignment") if role == "task": facets.append("plans") if role == "progress" and not re.search(PLANNED, n) and not administrative_only(text): facets.append("progress") if (phase or evidence_phase(text, role)) == "result": facets.append("results") if re.search( r"\b(?:problem\w*|tazkost\w*|tazke|prekazk\w*|chyb\w*|zlyha\w*|nepodar\w*|nevyries\w*|nefung\w*|nepasuj\w*|nesulad\w*|nekompatibil\w*)\b", n ) and role not in {"assignment", "task"}: facets.append("problems") tools = named_tools(text) if re.search( r"\b(?:metod\w*|technolog\w*|nastroj\w*|pouzi\w*|vyuziv\w*|pomocou|implementovan\w*|natrenovan\w* model\w*|indexov\w*|databaz\w*|webov\w* rozhrani\w*|frontend\w*|backend\w*|vyskus\w*|sumarizac\w*)\b", n, ) or tools or re.search(SEARCH_METHODS, n): actual_use = actual_methods_requested(question) # A status/result record naming a tool ("Sú Skills aj MCP server", "začíname pracovať na # GraphRAG a Neo4J") states real use unless it is phrased as a plan or as not done. stated_in_status = (role in {"progress", "result"} and bool(tools or re.search(SEARCH_METHODS, n)) and not re.search(PLANNED, n)) actual_statement = not re.search(NOT_COMPLETED, n) and ( re.search(COMPLETED, n) or re.search(r'\bvyskus(?:any|ana|ane|al|ala|ali)\b',n) or re.search(r"\b(?:pouziva\w*|pouzil\w*|vyuziva\w*|vyuzil\w*|vieme indexov\w*)\b", n) or (not re.search(PLANNED,n) and re.search(r'\b(?:frontend|backend)\s+\w+',n)) or stated_in_status ) if not actual_use or (role not in {"assignment", "task"} and actual_statement): facets.append("methods") if re.search( r"\b(?:experiment\w*|meran\w*|test\w*|otest\w*|vyhodnot\w*|benchmark\w*|f1|presnost\w*)\b", n, ): wants_done = re.search( r"\b(?:vykonal\w*|vykonan\w*|otestoval\w*|uskutocnil\w*|uskutocnen\w*|realizovan\w*|vyhodnotil\w*)\b", normalized(question), ) if wants_done and role in {"assignment", "task"}: pass elif ( not planned_experiments(question) or role in {"assignment", "task"} or re.search(PLANNED, n) ): facets.append("experiments") return facets def unit_score(text: str, role: str, facet: str = "") -> int: n = normalized(text) score = 25 if role == "progress" else 10 if re.search(COMPLETED, n): score += 45 if re.search( r"\b(?:model|system|algoritm\w*|vyhodnot\w*|experiment\w*|presnost\w*|pagerank|bert)\b", n ): score += 15 if administrative_only(text): score -= 70 if facet == "problems" and re.search(r"\b(?:problem\w*|chyb\w*|zlyha\w*)\b", n): score += 35 if role in {"assignment", "task"} and facet == "results": score -= 100 return score def prioritize_frames(frames: list[str], question: str) -> list[str]: """Order whole source windows before budgeting; do not rewrite their text.""" from tools.answers.rag_evidence import content_sections, TITLE_LABELS rankings = [] for facet in requested_facets(question): scores = [] for index, frame in enumerate(frames): relevant = [] for section in content_sections(frame): for text in split_units(section["text"]): role = effective_role(text, section["kind"]) phase = evidence_phase(text, role, section.get("phase", "unknown")) if len(text) <= 2500 and facet in facets_for(text, role, question, phase): relevant.append(unit_score(text, role, facet)) if relevant: scores.append((index, max(relevant))) rankings.append([i for i, _ in sorted(scores, key=lambda item: -item[1])]) order = [] if re.search(r"\bnaz(?:ov|v)\w*\b", normalized(question)) and frames: order = [ i for i, frame in enumerate(frames) if any( ":" in line and normalized(line.split(":", 1)[0]) in TITLE_LABELS for line in frame.splitlines() ) ] or [0] seen = set(order) for pos in range(max(map(len, rankings), default=0)): for ranking in rankings: if pos < len(ranking) and ranking[pos] not in seen: seen.add(ranking[pos]) order.append(ranking[pos]) return [frames[i] for i in [*order, *(i for i in range(len(frames)) if i not in seen)]] def evidence_units(context: dict, quotes: list[dict] | None = None) -> list[dict]: from tools.answers.rag_evidence import content_sections, quote_sections, quote_matches_passage rows = [] for passage in context.get("content_passages", []): pieces = ( content_sections(passage["text"]) if quotes is None else [ piece for quote in quotes if quote_matches_passage(quote, passage) for piece in quote_sections(quote, {"content_passages": [passage]}) ] ) for piece in pieces: for unit in split_units(piece["text"]): role = effective_role(unit, piece["kind"]) phase = evidence_phase(unit, role, piece.get("phase", "unknown")) rows.append( { k: passage.get(k) for k in [ "person", "work_type", "year", "work_label", "source_id", "passage_id", ] } | { "text": unit, "kind": role, "phase": phase, "facets": facets_for(unit, role, context.get("question", ""), phase), } ) # Overlapping chunks/quotes can repeat the same fact with different roles. # An explicit role wins over an unlabelled duplicate, within the same work. unique = {} for row in rows: clean = re.sub(r"^\s*(?:\d+[.)]|[-+*])\s+", "", row["text"]) key = tuple(row.get(k) for k in ["person", "work_type", "year"]) + ( normalized(row.get("work_label")), row["phase"], " ".join(clean.split()), ) old = unique.get(key) if old is None or (old["kind"] == "other" and row["kind"] != "other"): unique[key] = row units = list(unique.values()) # Prefer a whole longer excerpt over its overlapping prefix, within one # work and phase. Never merge people, years or plans with recorded results. def plain(row): return " ".join(re.sub(r"^\s*(?:\d+[.)]|[-+*])\s+", "", row["text"]).split()) return [ row for row in units if not any( all(row.get(k) == other.get(k) for k in ["person", "work_type", "year", "phase"]) and normalized(row.get("work_label")) == normalized(other.get("work_label")) and len(plain(row)) < len(plain(other)) and " " + plain(row) + " " in " " + plain(other) + " " for other in units ) ] def scope_groups(context: dict) -> list[tuple]: req = context.get("requirements", {}) groups = [] for person in req.get("people", []): labels = list( dict.fromkeys( s.get("work_label") for s in context.get("work_scopes", []) if s.get("person") == person ) ) ( groups.extend((person, label) for label in labels) if req.get("separate_works") and labels else groups.append((person, None)) ) return groups def matches_scope(row: dict, person: str, label: str | None) -> bool: return row.get("person") == person and ( label is None or normalized(row.get("work_label")) == normalized(label) ) def scope_context(context: dict, person: str, label: str | None) -> dict: return { **context, "content_passages": [ p for p in context.get("content_passages", []) if matches_scope(p, person, label) ], } def candidates(context: dict) -> list[dict]: from tools.answers.evidence_selection import identify facets = context["requirements"].get("facets", []) selected = [] seen = set() units = evidence_units(context) for person, label in scope_groups(context): for facet in facets: relevant = [ u for u in units if matches_scope(u, person, label) and facet in u["facets"] ] if ( facet == "progress" and "results" in facets and any(u["phase"] == "progress" for u in relevant) ): relevant = [u for u in relevant if u["phase"] == "progress"] if facet != "assignment": relevant.sort(key=lambda u: -unit_score(u["text"], u["kind"], facet)) accepted = 0 for unit in relevant: text = unit["text"] if len(text) > 2500: context["complete"] = False context["truncated"] = True context["truncation_reasons"] = list( dict.fromkeys( [*(context.get("truncation_reasons") or []), "evidence_unit_limit"] ) ) continue key = (unit.get("passage_id"), unit["source_id"], text, unit["phase"]) if key not in seen: seen.add(key) selected.append( identify( {**unit, "text": text}, required=unit["kind"] == "assignment" and "assignment" in facets, ) ) accepted += 1 if accepted == 2 and facet != "assignment": break return selected def missing_facets(answer: dict, context: dict) -> list[tuple[str, str]]: return list( dict.fromkeys( (person, facet) for person, _label, facet in missing_work_facets(answer, context) ) ) def missing_work_facets(answer: dict, context: dict) -> list[tuple]: found = evidence_units(context, answer.get("evidence_quotes", [])) return [ (person, label, facet) for person, label in scope_groups(context) for facet in context["requirements"].get("facets", []) if not any(matches_scope(row, person, label) and facet in row["facets"] for row in found) ] def fit_candidates(sent: dict, context: dict, limit: int, encoding: str) -> None: """Budget the actual candidate JSON, sharing space across named people.""" import json from tools.rag.build_chunks import TokenCounter counter = TokenCounter(encoding) if counter.encoding is None: raise RuntimeError("Missing context tokenizer") original = list(context.get("content_candidates", [])) groups = [ [row for row in original if matches_scope(row, person, label)] for person, label in scope_groups(context) ] ordered = [ group[i] for i in range(max(map(len, groups), default=0)) for group in groups if i < len(group) ] selected = [] for row in ordered: if ( counter.count( json.dumps({**sent, "content_candidates": [*selected, row]}, ensure_ascii=False) ) <= limit ): selected.append(row) if len(selected) != len(original): context["complete"] = False context["truncated"] = True context["truncation_reasons"] = list( dict.fromkeys([*(context.get("truncation_reasons") or []), "answer_candidate_budget"]) ) sent.update({key: context[key] for key in ["complete", "truncated", "truncation_reasons"]}) context["content_candidates"] = selected sent["content_candidates"] = selected # Account for the added limitation metadata as well. while selected and counter.count(json.dumps(sent, ensure_ascii=False)) > limit: selected.pop() if counter.count(json.dumps(sent, ensure_ascii=False)) > limit: raise ValueError("Mandatory answer metadata exceeds context budget") def facet_lines(answer: dict, context: dict) -> list[str]: from tools.answers.presentation import ( goal_text, evidence_subject, missing_aspects, comparison_table, ) req = context["requirements"] units = evidence_units(context, answer.get("evidence_quotes", [])) facets = req.get("facets", []) groups = [] labels = {f: FACET_LABELS[f] for f in facets} if "experiments" in facets and planned_experiments(context.get("question", "")): labels["experiments"] = "Plánované experimenty a vyhodnotenie" for person, work_label in scope_groups(context): group = { "subject": ( evidence_subject(person, scope_context(context, person, work_label)) if work_label is None else person + " — " + " ".join(work_label.split()) ), "items": {}, "missing": [], } seen = set() for facet in facets: rows = [ u for u in units if matches_scope(u, person, work_label) and facet in u["facets"] ] if ( facet == "progress" and "results" in facets and any(u["phase"] == "progress" for u in rows) ): rows = [u for u in rows if u["phase"] == "progress"] if facet != "assignment": rows.sort(key=lambda u: -unit_score(u["text"], u["kind"], facet)) if not rows: group["missing"].append(facet) rendered = [] for row in rows: key = ( " ".join(re.sub(r"^\s*(?:\d+[.)]|[-+*])\s+", "", row["text"]).split()), row["phase"], ) if key in seen: continue text = row["text"] if row["kind"] in {"assignment", "task"}: text, _ = goal_text(text) text = re.sub(r"^\s*(?:\d+[.)]|[-+*])\s+", "", text) phase = ( " (plán)" if row["kind"] in {"assignment", "task"} and facet not in {"assignment", "plans"} else "" ) state = ( "Zaznamenaný výsledok: " if row["phase"] == "result" and "results" in facets and facet != "results" else "" ) rendered.append(state + text + phase) seen.add(key) if len(rendered) == 2 and facet != "assignment": break group["items"][facet] = rendered groups.append(group) if len(groups) > 1 and re.search(r"\bporovnaj\w*\b", normalized(context.get("question", ""))): rows = [] for facet in facets: if not any(g["items"][facet] for g in groups): if all(facet not in g["missing"] for g in groups): continue values = [ "\n".join(g["items"][facet]) or ("V podkladoch chýbajú údaje." if facet in g["missing"] else "Doložené vyššie.") for g in groups ] rows.append((labels[facet], values)) if any(any(g["items"][f] for f in facets) for g in groups): table = comparison_table(["Oblasť", *(g["subject"] for g in groups)], rows) if table is not None: return table lines = [] for group in groups: title = group["subject"] if len(facets) == 1 and group["items"][facets[0]]: title += " — " + labels[facets[0]].lower() lines.extend(["**" + title + "**", ""]) for facet in facets: items = group["items"][facet] if not items: continue if len(facets) > 1: lines.append("**" + labels[facet] + ":**") lines.extend("- " + text for text in items) lines.append("") if group["missing"]: lines.extend([missing_aspects(group["missing"], context.get("question", "")), ""]) return lines