"""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.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, context_token: str | None = None, ) -> dict: from tools.answers.reference_links import checked_repository_links from tools.answers.conversation import resolve_question, issue_context, ContextError from tools.answers.question_plan import planning_catalog,QuestionPlan from tools.knowledge.store import load_index from tools.sync.index_state import read_version from tools.answers.delivery_contract import diagnose catalog=[];index=None;version=None try: with index_snapshot(db_file.parent,db_file=db_file): version=read_version(db_file.parent) catalog=planning_catalog(db_file) index=load_index(db_file.parent,version) except (OSError,ValueError,RuntimeError): if context_token: return diagnose(public_failure({'reason':'stale_context','engine':engine}),question,engine) try: resolved=resolve_question(question,token=context_token,catalog=catalog,index=index,version=version) except ContextError as error: return diagnose(public_failure({'reason':str(error),'engine':engine}),question,engine) if resolved.clarification: result=clarification_result(resolved.clarification,resolved.reason,engine=engine,index_version=version) else: result = _deliver_answer( resolved.query, engine=engine, limit=limit, complete=complete, db_file=db_file, include_diagnostics=include_diagnostics, meeting=resolved.meeting, expected_version=version if context_token else None, ) plan_data=result.pop('_delivery_plan',None) facts=result.pop('_delivery_facts',[]) result=checked_repository_links(result) from tools.answers.notices import clean_answer_notices result=clean_answer_notices(result) if plan_data: from tools.answers.delivery_contract import enforce result=enforce(question,engine,QuestionPlan.model_validate(plan_data),result,index=index,facts=facts,complete=complete) else:result=diagnose(result,question,engine) if resolved.query!=question.strip():result['resolved_query']=resolved.query conversation=issue_context(resolved.query,result,catalog,index,version) if conversation:result['conversation_context']=conversation return result def _deliver_answer( question: str, *, engine: str = "rag", limit: int = 5, complete=None, db_file: Path = DB_FILE, include_diagnostics: bool = False, meeting=None, expected_version=None, ) -> 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_plan import (build_question_plan, planning_catalog, QuestionPlan, PlanMeaning, PlanRejected, PlanModelUnavailable) from tools.knowledge.query import deliver_content, ContentModelUnavailable, ContentAnswerRejected try: with index_snapshot(db_file.parent, db_file=db_file) as planned_version: if expected_version is not None and expected_version!=planned_version: return public_failure({'reason':'stale_context','engine':engine}) catalog = planning_catalog(db_file) plan_model = complete or (lambda messages: complete_with_configured_model( messages, output_schema=QuestionPlan.model_json_schema())) verify_model = complete or (lambda messages: complete_with_configured_model( messages, output_schema=PlanMeaning.model_json_schema())) plan = build_question_plan(question, catalog, complete=plan_model,verify_complete=verify_model,index_version=planned_version,meeting=meeting) except PlanModelUnavailable: return public_failure({'reason':'model_unavailable','engine':engine}) except PlanRejected: return public_failure({'reason':'answer_rejected','engine':engine}) except Exception: return retrieval_failure(engine) if plan.clarification: value=clarification_result(plan.clarification,plan.reason,engine=engine,index_version=planned_version) if plan.resolution.get('status')=='no_answer' and plan.reason==plan.resolution.get('reason'):value['status']='no_answer' value['name_resolution']=plan.resolution if include_diagnostics:value['question_plan']=plan.model_dump() return value def planned(value, facts=()): return {**value,'_delivery_plan':plan.spec.model_dump(),'_delivery_facts':list(facts)} question = plan.spec.question try: content_result = deliver_content( question, engine=engine, limit=limit, db_file=db_file, complete=model, plan=plan, include_diagnostics=include_diagnostics, ) if content_result is not None: from tools.answers.synthesis import synthesize from tools.answers.recommendations import generate render=generate if plan.spec.intent=='proposal' else synthesize content_result = render( content_result, plan.spec, complete=complete, known_people=[person['name'] for person in catalog if person.get('kind') != 'Document'], diagnostics=include_diagnostics, ) if include_diagnostics:content_result["question_plan"]=plan.model_dump() return planned(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}) if engine == "rag": try: with index_snapshot(db_file.parent, db_file=db_file): retrieved = build_rag_context(db_file, question, limit=limit, question_plan=plan.spec) if retrieved.get("status") == "error": return retrieval_failure(engine) if failure := name_failure(retrieved, engine): return failure question = retrieved.get("resolved_query") or question if planned_version is not None and retrieved.get("index_version") != planned_version: return retrieval_failure(engine) retrieved["question_plan"] = plan.spec.model_dump() retrieved["name_resolution"] = plan.resolution 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": if plan.spec.intent != "metadata": 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, question_plan=plan.spec) if retrieved.get("status") == "error": return retrieval_failure(engine) if failure := name_failure(retrieved, engine): return failure question = retrieved.get("resolved_query") or question if planned_version is not None and retrieved.get("index_version") != planned_version: return retrieval_failure(engine) retrieved["name_resolution"] = plan.resolution 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 planned({ **result, "engine": engine, **({"retrieval_context": context, "question_plan": plan.model_dump()} if include_diagnostics else {}), },context.get('facts',[])) finally: _capacity.release()