Spaces:
Runtime error
Runtime error
| """ | |
| Pydantic v2 data models for the RAG system. | |
| These models are shared across ingestion, retrieval, generation, the REST API, | |
| and the evaluation harness β single source of truth. | |
| """ | |
| from __future__ import annotations | |
| from datetime import UTC, datetime | |
| from enum import Enum | |
| from typing import Any | |
| from pydantic import BaseModel, Field, model_validator | |
| # ββ Enums βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class DocumentType(str, Enum): | |
| """Supported document source types.""" | |
| PDF = "pdf" | |
| TXT = "txt" | |
| DOCX = "docx" | |
| MARKDOWN = "markdown" | |
| URL = "url" | |
| UNKNOWN = "unknown" | |
| class QueryMode(str, Enum): | |
| """Retrieval strategy modes.""" | |
| DENSE = "dense" # pure vector search | |
| SPARSE = "sparse" # pure BM25 | |
| HYBRID = "hybrid" # dense + BM25 with RRF fusion | |
| # ββ Chunk / Document Models βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ChunkMetadata(BaseModel): | |
| """Rich metadata attached to every stored chunk.""" | |
| source_file: str = Field(..., description="Original filename or URL") | |
| doc_type: DocumentType = Field(default=DocumentType.UNKNOWN) | |
| page_number: int | None = Field(default=None, description="PDF page number (1-indexed)") | |
| chunk_index: int = Field(..., ge=0, description="Position of chunk within its source document") | |
| timestamp_ingested: datetime = Field(default_factory=lambda: datetime.now(UTC)) | |
| word_count: int = Field(..., ge=0) | |
| char_count: int = Field(..., ge=0) | |
| content_hash: str = Field(..., description="SHA-256 of chunk text for deduplication") | |
| section_title: str | None = Field( | |
| default=None, description="Nearest heading above this chunk (if detectable)" | |
| ) | |
| class DocumentChunk(BaseModel): | |
| """A single chunk ready for embedding and storage.""" | |
| text: str = Field(..., min_length=1) | |
| metadata: ChunkMetadata | |
| embedding: list[float] | None = Field(default=None, exclude=True) | |
| def chunk_id(self) -> str: | |
| """Stable ID derived from content hash + chunk index.""" | |
| return f"{self.metadata.content_hash[:16]}-{self.metadata.chunk_index}" | |
| # ββ Ingestion Models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class IngestRequest(BaseModel): | |
| """REST API request body for /ingest.""" | |
| file_path: str = Field(..., description="Absolute or relative path to file, or a URL") | |
| collection: str = Field(default="default", min_length=1, max_length=64) | |
| overwrite: bool = Field( | |
| default=False, description="Re-ingest even if chunk hash already exists" | |
| ) | |
| class IngestResult(BaseModel): | |
| """Result of an ingestion operation.""" | |
| collection: str | |
| source: str | |
| chunks_added: int = Field(ge=0) | |
| duplicates_skipped: int = Field(ge=0) | |
| total_chunks_processed: int = Field(ge=0) | |
| elapsed_seconds: float = Field(ge=0.0) | |
| def check_totals(self) -> IngestResult: | |
| assert self.chunks_added + self.duplicates_skipped == self.total_chunks_processed | |
| return self | |
| # ββ Retrieval Models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class RetrievalResult(BaseModel): | |
| """A single retrieved chunk with scoring information.""" | |
| chunk_text: str | |
| source: str = Field(..., description="Source filename or URL") | |
| similarity_score: float = Field(..., ge=0.0, le=1.0) | |
| rerank_score: float | None = Field( | |
| default=None, description="Cross-encoder score (higher = more relevant)" | |
| ) | |
| chunk_index: int | |
| page_number: int | None = None | |
| section_title: str | None = None | |
| metadata: dict[str, Any] = Field(default_factory=dict) | |
| class RetrievalContext(BaseModel): | |
| """All retrieved chunks for a single query, ready for prompt construction.""" | |
| query: str | |
| results: list[RetrievalResult] = Field(default_factory=list) | |
| query_mode: QueryMode = QueryMode.HYBRID | |
| retrieved_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | |
| expanded_queries: list[str] = Field( | |
| default_factory=list, description="Multi-query expansions used" | |
| ) | |
| hyde_hypothesis: str | None = Field( | |
| default=None, description="HyDE hypothetical document if used" | |
| ) | |
| def is_empty(self) -> bool: | |
| return len(self.results) == 0 | |
| # ββ Query / Response Models βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class QueryRequest(BaseModel): | |
| """REST API request body for /query.""" | |
| question: str = Field(..., min_length=1, max_length=2000) | |
| collection: str = Field(default="default") | |
| top_k: int = Field(default=6, ge=1, le=50) | |
| mode: QueryMode = Field(default=QueryMode.HYBRID) | |
| use_hyde: bool = Field(default=False) | |
| use_multi_query: bool = Field(default=False) | |
| class SourceCitation(BaseModel): | |
| """A source citation returned with every answer.""" | |
| source: str | |
| chunk_index: int | |
| page_number: int | None = None | |
| similarity_score: float | |
| excerpt: str = Field(..., description="First 200 chars of the chunk") | |
| class QueryResponse(BaseModel): | |
| """REST API response for /query β also used internally.""" | |
| question: str | |
| answer: str | |
| sources: list[SourceCitation] = Field(default_factory=list) | |
| tokens_used: int = Field(ge=0) | |
| latency_ms: float = Field(ge=0.0) | |
| collection: str | |
| llm_backend: str | |
| model_used: str | |
| cache_hit: bool = Field(default=False) | |
| retrieval_context: RetrievalContext | None = Field(default=None, exclude=True) | |
| # ββ Collection Models βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class CollectionInfo(BaseModel): | |
| """Metadata about a ChromaDB collection (knowledge base).""" | |
| name: str | |
| document_count: int = Field(ge=0) | |
| created_at: datetime | None = None | |
| embedding_model: str | |
| class CollectionListResponse(BaseModel): | |
| """Response for GET /collections.""" | |
| collections: list[CollectionInfo] | |
| total: int = Field(ge=0) | |
| def set_total(self) -> CollectionListResponse: | |
| self.total = len(self.collections) | |
| return self | |
| class DeleteCollectionResponse(BaseModel): | |
| """Response for DELETE /collection/{name}.""" | |
| name: str | |
| deleted: bool | |
| message: str | |
| # ββ Evaluation Models βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class EvalSample(BaseModel): | |
| """A single (question, expected_answer, relevant_sources) test case.""" | |
| question: str | |
| expected_answer: str | |
| relevant_sources: list[str] = Field( | |
| default_factory=list, description="Filenames that should appear in top-k" | |
| ) | |
| collection: str = Field(default="default") | |
| class EvalResult(BaseModel): | |
| """Evaluation result for a single test case.""" | |
| question: str | |
| generated_answer: str | |
| expected_answer: str | |
| sources_retrieved: list[str] | |
| relevant_sources: list[str] | |
| recall_at_k: float = Field( | |
| ge=0.0, le=1.0, description="Fraction of relevant sources found in top-k" | |
| ) | |
| faithfulness_score: float = Field( | |
| ge=1.0, le=5.0, description="LLM-judged faithfulness score (1-5)" | |
| ) | |
| answer_relevancy: float = Field( | |
| ge=0.0, le=1.0, description="Cosine similarity of answer embedding to question" | |
| ) | |
| latency_ms: float = Field(ge=0.0) | |
| class EvalSummary(BaseModel): | |
| """Aggregate evaluation summary across all test cases.""" | |
| total_samples: int = Field(ge=0) | |
| mean_recall_at_k: float = Field(ge=0.0, le=1.0) | |
| mean_faithfulness: float = Field(ge=1.0, le=5.0) | |
| mean_answer_relevancy: float = Field(ge=0.0, le=1.0) | |
| mean_latency_ms: float = Field(ge=0.0) | |
| results: list[EvalResult] = Field(default_factory=list) | |
| # ββ Cache Models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class CacheEntry(BaseModel): | |
| """A cached query-answer pair with embedding for similarity lookup.""" | |
| question: str | |
| response: QueryResponse | |
| embedding: list[float] | |
| created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) | |
| hit_count: int = Field(default=0, ge=0) | |
| # ββ Error Models ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ErrorResponse(BaseModel): | |
| """Standard error response for the REST API.""" | |
| error: str | |
| detail: str | None = None | |
| code: int | |