rebase
This commit is contained in:
125
app/config.py
Normal file
125
app/config.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Application configuration loaded from environment variables / settings DB.
|
||||
|
||||
Settings are layered:
|
||||
1. Defaults defined in this module.
|
||||
2. Overrides from environment variables (or `.env` file).
|
||||
3. Runtime overrides from the `settings` table (loaded on startup and cached).
|
||||
|
||||
The Settings class below is a Pydantic-Settings model — it only handles (1) and (2).
|
||||
The runtime DB overrides are managed by `app.core.settings_service`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Layered configuration for the AI-RPG backend."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
# === Application ===
|
||||
app_name: str = "AI-RPG"
|
||||
app_version: str = "1.0.0"
|
||||
debug: bool = False
|
||||
log_level: str = "INFO"
|
||||
secret_key: str = "change-me-in-production-please-32-bytes-long"
|
||||
jwt_algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 60 * 24 # 24 hours
|
||||
refresh_token_expire_minutes: int = 60 * 24 * 7 # 7 days
|
||||
cors_origins: list[str] = Field(default_factory=lambda: ["*"])
|
||||
|
||||
# === Admin setup ===
|
||||
admin_setup_token: str = "" # if empty, will be auto-generated and stored in DB
|
||||
|
||||
# === Database ===
|
||||
database_url: str = "postgresql+asyncpg://airpg:airpg@localhost:5432/airpg"
|
||||
database_url_sync: str = "postgresql+psycopg2://airpg:airpg@localhost:5432/airpg"
|
||||
db_pool_size: int = 10
|
||||
db_max_overflow: int = 20
|
||||
db_echo: bool = False
|
||||
|
||||
# === Qdrant ===
|
||||
qdrant_url: str = "http://localhost:6333"
|
||||
qdrant_api_key: str = ""
|
||||
qdrant_collection_prefix: str = ""
|
||||
qdrant_timeout: float = 30.0
|
||||
|
||||
# === LLM (defaults; runtime overrides in `settings` table) ===
|
||||
llm_api_url: str = "http://localhost:11434/v1"
|
||||
llm_api_key: str = ""
|
||||
llm_model: str = "qwen2.5-7b-instruct"
|
||||
llm_temperature_orchestrator: float = 0.7
|
||||
llm_temperature_writer: float = 0.85
|
||||
llm_max_tokens: int = 2048
|
||||
llm_timeout_seconds: int = 60
|
||||
|
||||
# === Embeddings ===
|
||||
embeddings_provider: str = "offline_hash" # "offline_hash" | "openai"
|
||||
embeddings_api_url: str = ""
|
||||
embeddings_api_key: str = ""
|
||||
embeddings_model: str = "text-embedding-3-small"
|
||||
embeddings_dimension: int = 256 # for offline_hash; will be probed for openai
|
||||
embeddings_timeout_seconds: int = 30
|
||||
embeddings_batch_size: int = 32
|
||||
embeddings_cache_ttl_seconds: int = 300
|
||||
embeddings_max_text_chars: int = 4000
|
||||
|
||||
# === Context manager ===
|
||||
context_guaranteed_messages: int = 10
|
||||
context_compression_threshold_messages: int = 20
|
||||
context_compression_threshold_tokens: int = 6000
|
||||
context_scene_text_truncate_tokens: int = 500
|
||||
context_auto_rag_on_entity_mention: bool = False
|
||||
context_safety_margin_tokens: int = 500
|
||||
llm_context_window_tokens: int = 8192 # for tokenizer-based budgeting
|
||||
|
||||
# === Game ===
|
||||
game_deferred_triggers_enabled: bool = True
|
||||
game_max_substeps_per_iteration: int = 8
|
||||
game_max_suggested_actions: int = 3
|
||||
|
||||
# === UI ===
|
||||
ui_page_title: str = "AI-RPG"
|
||||
ui_favicon_url: str = "/icon.png"
|
||||
ui_logo_url: str = "/icon.png"
|
||||
ui_og_image_url: str = ""
|
||||
|
||||
# === Storage ===
|
||||
data_dir: str = "/home/z/my-project/ai-rpg/data"
|
||||
assets_dir: str = "" # computed in __init__
|
||||
max_upload_size_bytes: int = 1024 * 1024 # 1 MB
|
||||
|
||||
def __init__(self, **values):
|
||||
super().__init__(**values)
|
||||
if not self.assets_dir:
|
||||
self.assets_dir = str(Path(self.data_dir) / "assets")
|
||||
|
||||
@field_validator("cors_origins", mode="before")
|
||||
@classmethod
|
||||
def _split_cors(cls, v):
|
||||
if isinstance(v, str):
|
||||
return [item.strip() for item in v.split(",") if item.strip()]
|
||||
return v
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
"""Cached settings instance. Use as the single source of truth for env config."""
|
||||
return Settings()
|
||||
|
||||
|
||||
def reload_settings() -> Settings:
|
||||
"""Force reload settings (used in tests)."""
|
||||
get_settings.cache_clear()
|
||||
return get_settings()
|
||||
Reference in New Issue
Block a user