"""Production answer path: retrieval -> model selection -> guard -> rendered text.""" from __future__ import annotations import threading from pathlib import Path from tools.rag.common import DB_FILE from tools.rag.rag_utils import build_rag_context from tools.rag.llm_context import compact_rag_payload from tools.graphrag.graph_search import search_graph from tools.answers.graph_context import graph_context from tools.answers.checked_generation import generate_checked_answer from tools.answers.verified_rag import generate_verified_rag from tools.answers.rag_evidence import expand_comparison_sources from tools.answers.rag_preparation import prepare_named_content from tools.retrieval.question_constraints import unsupported_constraint, constraint_message from tools.sync.index_state import index_snapshot from tools.answers.model_client import complete_with_configured_model from tools.answers.answer_errors import public_failure, clarification_result _capacity = threading.BoundedSemaphore(2) def retrieval_failure(engine: str) -> dict: return public_failure( { "reason": "retrieval_unavailable", "engine": engine, "index_version": None, "complete": False, "truncated": False, "truncation_reasons": [], } ) def name_failure(retrieved: dict, engine: str) -> dict | None: resolution = retrieved.get("name_resolution") or {} if not resolution or resolution.get("status") == "ok": return None return { "status": resolution["status"], "reason": resolution["reason"], "engine": engine, "accepted_for_delivery": True, "answer": resolution["message"], "claims": [], "source_urls": [], "name_resolution": resolution, "index_version": retrieved.get("index_version"), "complete": False, "truncated": False, "truncation_reasons": [], } def with_name_resolution(result: dict, retrieved: dict) -> dict: resolution = retrieved.get("name_resolution") or {} if result.get("accepted_for_delivery") and resolution.get("notices"): result["answer"] = "\n".join(resolution["notices"]) + "\n\n" + result["answer"] if resolution: result["name_resolution"] = resolution return result def deliver_answer( question: str, *, engine: str = "rag", limit: int = 5, complete=None, db_file: Path = DB_FILE, include_diagnostics: bool = False, ) -> dict: from tools.answers.reference_links import checked_repository_links result = _deliver_answer( question, engine=engine, limit=limit, complete=complete, db_file=db_file, include_diagnostics=include_diagnostics, ) return checked_repository_links(result) def _deliver_answer( question: str, *, engine: str = "rag", limit: int = 5, complete=None, db_file: Path = DB_FILE, include_diagnostics: bool = False, ) -> dict: if not _capacity.acquire(blocking=False): return public_failure({"reason": "answer_capacity_exceeded", "engine": engine}) try: model = complete or complete_with_configured_model from tools.answers.question_intent import unresolved_followup if unresolved_followup(question): return clarification_result( "Uveď osobu a presné označenia prác alebo úplnú otázku. Samotné „obe“ bez predchádzajúcich možností neviem spoľahlivo vyhľadať.", "missing_followup_context", engine=engine, ) from tools.knowledge.query import ( invalid_question, deliver_content, ContentModelUnavailable, ContentAnswerRejected, ) if invalid_question(question): return clarification_result( "Otázke nerozumiem. Napíš, na ktorého študenta, tému alebo výsledok sa pýtaš.", "unintelligible_question", engine=engine, ) import re from tools.answers.context_policy import normalized wording = normalized(question) if re.search(r"\bsemest\w*\b", wording) and re.search( r"\b(?:aktual\w*|teraz|v akom|v ktorom)\b", wording ): return clarification_result( "Aktuálny semester neviem určiť len z roku začiatku štúdia alebo z názvu práce. Potrebný je doložený údaj o aktuálnom štúdiu; môžem vyhľadať evidované práce a ich roky.", "enrollment_not_established", engine=engine, ) unsupported = unsupported_constraint(question) from tools.knowledge.academic import academic_intent if ( unsupported and unsupported != "year_range" and not (unsupported == "work_event_date" and academic_intent(question) == "completion") ): return clarification_result( constraint_message(unsupported), "unsupported_constraint", engine=engine ) try: content_result = deliver_content( question, engine=engine, limit=limit, db_file=db_file, complete=model ) if content_result is not None: return content_result except ContentModelUnavailable: return public_failure({"reason": "model_unavailable", "engine": engine}) except ContentAnswerRejected: return public_failure({"reason": "answer_rejected", "engine": engine}) except Exception: return public_failure({"reason": "content_index_unavailable", "engine": engine}) unsupported = unsupported_constraint(question) if unsupported: return clarification_result( constraint_message(unsupported), "unsupported_constraint", engine=engine ) if engine == "rag": try: with index_snapshot(db_file.parent, db_file=db_file): retrieved = build_rag_context(db_file, question, limit=limit) if retrieved.get("status") == "error": return retrieval_failure(engine) if failure := name_failure(retrieved, engine): return failure question = retrieved.get("resolved_query") or question retrieved = expand_comparison_sources(db_file, question, retrieved) retrieved = prepare_named_content(db_file, question, retrieved) retrieved = compact_rag_payload(retrieved) except Exception: return retrieval_failure(engine) result = generate_verified_rag(question, retrieved, model) context = retrieved elif engine == "graphrag": from tools.answers.question_intent import graph_needs_text from tools.retrieval.work_selector import parse_work_selector if graph_needs_text(question) or parse_work_selector(question).specific: return clarification_result( "Táto otázka vyžaduje textové podklady o zadaní, priebehu alebo výsledkoch práce. Použi /rag s rovnakou otázkou; graf samotný tieto výňatky neposkytuje.", "text_evidence_required", engine=engine, ) try: retrieved = search_graph(question, limit=limit) if retrieved.get("status") == "error": return retrieval_failure(engine) if failure := name_failure(retrieved, engine): return failure question = retrieved.get("resolved_query") or question context = graph_context(retrieved) except Exception: return retrieval_failure(engine) if context.get("clarification"): return with_name_resolution( clarification_result( context["clarification"], context.get("reason"), engine=engine, index_version=context.get("index_version"), ), retrieved, ) prompt = ( Path(__file__).resolve().parents[1] / "answers/graph_answer_prompt.txt" ).read_text() generated = generate_checked_answer(question, context, prompt, model) if generated.get("accepted_for_delivery"): result = { **generated["answer"], "accepted_for_delivery": True, "reason": None, "index_version": context.get("index_version"), "complete": context.get("complete"), "truncated": context.get("truncated"), "truncation_reasons": context.get("truncation_reasons"), "attempts": generated["attempts"], } else: reason = generated.get("reason", "answer_rejected") result = public_failure( { "reason": reason, "index_version": context.get("index_version"), "attempts": generated["attempts"], } ) else: raise ValueError("Unsupported answer engine") if result.get("status") == "error" or result.get("accepted_for_delivery") is not True: result = public_failure(result) result = with_name_resolution(result, retrieved) return { **result, "engine": engine, **({"retrieval_context": context} if include_diagnostics else {}), } finally: _capacity.release()