"""Shared machine-readable fields for RAG and GraphRAG tool responses. Completeness describes retrieval, not correctness or answerability. RAG is a ranked selection and cannot promise exhaustive coverage of the wiki. """ from __future__ import annotations import sqlite3 from typing import Any, Literal, Sequence from neo4j.exceptions import Neo4jError, ServiceUnavailable, SessionExpired from pydantic import BaseModel, ConfigDict, Field RetrievalStatus = Literal["ok", "no_answer", "ambiguous", "error"] class RetrievalSource(BaseModel): model_config = ConfigDict(extra="allow") source_id: str document_path: str source_url: str | None = None title: str | None = None class RetrievalResponse(BaseModel): status: RetrievalStatus = "ok" reason: str | None = None sources: list[RetrievalSource] = Field(default_factory=list) index_version: dict[str, Any] | None = None complete: bool | None = Field( default=None, description=( "Úplnosť vyhľadania v rozsahu nástroja: true=úplné, false=neúplné, " "null=nemožno zaručiť. Nevyjadruje pravdivosť odpovede." ), ) truncated: bool = False truncation_reasons: list[str] = Field(default_factory=list) class RetrievalErrorResponse(RetrievalResponse): status: Literal["error"] = "error" detail: Any = None def source_references(rows: list[dict]) -> list[dict]: """Stable citation shape; never invent a URL or a document title.""" return [ { "source_id": f"S{number}", "document_path": row["document_path"], "source_url": row.get("source_url"), "title": row.get("title"), } for number, row in enumerate(rows, 1) ] def result_fields( *, status: RetrievalStatus, sources: list[dict], reason: str | None = None, complete: bool | None = None, truncation_reasons: Sequence[str] = (), index_version: dict | None = None, ) -> dict: reasons = list(dict.fromkeys(truncation_reasons)) return { "status": status, "reason": reason, "sources": sources, "index_version": index_version, "complete": False if reasons or status in {"error", "ambiguous"} else complete, "truncated": bool(reasons), "truncation_reasons": reasons, } def error_reason(error: Exception) -> str: if isinstance(error, ValueError): return "invalid_query" if isinstance( error, ( FileNotFoundError, sqlite3.Error, RuntimeError, Neo4jError, ServiceUnavailable, SessionExpired, ), ): return "retrieval_unavailable" return "internal_error" def error_result(error: Exception | None = None, *, reason: str | None = None) -> dict: # Raw exception messages can contain credentials, SQL or private paths. return result_fields( status="error", sources=[], complete=False, reason=reason or error_reason(error) )