Files
library-desk/src/config.py
T
jpmschweitzerandClaude Fable 5 bc68d3b691 fix: make runtime prefetch task registration executable end-to-end
SchedulerClient.register_volatile_fetch (consolidation's prefetch
routing) registered tasks that were dead on arrival:
- JSON body stored under 'body', which rest_api_executor ignores
  (it only reads config['payload'])
- no auth block, so the scheduled POST to /volatile/fetch would 401
  against library-desk's verify_api_key
- user placed in the body while /volatile/fetch endpoints require it
  as a query parameter (RequiredUserQuery) - would 422 regardless

The task config now carries user in the URL query string (encoded),
an empty payload, and auth {type: bearer, token: ${LIBRARY_API_KEY}}
substituted Scheduler-side (never stored raw).

SchedulerClient also sent no Authorization to the Scheduler API itself,
so registration 401'd silently at consolidation time; it now sends
Bearer auth from the new scheduler_api_key setting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
2026-07-14 15:16:09 +02:00

184 lines
9.3 KiB
Python

"""
Application configuration using Pydantic Settings.
Following best practices: modular settings, environment-based config.
"""
from functools import lru_cache
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
# Read version from pyproject.toml
try:
import tomllib
_pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
with open(_pyproject_path, "rb") as f:
_pyproject = tomllib.load(f)
__version__ = _pyproject["project"]["version"]
except Exception:
__version__ = "0.0.0" # Fallback if pyproject.toml not found
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# API Configuration
library_api_key: str = Field(..., description="API key for authentication")
# Neo4j Configuration
neo4j_uri: str = Field(default="bolt://neo4j:7687", description="Neo4j Bolt URI")
neo4j_user: str = Field(default="neo4j", description="Neo4j username")
neo4j_password: str = Field(..., description="Neo4j password")
# Qdrant Configuration
qdrant_host: str = Field(default="qdrant", description="Qdrant host")
qdrant_port: int = Field(default=6333, description="Qdrant port")
qdrant_timeout: int = Field(default=30, ge=1, le=300, description="Qdrant client timeout in seconds")
# Wiki.js Configuration
wikijs_url: str = Field(default="http://wiki:3000", description="Wiki.js URL")
wiki_graphql_api: str = Field(default="", description="Wiki.js GraphQL API token (optional - API may be open)")
# Legacy auth fields - kept for backwards compatibility but deprecated
wikijs_username: str = Field(default="", description="Wiki.js username (deprecated, use wiki_graphql_api)")
wikijs_password: str = Field(default="", description="Wiki.js password (deprecated, use wiki_graphql_api)")
# Wiki.js Database Configuration (for change listener)
wikijs_db_host: str = Field(default="postgres-shared", description="Wiki.js PostgreSQL host")
wikijs_db_port: int = Field(default=5432, description="Wiki.js PostgreSQL port")
wikijs_db_name: str = Field(default="library", description="Wiki.js database name")
wikijs_db_user: str = Field(default="library_desk_listener", description="Wiki.js database user (read-only)")
wikijs_db_password: str = Field(..., description="Wiki.js database password")
wikijs_change_listener_debounce_seconds: int = Field(
default=5,
ge=1,
le=60,
description="Debounce duration to prevent processing duplicate notifications"
)
# SearXNG Configuration
searxng_url: str = Field(default="http://searxng:8080", description="SearXNG URL")
# Ollama Configuration
ollama_url: str = Field(default="http://ollama:11434", description="Ollama URL")
# Named ollama_llm_model (env: OLLAMA_LLM_MODEL) to avoid collision with the
# OLLAMA_MODEL container env var, which is used for the embedding model.
ollama_llm_model: str = Field(default="gemma4:e2b", description="Ollama LLM model for generation (keyword extraction, re-ranking, consolidation)")
ollama_embedding_model: str = Field(default="nomic-embed-text", description="Ollama embedding model")
# HybridRAG Configuration
reranker_enabled: bool = Field(default=True, description="Enable LLM re-ranking")
hybrid_rag_vector_limit: int = Field(default=10, ge=1, le=50, description="Vector search limit")
hybrid_rag_graph_limit: int = Field(default=10, ge=1, le=50, description="Graph search limit")
hybrid_rag_web_limit: int = Field(default=5, ge=1, le=20, description="Web search limit")
vector_similarity_threshold: float = Field(default=0.7, ge=0.0, le=1.0, description="Minimum similarity score for vector results")
# Entity Linking Fuzzy Matching Configuration
entity_linking_min_confidence: float = Field(default=0.70, ge=0.0, le=1.0, description="Minimum confidence for entity-document matching")
entity_linking_min_entity_length: int = Field(default=5, ge=1, le=50, description="Minimum entity name length for matching")
entity_linking_min_containment_ratio: float = Field(default=0.30, ge=0.0, le=1.0, description="Minimum containment ratio for substring matching")
entity_linking_min_token_overlap: float = Field(default=0.60, ge=0.0, le=1.0, description="Minimum token overlap ratio for matching")
# Redis Configuration (for job tracking - separate DB from wiki)
redis_host: str = Field(default="redis-shared", description="Redis host")
redis_port: int = Field(default=6379, description="Redis port")
redis_db: int = Field(default=4, description="Redis database number (4 for library-desk jobs)")
# Application
app_name: str = Field(default="Library Desk", description="Application name")
app_version: str = Field(default=__version__, description="Application version")
debug: bool = Field(default=False, description="Debug mode")
# CORS: comma-separated list of allowed browser origins. The default "*"
# is only acceptable because allow_credentials is disabled (see main.py).
cors_allow_origins: str = Field(
default="*",
description="Comma-separated CORS allowed origins (credentials are never allowed)"
)
@property
def cors_allow_origins_list(self) -> list[str]:
"""cors_allow_origins parsed into a list for CORSMiddleware."""
return [o.strip() for o in self.cors_allow_origins.split(",") if o.strip()]
# RAG Search Configuration
search_cache_ttl: int = Field(default=300, ge=0, le=3600, description="Search cache TTL in seconds")
search_timeout: int = Field(default=10, ge=1, le=60, description="SearXNG timeout in seconds")
search_default_limit: int = Field(default=10, ge=1, le=20, description="Default number of search results")
# Content Extraction Configuration
content_extraction_timeout: int = Field(default=5, ge=1, le=30, description="Trafilatura per-URL timeout in seconds")
content_max_length: int = Field(default=2000, ge=500, le=10000, description="Max extracted content length per result")
# Paperless-ngx Configuration
paperless_url: str = Field(default="http://paperless:8000", description="Paperless-ngx URL")
paperless_token: str = Field(default="", description="Paperless-ngx API token")
paperless_timeout: int = Field(default=30, ge=5, le=120, description="Paperless API timeout in seconds")
# Document Store Configuration
document_store_enabled: bool = Field(default=True, description="Enable document store feature")
document_catalog_path_prefix: str = Field(default="docs", description="Wiki path prefix for catalog pages")
# Volatile Cache Configuration
volatile_cache_enabled: bool = Field(default=True, description="Enable volatile cache feature")
volatile_default_ttl: int = Field(default=3600, ge=60, le=86400, description="Default TTL in seconds")
volatile_weather_ttl: int = Field(default=1800, ge=60, le=7200, description="Weather data TTL in seconds")
volatile_news_ttl: int = Field(default=7200, ge=300, le=86400, description="News data TTL in seconds")
volatile_financial_ttl: int = Field(default=300, ge=60, le=3600, description="Financial data TTL in seconds")
# Maintenance Configuration
maintenance_orphan_cleanup_enabled: bool = Field(default=True, description="Enable automatic orphan cleanup")
maintenance_cleanup_batch_size: int = Field(default=100, ge=10, le=1000, description="Cleanup batch size")
# Central Settings Database (Tatlock-wide)
system_settings_host: str = Field(default="postgres-shared", description="System settings PostgreSQL host")
system_settings_port: int = Field(default=5432, description="System settings PostgreSQL port")
system_settings_db: str = Field(default="system_settings", description="System settings database name")
system_settings_user: str = Field(default="settings", description="System settings database user")
system_settings_password: str = Field(default="", description="System settings database password")
# Scheduler Service
scheduler_url: str = Field(default="http://scheduler:8090", description="Scheduler service URL")
scheduler_api_key: str = Field(
default="",
description=(
"Bearer key for the Scheduler's auth-guarded task-management API; "
"required for runtime prefetch task registration"
),
)
@property
def qdrant_url(self) -> str:
"""Computed Qdrant URL."""
return f"http://{self.qdrant_host}:{self.qdrant_port}"
@property
def redis_url(self) -> str:
"""Computed Redis URL."""
return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
@property
def system_settings_dsn(self) -> str:
"""Computed System Settings PostgreSQL DSN."""
if not self.system_settings_password:
return ""
return (
f"postgresql://{self.system_settings_user}:{self.system_settings_password}"
f"@{self.system_settings_host}:{self.system_settings_port}/{self.system_settings_db}"
)
@lru_cache
def get_settings() -> Settings:
"""
Get cached settings instance.
Uses lru_cache to ensure single instance across app.
"""
return Settings()