Spaces:
Running
Running
| """Application configuration loaded from the environment. | |
| Configuration is read from environment variables (and an optional ``.env`` | |
| file) via ``pydantic-settings``. The loader validates cross-field combinations | |
| that the specs require and fails fast with an actionable message, so a | |
| misconfiguration is caught at startup rather than at the first model call. | |
| See ``docs/04_project_setup.md`` section 3 for the canonical variable list and | |
| ``docs/02_architecture.md`` section 5 for the backend capabilities that drive | |
| the validation rules. | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Any, Literal | |
| from pydantic import Field, ValidationError, model_validator | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| # Backends able to consume an image directly (vision-direct). Ollama is | |
| # text-only and must go through the OCR path first (architecture section 5). | |
| MULTIMODAL_BACKENDS: frozenset[str] = frozenset({"gemini"}) | |
| BackendName = Literal["gemini", "ollama"] | |
| ImageStrategy = Literal["vision_direct", "ocr_then_text"] | |
| class ConfigError(RuntimeError): | |
| """Raised when configuration is missing or internally inconsistent. | |
| Carries a human-readable, actionable message intended to be shown at | |
| startup so the operator can fix the environment and retry. | |
| """ | |
| class Settings(BaseSettings): | |
| """Validated runtime configuration for the extraction agent. | |
| Attributes: | |
| extraction_backend: Which model backend to use ("gemini" | "ollama"). | |
| gemini_api_key: Google AI Studio key; required when using Gemini. | |
| gemini_model: Gemini model identifier (config, never hardcoded). | |
| ollama_host: Base URL of the local Ollama server. | |
| ollama_model: Ollama model identifier. | |
| image_strategy: How images are handled ("vision_direct" | | |
| "ocr_then_text"). vision_direct requires a multimodal backend. | |
| confidence_threshold: Auto-accept threshold in [0, 1]; set to 0.50 from | |
| the SROIE eval (see the field comment and README for the caveat). | |
| inbox_dir: Batch-mode inbox directory. | |
| processed_dir: Destination for accepted documents in batch mode. | |
| review_dir: Destination for documents routed to review. | |
| export_dir: CSV export directory. | |
| db_path: SQLite database path. | |
| """ | |
| model_config = SettingsConfigDict( | |
| env_file=".env", | |
| env_file_encoding="utf-8", | |
| case_sensitive=False, | |
| extra="ignore", | |
| ) | |
| # Backend selection. | |
| extraction_backend: BackendName = "gemini" | |
| # Gemini (free tier). | |
| gemini_api_key: str = "" | |
| gemini_model: str = "gemini-flash-latest" | |
| # Ollama (local). | |
| ollama_host: str = "http://localhost:11434" | |
| ollama_model: str = "qwen2.5:7b" | |
| # Image handling strategy. | |
| image_strategy: ImageStrategy = "vision_direct" | |
| # Routing. Set to 0.50 from the SROIE evaluation (eval/), but this is NOT a | |
| # tuned operating point: the Gemini backend exposes no per-field confidence, | |
| # so score() falls back to a neutral 0.50 prior and document scores are | |
| # structurally capped at 0.50 -- the threshold sweep is effectively binary. | |
| # Auto-accept precision on the critical fields is delivered by the H2/H3 | |
| # arithmetic cross-checks in validation, not by this confidence score. See the | |
| # README results section for the evidence and the known-limitation note. | |
| confidence_threshold: float = Field(default=0.50, ge=0.0, le=1.0) | |
| # Paths (batch mode). | |
| inbox_dir: Path = Path("./data/inbox") | |
| processed_dir: Path = Path("./data/processed") | |
| review_dir: Path = Path("./data/review") | |
| export_dir: Path = Path("./data/exports") | |
| db_path: Path = Path("./data/agent.db") | |
| def _validate_combinations(self) -> "Settings": | |
| """Validate cross-field combinations the specs require. | |
| Returns: | |
| The validated settings instance. | |
| Raises: | |
| ValueError: If the Gemini backend is selected without an API key, | |
| or if vision_direct is paired with a text-only backend. | |
| """ | |
| if self.extraction_backend == "gemini" and not self.gemini_api_key.strip(): | |
| raise ValueError( | |
| "EXTRACTION_BACKEND=gemini requires GEMINI_API_KEY to be set " | |
| "(get a free key from Google AI Studio)." | |
| ) | |
| if ( | |
| self.image_strategy == "vision_direct" | |
| and self.extraction_backend not in MULTIMODAL_BACKENDS | |
| ): | |
| supported = ", ".join(sorted(MULTIMODAL_BACKENDS)) | |
| raise ValueError( | |
| f"IMAGE_STRATEGY=vision_direct requires a multimodal backend " | |
| f"({supported}); EXTRACTION_BACKEND={self.extraction_backend} is " | |
| "text-only -- use IMAGE_STRATEGY=ocr_then_text instead." | |
| ) | |
| return self | |
| def _format_validation_error(exc: ValidationError) -> str: | |
| """Render a pydantic ValidationError into an actionable one-line summary. | |
| Args: | |
| exc: The validation error raised while constructing ``Settings``. | |
| Returns: | |
| A semicolon-joined string of the underlying error messages. | |
| """ | |
| messages: list[str] = [] | |
| for error in exc.errors(): | |
| location = ".".join(str(part) for part in error.get("loc", ())) or "config" | |
| messages.append(f"{location}: {error.get('msg', 'invalid value')}") | |
| return "; ".join(messages) | |
| def load_config(**overrides: Any) -> Settings: | |
| """Load and validate configuration, failing fast on misconfiguration. | |
| Args: | |
| **overrides: Optional field overrides passed directly to ``Settings``; | |
| primarily used by tests to bypass the environment. | |
| Returns: | |
| A validated ``Settings`` instance. | |
| Raises: | |
| ConfigError: If the environment is missing required values or contains | |
| an unsupported combination of settings. | |
| """ | |
| try: | |
| return Settings(**overrides) | |
| except ValidationError as exc: | |
| raise ConfigError(_format_validation_error(exc)) from exc | |