2026-06-19 11:28:04 +03:00
|
|
|
"""Application configuration loaded from environment + DB-backed admin settings."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import secrets
|
|
|
|
|
from functools import lru_cache
|
2026-06-19 11:44:47 +03:00
|
|
|
from typing import Annotated, List
|
2026-06-19 11:28:04 +03:00
|
|
|
|
|
|
|
|
from pydantic import Field, field_validator
|
2026-06-19 11:44:47 +03:00
|
|
|
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
2026-06-19 11:28:04 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
|
|
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False)
|
|
|
|
|
|
|
|
|
|
# Database
|
|
|
|
|
database_url: str = "postgresql+asyncpg://airpg:airpg_secret@localhost:5432/airpg"
|
|
|
|
|
|
|
|
|
|
# Redis
|
|
|
|
|
redis_url: str = "redis://localhost:6379/0"
|
|
|
|
|
|
|
|
|
|
# Qdrant
|
|
|
|
|
qdrant_url: str = "http://localhost:6333"
|
|
|
|
|
|
|
|
|
|
# Auth
|
|
|
|
|
jwt_secret: str = Field(default_factory=lambda: secrets.token_hex(32))
|
|
|
|
|
jwt_algorithm: str = "HS256"
|
|
|
|
|
access_token_expire_minutes: int = 60 * 24 * 7 # 7 days
|
|
|
|
|
|
|
|
|
|
# Admin setup
|
|
|
|
|
# If empty, will be generated at first run and printed to console.
|
|
|
|
|
admin_setup_token: str = ""
|
|
|
|
|
|
|
|
|
|
# CORS
|
2026-06-19 11:44:47 +03:00
|
|
|
# `NoDecode` tells pydantic-settings NOT to JSON-parse the env value, so a
|
|
|
|
|
# plain comma-separated string like "http://localhost:5173,http://localhost:8080"
|
|
|
|
|
# reaches our `@field_validator` intact, which then splits it into a list.
|
|
|
|
|
cors_origins: Annotated[List[str], NoDecode] = Field(
|
|
|
|
|
default_factory=lambda: ["http://localhost:5173"]
|
|
|
|
|
)
|
2026-06-19 11:28:04 +03:00
|
|
|
|
|
|
|
|
@field_validator("cors_origins", mode="before")
|
|
|
|
|
@classmethod
|
|
|
|
|
def _split_origins(cls, v):
|
|
|
|
|
if isinstance(v, str):
|
2026-06-19 11:44:47 +03:00
|
|
|
# Allow both JSON arrays (e.g. '["http://a","http://b"]') and plain
|
|
|
|
|
# comma-separated strings (e.g. "http://a,http://b") from env vars.
|
|
|
|
|
v = v.strip()
|
|
|
|
|
if v.startswith("["):
|
|
|
|
|
import json
|
|
|
|
|
try:
|
|
|
|
|
return [o.strip() for o in json.loads(v) if o.strip()]
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-06-19 11:28:04 +03:00
|
|
|
return [o.strip() for o in v.split(",") if o.strip()]
|
2026-06-19 11:44:47 +03:00
|
|
|
if isinstance(v, (list, tuple)):
|
|
|
|
|
return [str(o).strip() for o in v if str(o).strip()]
|
2026-06-19 11:28:04 +03:00
|
|
|
return v
|
|
|
|
|
|
|
|
|
|
# Logging
|
|
|
|
|
log_level: str = "INFO"
|
|
|
|
|
|
|
|
|
|
# Default LLM (used to seed DB on first run; overridable via admin panel)
|
|
|
|
|
default_llm_base_url: str = "http://localhost:1234/v1"
|
|
|
|
|
default_llm_api_key: str = "dummy"
|
|
|
|
|
default_llm_model: str = "local-model"
|
|
|
|
|
|
|
|
|
|
# Default embeddings / RAG settings (overridable via admin panel)
|
|
|
|
|
# provider="hash" is a deterministic offline fallback (no semantic quality).
|
|
|
|
|
# Switch to "openai" and point embedding.base_url at an OpenAI-compatible /embeddings endpoint
|
|
|
|
|
# for real semantic search.
|
|
|
|
|
default_embedding_provider: str = "hash"
|
|
|
|
|
default_embedding_base_url: str = "" # empty = reuse llm.base_url
|
|
|
|
|
default_embedding_api_key: str = "" # empty = reuse llm.api_key
|
|
|
|
|
default_embedding_model: str = "text-embedding-3-small"
|
|
|
|
|
default_embedding_dim: int = 0 # 0 = auto-probe from endpoint
|
|
|
|
|
default_embedding_request_timeout: int = 60
|
|
|
|
|
|
|
|
|
|
# Context manager defaults (admin-overridable)
|
|
|
|
|
default_recent_messages: int = 10
|
|
|
|
|
default_compress_threshold: int = 20
|
|
|
|
|
default_summary_messages: int = 10
|
|
|
|
|
|
|
|
|
|
# Worker mode flag
|
|
|
|
|
worker_mode: bool = False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_worker(self) -> bool:
|
|
|
|
|
return bool(os.getenv("WORKER_MODE")) or self.worker_mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@lru_cache
|
|
|
|
|
def get_settings() -> Settings:
|
|
|
|
|
return Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
settings = get_settings()
|