210 lines
6.7 KiB
Python
210 lines
6.7 KiB
Python
import secrets
|
|
from pathlib import Path
|
|
from typing import NamedTuple
|
|
|
|
import tomllib
|
|
from pydantic import Field, computed_field, field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
ENV_FILE_PATH = BASE_DIR / ".env"
|
|
toml_path = BASE_DIR / "pyproject.toml"
|
|
|
|
|
|
class ConfigurationError(Exception):
|
|
"""Raised when configuration is invalid."""
|
|
|
|
pass
|
|
|
|
|
|
class ProjectVersions(NamedTuple):
|
|
"""
|
|
Immutable structure for project versions.
|
|
Forces explicit naming and prevents accidental swaps.
|
|
"""
|
|
|
|
librechat: str
|
|
rag: str
|
|
alfred: str
|
|
|
|
|
|
def get_versions_from_toml() -> ProjectVersions:
|
|
"""
|
|
Reads versioning information from pyproject.toml.
|
|
Returns the default value if the file or key is missing.
|
|
"""
|
|
|
|
if not toml_path.exists():
|
|
raise FileNotFoundError(f"pyproject.toml not found: {toml_path}")
|
|
|
|
with open(toml_path, "rb") as f:
|
|
data = tomllib.load(f)
|
|
try:
|
|
return ProjectVersions(
|
|
librechat=data["tool"]["alfred"]["settings"]["librechat_version"],
|
|
rag=data["tool"]["alfred"]["settings"]["rag_version"],
|
|
alfred=data["tool"]["poetry"]["version"],
|
|
)
|
|
except KeyError as e:
|
|
raise KeyError(f"Error: Missing key {e} in pyproject.toml") from e
|
|
|
|
|
|
# Load versions once
|
|
VERSIONS: ProjectVersions = get_versions_from_toml()
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(
|
|
env_file=ENV_FILE_PATH,
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
case_sensitive=False,
|
|
)
|
|
# --- GENERAL SETTINGS ---
|
|
host: str = "0.0.0.0"
|
|
port: int = 3080
|
|
debug_logging: bool = False
|
|
debug_console: bool = False
|
|
data_storage: str = "data"
|
|
librechat_version: str = Field(VERSIONS.librechat, description="Librechat version")
|
|
rag_version: str = Field(VERSIONS.rag, description="RAG engine version")
|
|
alfred_version: str = Field(VERSIONS.alfred, description="Alfred version")
|
|
|
|
# --- CONTEXT SETTINGS ---
|
|
max_history_messages: int = 10
|
|
max_tool_iterations: int = 10
|
|
request_timeout: int = 30
|
|
|
|
# TODO: Finish
|
|
deepseek_base_url: str = "https://api.deepseek.com"
|
|
deepseek_model: str = "deepseek-chat"
|
|
|
|
# --- API KEYS ---
|
|
anthropic_api_key: str | None = Field(None, description="Claude API key")
|
|
deepseek_api_key: str | None = Field(None, description="Deepseek API key")
|
|
google_api_key: str | None = Field(None, description="Gemini API key")
|
|
kimi_api_key: str | None = Field(None, description="Kimi API key")
|
|
openai_api_key: str | None = Field(None, description="ChatGPT API key")
|
|
|
|
# --- SECURITY KEYS ---
|
|
# Generated automatically if not in .env to ensure "Secure by Default"
|
|
jwt_secret: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
|
jwt_refresh_secret: str = Field(default_factory=lambda: secrets.token_urlsafe(32))
|
|
|
|
# We keep these for encryption of keys in MongoDB (AES-256 Hex format)
|
|
creds_key: str = Field(default_factory=lambda: secrets.token_hex(32))
|
|
creds_iv: str = Field(default_factory=lambda: secrets.token_hex(16))
|
|
|
|
# --- SERVICES ---
|
|
qbittorrent_url: str = "http://qbittorrent:16140"
|
|
qbittorrent_username: str = "admin"
|
|
qbittorrent_password: str = Field(default_factory=lambda: secrets.token_urlsafe(16))
|
|
|
|
mongo_host: str = "mongodb"
|
|
mongo_user: str = "alfred"
|
|
mongo_password: str = Field(
|
|
default_factory=lambda: secrets.token_urlsafe(24), repr=False, exclude=True
|
|
)
|
|
mongo_port: int = 27017
|
|
mongo_db_name: str = "alfred"
|
|
|
|
@computed_field(repr=False)
|
|
@property
|
|
def mongo_uri(self) -> str:
|
|
return (
|
|
f"mongodb://{self.mongo_user}:{self.mongo_password}"
|
|
f"@{self.mongo_host}:{self.mongo_port}/{self.mongo_db_name}"
|
|
f"?authSource=admin"
|
|
)
|
|
|
|
postgres_host: str = "vectordb"
|
|
postgres_user: str = "alfred"
|
|
postgres_password: str = Field(
|
|
default_factory=lambda: secrets.token_urlsafe(24), repr=False, exclude=True
|
|
)
|
|
postgres_port: int = 5432
|
|
postgres_db_name: str = "alfred"
|
|
|
|
@computed_field(repr=False)
|
|
@property
|
|
def postgres_uri(self) -> str:
|
|
return (
|
|
f"postgresql://{self.postgres_user}:{self.postgres_password}"
|
|
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db_name}"
|
|
)
|
|
|
|
tmdb_api_key: str | None = Field(None, description="The Movie Database API key")
|
|
tmdb_base_url: str = "https://api.themoviedb.org/3"
|
|
|
|
# --- LLM PICKER & CONFIG ---
|
|
# Providers: 'local', 'deepseek', ...
|
|
default_llm_provider: str = "local"
|
|
ollama_base_url: str = "http://ollama:11434"
|
|
# Models: ...
|
|
ollama_model: str = "llama3.3:latest"
|
|
llm_temperature: float = 0.2
|
|
|
|
# --- RAG ENGINE ---
|
|
rag_enabled: bool = True # TODO: Handle False
|
|
rag_api_url: str = "http://rag_api:8000"
|
|
embeddings_provider: str = "ollama"
|
|
# Models: ...
|
|
embeddings_model: str = "nomic-embed-text"
|
|
|
|
# --- MEILISEARCH ---
|
|
meili_enabled: bool = Field(True, description="Enable meili")
|
|
meili_no_analytics: bool = True
|
|
meili_host: str = "http://meilisearch:7700"
|
|
meili_master_key: str = Field(
|
|
default_factory=lambda: secrets.token_urlsafe(32),
|
|
description="Master key for Meilisearch",
|
|
repr=False,
|
|
)
|
|
|
|
# --- VALIDATORS ---
|
|
@field_validator("llm_temperature")
|
|
@classmethod
|
|
def validate_temperature(cls, v: float) -> float:
|
|
if not 0.0 <= v <= 2.0:
|
|
raise ConfigurationError(
|
|
f"Temperature must be between 0.0 and 2.0, got {v}"
|
|
)
|
|
return v
|
|
|
|
@field_validator("max_tool_iterations")
|
|
@classmethod
|
|
def validate_max_iterations(cls, v: int) -> int:
|
|
if not 1 <= v <= 20:
|
|
raise ConfigurationError(
|
|
f"max_tool_iterations must be between 1 and 50, got {v}"
|
|
)
|
|
return v
|
|
|
|
@field_validator("request_timeout")
|
|
@classmethod
|
|
def validate_timeout(cls, v: int) -> int:
|
|
if not 1 <= v <= 300:
|
|
raise ConfigurationError(
|
|
f"request_timeout must be between 1 and 300 seconds, got {v}"
|
|
)
|
|
return v
|
|
|
|
@field_validator("deepseek_base_url", "tmdb_base_url")
|
|
@classmethod
|
|
def validate_url(cls, v: str, info) -> str:
|
|
if not v.startswith(("http://", "https://")):
|
|
raise ConfigurationError(f"Invalid {info.field_name}")
|
|
return v
|
|
|
|
def is_tmdb_configured(self):
|
|
return bool(self.tmdb_api_key)
|
|
|
|
def is_deepseek_configured(self):
|
|
return bool(self.deepseek_api_key)
|
|
|
|
def dump_safe(self):
|
|
return self.model_dump(exclude_none=False)
|
|
|
|
|
|
settings = Settings()
|