"""Configuration for the EPUB search app.""" from __future__ import annotations from dataclasses import dataclass from os import getenv def getenv_bool(name: str, *, default: bool) -> bool: """Read a boolean environment variable with a default fallback.""" value = getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} def getenv_int(name: str, *, default: int) -> int: """Read an integer environment variable with a default fallback.""" value = getenv(name) if value is None or not value.strip(): return default return int(value) @dataclass(frozen=True) class RerankConfig: """vLLM reranker settings.""" enabled: bool = False base_url: str = "http://192.168.90.25:8001" model: str = "qwen3-reranker-06b" candidates: int = 24 timeout_seconds: float = 30.0 @dataclass(frozen=True) class EbookSearchConfig: """Runtime settings for EPUB search.""" rerank: RerankConfig top_k: int = 12 library_paths: tuple[str, ...] = () vllm_base_url: str = "https://ollama.com/v1" vllm_api_key: str = "not-needed" chat_model: str = "deepseek-v4-flash" answer_enabled: bool = True embedding_base_url: str = "http://192.168.90.25:8000/v1" embedding_api_key: str = "not-needed" embedding_model: str = "qwen3-embedding-0.6b" embedding_batch_size: int = 32 bm25_index_dir: str = ".ebook_search_bm25" bm25_refresh_delay_seconds: int = 60 def load_rerank_config() -> RerankConfig: """Load reranker config from environment variables.""" return RerankConfig( enabled=getenv_bool("EBOOK_SEARCH_RERANK_ENABLED", default=False), base_url=getenv("EBOOK_SEARCH_RERANK_BASE_URL", "http://192.168.90.25:8001"), model=getenv("EBOOK_SEARCH_RERANK_MODEL", "qwen3-reranker-06b"), candidates=getenv_int("EBOOK_SEARCH_RERANK_CANDIDATES", default=24), timeout_seconds=float(getenv_int("EBOOK_SEARCH_RERANK_TIMEOUT_SECONDS", default=30)), ) def load_config() -> EbookSearchConfig: """Load EPUB search config from environment variables.""" return EbookSearchConfig( rerank=load_rerank_config(), top_k=getenv_int("EBOOK_SEARCH_TOP_K", default=12), library_paths=library_paths_from_env(), vllm_base_url=getenv("EBOOK_SEARCH_VLLM_BASE_URL", "https://ollama.com/v1"), vllm_api_key=getenv("EBOOK_SEARCH_VLLM_API_KEY") or getenv("OLLAMA_API_KEY") or "not-needed", chat_model=getenv("EBOOK_SEARCH_CHAT_MODEL", "deepseek-v4-flash"), answer_enabled=getenv_bool("EBOOK_SEARCH_ANSWER_ENABLED", default=True), embedding_base_url=getenv("EBOOK_SEARCH_EMBEDDING_BASE_URL", "http://192.168.90.25:8000/v1"), embedding_api_key=getenv("EBOOK_SEARCH_EMBEDDING_API_KEY", "not-needed"), embedding_model=normalize_embedding_model(), embedding_batch_size=getenv_int("EBOOK_SEARCH_EMBEDDING_BATCH_SIZE", default=32), bm25_index_dir=getenv("EBOOK_SEARCH_BM25_INDEX_DIR", ".ebook_search_bm25"), bm25_refresh_delay_seconds=getenv_int("EBOOK_SEARCH_BM25_REFRESH_DELAY_SECONDS", default=60), ) def normalize_embedding_model(default: str = "qwen3-embedding-0.6b") -> str: """Normalize supported embedding aliases to provider model names.""" aliases = { "Qwen3-Embedding-0.6B": "qwen3-embedding-0.6b", "Qwen3-Embedding-4B": "qwen3-embedding-4b", "Qwen3-Embedding-8B": "qwen3-embedding-8b", "Qwen/Qwen3-Embedding-0.6B": "qwen3-embedding-0.6b", "Qwen/Qwen3-Embedding-4B": "qwen3-embedding-4b", "Qwen/Qwen3-Embedding-8B": "qwen3-embedding-8b", "qwen3-embedding:0.6b": "qwen3-embedding-0.6b", "qwen3-embedding:4b": "qwen3-embedding-4b", "qwen3-embedding:8b": "qwen3-embedding-8b", "qwen3-embedding-0.6b": "qwen3-embedding-0.6b", "qwen3-embedding-4b": "qwen3-embedding-4b", "qwen3-embedding-8b": "qwen3-embedding-8b", } model = getenv("EBOOK_SEARCH_EMBEDDING_MODEL", default) standard_model = aliases.get(model) if standard_model is None: error = f"Embedding model {model} is not supported. Supported models are {aliases.keys()}" raise ValueError(error) return standard_model def library_paths_from_env() -> tuple[str, ...]: """Read configured EPUB library paths from the environment.""" value = getenv("EBOOK_SEARCH_LIBRARY_PATHS") if value is None: return () return tuple(path for path in value.split(":") if path)