"""Configuration for the scheduler container. All settings flow from env vars (prefix ``SCHED_``) so deployment can tune intervals + feed lists without rebuilds. Reasonable defaults for a typical DIDI deployment are baked in, but ``brain_api_url`` is required to fail-fast if misconfigured. """ from __future__ import annotations from functools import lru_cache from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class SchedulerSettings(BaseSettings): """Top-level scheduler config — validated at startup.""" model_config = SettingsConfigDict( env_file=Path(__file__).parent.parent / ".env", env_file_encoding="utf-8", extra="ignore", env_prefix="SCHED_", ) # ---- Brain API target ------------------------------------------------- brain_api_url: str = Field( default="http://brain-api:8090", description="Base URL of the brain HTTP API. Docker DNS by default.", ) brain_api_timeout_s: float = Field(default=120.0) # ---- Feeder (Pilon 4) ------------------------------------------------- feeder_enabled: bool = Field(default=True) # Intervals (seconds) per volatility — controls how often we poll RSS # feeds in each tier. feeder_volatile_interval_s: int = Field(default=900) # 15 min feeder_evolving_interval_s: int = Field(default=21600) # 6 h feeder_stable_interval_s: int = Field(default=86400) # 24 h feeder_max_items_per_run: int = Field(default=20) feeder_min_published_age_s: int = Field(default=3600) # skip <1h items feeder_max_published_age_s: int = Field(default=259200) # skip >3d items # ---- Auditor (Pilon 5) ------------------------------------------------ auditor_enabled: bool = Field(default=True) auditor_interval_s: int = Field(default=86400) # 24h sweep auditor_batch_limit: int = Field(default=200) auditor_min_age_hours: float = Field( default=24.0, description=( "Don't audit atoms younger than this — they were just written, " "judging them yields no new signal." ), ) # ---- Breaking-news watcher (Pilon 9) ---------------------------------- watcher_enabled: bool = Field(default=True) watcher_poll_interval_s: int = Field(default=300) # 5 min watcher_max_items_per_run: int = Field(default=10) watcher_min_published_age_s: int = Field(default=60) # >1min old watcher_max_published_age_s: int = Field(default=3600) # <1h old # ---- Logging ---------------------------------------------------------- log_level: str = Field(default="INFO") log_json: bool = Field(default=False) @lru_cache(maxsize=1) def get_settings() -> SchedulerSettings: return SchedulerSettings() settings = get_settings() # ---------------------------------------------------------------------------- # Default feed lists per volatility — overridable via env (FEED_VOLATILE_URLS, # FEED_EVOLVING_URLS, FEED_STABLE_URLS, FEED_BREAKING_URLS as # comma-separated strings) for ops flexibility. # ---------------------------------------------------------------------------- # Volatile: fast-moving news (war, breaking events, daily politics). # Note: Reuters retired their public RSS feeds. Operators with paid Reuters # access can add their feed URLs via the FEED_VOLATILE_URLS env override. DEFAULT_VOLATILE_FEEDS: list[str] = [ "https://feeds.bbci.co.uk/news/world/rss.xml", "https://www.aljazeera.com/xml/rss/all.xml", "https://feeds.npr.org/1004/rss.xml", "https://www.theguardian.com/world/rss", # Romanian "https://www.digi24.ro/rss", "https://www.hotnews.ro/rss", "https://www.g4media.ro/feed", ] # Evolving: weekly-stable topics (economy, climate, science debates). DEFAULT_EVOLVING_FEEDS: list[str] = [ "https://feeds.bbci.co.uk/news/business/rss.xml", "https://feeds.bbci.co.uk/news/health/rss.xml", "https://rss.nytimes.com/services/xml/rss/nyt/Science.xml", ] # Stable: long-cycle topics (basic science, history, settled facts). DEFAULT_STABLE_FEEDS: list[str] = [ "https://feeds.bbci.co.uk/news/science_and_environment/rss.xml", ] # Breaking: small set polled fast (5 min) — only the highest-credibility # real-time wires. Each item runs through the LLM classifier to decide # which topics/entities to invalidate caches for. DEFAULT_BREAKING_FEEDS: list[str] = [ "https://www.theguardian.com/world/rss", "https://feeds.bbci.co.uk/news/world/rss.xml", "https://www.aljazeera.com/xml/rss/all.xml", ] def parse_csv(value: str | None, default: list[str]) -> list[str]: """Parse a comma-separated env var into a list, falling back to default.""" if not value: return list(default) return [v.strip() for v in value.split(",") if v.strip()]