API authentication: - Add X-API-Secret shared-secret header validation on /chat and /stats - /health remains public for monitoring - Auth is a no-op when API_SECRET is empty (dev mode) Rate limiting: - Add per-user sliding-window rate limiter on /chat (10 req/60s default) - Returns 429 with clear message when exceeded - Self-cleaning memory (prunes expired entries on each check) Exception sanitization: - Discord bot no longer exposes raw exception text to users - Error embeds show generic "Something went wrong" message - Full exception details logged server-side with context - query_chat_api RuntimeError no longer includes response body Async correctness: - Wrap synchronous RuleRepository.search() in run_in_executor() to prevent blocking the event loop during SentenceTransformer inference - Port contract stays synchronous; service owns the async boundary Test coverage: 101 passed, 1 skipped (11 new tests for auth + rate limiting) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
3.4 KiB
Python
80 lines
3.4 KiB
Python
"""Application settings — Pydantic v2 style, no module-level singleton.
|
|
|
|
The container (config/container.py) instantiates Settings once at startup
|
|
and passes it down to adapters. This keeps tests free of singleton state.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from pydantic import Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""All runtime configuration, sourced from environment variables or a .env file.
|
|
|
|
Fields use explicit ``env=`` aliases so the variable names are immediately
|
|
visible and grep-able without needing to know Pydantic's casing rules.
|
|
"""
|
|
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
# Allow unknown env vars — avoids breakage when the .env file has
|
|
# variables that belong to other services (Discord bot, scripts, etc.).
|
|
extra="ignore",
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# OpenRouter / LLM
|
|
# ------------------------------------------------------------------
|
|
openrouter_api_key: str = Field(default="", alias="OPENROUTER_API_KEY")
|
|
openrouter_model: str = Field(
|
|
default="stepfun/step-3.5-flash:free", alias="OPENROUTER_MODEL"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Discord
|
|
# ------------------------------------------------------------------
|
|
discord_bot_token: str = Field(default="", alias="DISCORD_BOT_TOKEN")
|
|
discord_guild_id: Optional[str] = Field(default=None, alias="DISCORD_GUILD_ID")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Gitea issue tracker
|
|
# ------------------------------------------------------------------
|
|
gitea_token: str = Field(default="", alias="GITEA_TOKEN")
|
|
gitea_owner: str = Field(default="cal", alias="GITEA_OWNER")
|
|
gitea_repo: str = Field(default="strat-chatbot", alias="GITEA_REPO")
|
|
gitea_base_url: str = Field(
|
|
default="https://git.manticorum.com/api/v1", alias="GITEA_BASE_URL"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# File-system paths
|
|
# ------------------------------------------------------------------
|
|
data_dir: Path = Field(default=Path("./data"), alias="DATA_DIR")
|
|
rules_dir: Path = Field(default=Path("./data/rules"), alias="RULES_DIR")
|
|
chroma_dir: Path = Field(default=Path("./data/chroma"), alias="CHROMA_DIR")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Database
|
|
# ------------------------------------------------------------------
|
|
db_url: str = Field(
|
|
default="sqlite+aiosqlite:///./data/conversations.db", alias="DB_URL"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# API authentication
|
|
# ------------------------------------------------------------------
|
|
api_secret: str = Field(default="", alias="API_SECRET")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Conversation / retrieval tuning
|
|
# ------------------------------------------------------------------
|
|
conversation_ttl: int = Field(default=1800, alias="CONVERSATION_TTL")
|
|
top_k_rules: int = Field(default=10, alias="TOP_K_RULES")
|
|
embedding_model: str = Field(
|
|
default="sentence-transformers/all-MiniLM-L6-v2", alias="EMBEDDING_MODEL"
|
|
)
|