from functools import lru_cache from pathlib import Path from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict DEPRECATED_VISION_MODELS: dict[str, str] = { "google/gemini-2.0-flash-lite-001": "google/gemini-2.5-flash-lite", "google/gemini-2.0-flash-lite": "google/gemini-2.5-flash-lite", } def resolve_vision_model(model: str) -> str: stripped = model.strip() return DEPRECATED_VISION_MODELS.get(stripped, stripped) class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=(".env", "../.env"), env_file_encoding="utf-8", extra="ignore", ) host: str = "0.0.0.0" port: int = 8080 openrouter_api_key: str = "" openrouter_model: str = "deepseek/deepseek-chat" openrouter_base_url: str = "https://openrouter.ai/api/v1" # Отдельная модель для JSON-задач (память, фитнес). Пусто = та же, что OPENROUTER_MODEL. memory_extract_model: str = "" # Некоторые модели (reasoning / без function calling) — выключить tools. openrouter_tools_enabled: bool = True # DeepSeek V4 / reasoning: none | low | medium | high | xhigh. none = без thinking. openrouter_reasoning_effort: str = "none" openrouter_vision_model: str = "google/gemini-2.5-flash-lite" vision_max_edge_px: int = 1280 vision_jpeg_quality: int = 85 vision_debug_enabled: bool = True vision_max_images: int = 8 uploads_dir: str = "./data/uploads" @field_validator("openrouter_vision_model") @classmethod def migrate_vision_model(cls, value: str) -> str: return resolve_vision_model(value) database_url: str = "sqlite:///./data/assistant.db" cors_origins: str = "http://localhost:5173,http://localhost:8080,http://localhost:3000" system_prompt_path: str = "./prompts/assistant.md" memory_auto_extract: bool = True default_user_username: str = "owner" default_user_display_name: str = "" default_api_token: str = "" auth_required: bool = True qdrant_url: str = "http://qdrant:6333" embedding_model: str = "openai/text-embedding-3-small" rag_enabled: bool = False rag_top_k: int = 8 memory_facts_in_context: int = 8 # Taiga/Gitea on host (not in Docker) — use host.docker.internal from container taiga_base_url: str = "http://host.docker.internal:9000" taiga_username: str = "" taiga_password: str = "" taiga_public_url: str = "https://taiga.grigowashere.ru" gitea_base_url: str = "http://host.docker.internal:3000" gitea_token: str = "" gitea_public_url: str = "https://git.grigowashere.ru" gitea_webhook_secret: str = "" repos_dir: str = "/data/repos" wger_base_url: str = "https://wger.de/api/v2" openfoodfacts_base_url: str = "https://world.openfoodfacts.org" fitness_reminders_enabled: bool = True reminders_enabled: bool = True openmeteo_base_url: str = "http://192.168.1.109:8085" weather_lat: float = 59.9343 weather_lon: float = 30.3351 weather_location_name: str = "Санкт-Петербург" weather_cache_sec: int = 300 weather_forecast_days: int = 7 openmeteo_fallback_url: str = "https://api.open-meteo.com" openmeteo_fallback_on_partial: bool = True news_rss_urls: str = ( "https://habr.com/ru/rss/all/all/," "https://www.reddit.com/r/programming/.rss" ) news_cache_sec: int = 1800 news_max_items: int = 7 morning_digest_enabled: bool = True morning_digest_hour: int = 8 morning_digest_minute: int = 0 comfyui_base_url: str = "http://192.168.1.109:8188" comfyui_enabled: bool = True # Anima split-model (default): set UNET+CLIP+VAE, leave CHECKPOINT empty comfyui_checkpoint: str = "" comfyui_unet: str = "anima-preview3-base.safetensors" comfyui_clip: str = "qwen_3_06b_base.safetensors" comfyui_vae: str = "qwen_image_vae.safetensors" comfyui_style_lora: str = "anima-preview-3-masterpieces-v5.safetensors" comfyui_style_lora_weight: float = 0.7 comfyui_steps: int = 30 comfyui_cfg: float = 4.0 comfyui_sampler: str = "er_sde" comfyui_scheduler: str = "simple" comfyui_width: int = 1024 comfyui_height: int = 720 comfyui_negative_prompt: str = ( "worst quality, low quality, score_1, score_2, score_3, blurry, jpeg artifacts, sepia" ) comfyui_poll_interval_sec: float = 2.0 comfyui_timeout_sec: float = 180.0 comfyui_rofl_enabled: bool = True comfyui_rofl_max_per_day: int = 1 comfyui_rofl_probability: float = 0.15 comfyui_rofl_min_interval_hours: int = 12 generated_media_dir: str = "./data/generated" netdata_base_url: str = "http://host.docker.internal:19999" netdata_public_url: str = "" netdata_alerts_enabled: bool = True netdata_poll_interval_sec: int = 120 rp_chat_base_url: str = "http://host.docker.internal:8201" rp_chat_enabled: bool = True rp_chat_timeout_sec: float = 300.0 @property def cors_origins_list(self) -> list[str]: return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()] @property def taiga_configured(self) -> bool: return bool(self.taiga_username and self.taiga_password) @property def gitea_configured(self) -> bool: return bool(self.gitea_token) @property def news_rss_urls_list(self) -> list[str]: return [u.strip() for u in self.news_rss_urls.split(",") if u.strip()] def load_system_prompt(self) -> str: path = Path(self.system_prompt_path) if path.is_file(): return path.read_text(encoding="utf-8") return "Ты домашний ИИ-ассистент. Общайся на русском." @lru_cache def get_settings() -> Settings: return Settings()