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
184 lines
9.3 KiB
Python
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()
|