Files

1 line
104 KiB
JSON

{"repo": "ass411/agentic-rag-kb", "files": {"backend/app/config.py": {"098de9516ee1fb6e4b5a00cd7edf9590e2b1e27a": {"t": 1785746883.6010706, "n": 12699, "h": "451ca0951ff3e316", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=10,\n ge=1,\n le=10,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n catalog_collection: str = Field(\n default=\"doc_catalog\",\n description=\"Side collection used by DocCatalog to embed each \"\n \"uploaded document's file_name. Lives on the same \"\n \"persist_dir as the main knowledge_base collection.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n semantic_chunking_enabled: bool = Field(default=True)\n child_chunk_size: int = Field(default=800, ge=100, le=2000)\n child_chunk_overlap: int = Field(default=150, ge=0)\n parent_chunk_max_chars: int = Field(default=3000, ge=1000, le=5000)\n semantic_threshold: float = Field(default=0.5, ge=0.0, le=1.0)\n\n # Hybrid search (BM25 + vector)\n hybrid_search_enabled: bool = Field(default=True)\n hybrid_rrf_k: int = Field(\n default=60, ge=1, le=200,\n description=\"K constant for Reciprocal Rank Fusion\",\n )\n\n # Doc-name semantic catalog\n doc_catalog_enabled: bool = Field(\n default=True,\n description=\"Whether to use the doc-name semantic catalog for \"\n \"doc_filter resolution. When False, falls back to \"\n \"the regex-based extractor in extract_target_doc_names.\",\n )\n doc_catalog_threshold: float = Field(\n default=0.75, ge=0.0, le=1.0,\n description=\"Cosine-similarity floor (0..1) for the doc-name \"\n \"catalog. Documents below this similarity are NOT \"\n \"used as a doc_filter.\",\n )\n doc_catalog_min_gap: float = Field(\n default=0.03, ge=0.0, le=1.0,\n description=\"Minimum gap between top-1 and top-2 catalog hits. \"\n \"If the gap is smaller, the question is treated as \"\n \"non-specific and no doc_filter is applied (this \"\n \"prevents generic questions like '什么是知识图谱' \"\n \"from being routed to the highest-baseline doc).\",\n )\n doc_catalog_top_k: int = Field(\n default=3, ge=1, le=20,\n description=\"Number of catalog candidates to consider when \"\n \"resolving a doc_filter from a question.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass EvaluationSettings(BaseSettings):\n \"\"\"RAGAS evaluation configuration.\"\"\"\n\n enabled: bool = Field(default=True)\n default_metrics: list[str] = Field(\n default=[\"faithfulness\", \"answer_relevancy\", \"context_precision\", \"context_recall\"],\n )\n max_samples_per_eval: int = Field(default=100, ge=1, le=500)\n use_parent_chunks_for_eval: bool = Field(default=True)\n store_reports: bool = Field(default=True)\n report_retention_days: int = Field(default=90)\n\n model_config = SettingsConfigDict(env_prefix=\"EVAL_\", extra=\"ignore\")\n\n\nclass RedisSettings(BaseSettings):\n \"\"\"Redis cache configuration.\"\"\"\n\n url: str = Field(\n default=\"redis://localhost:6379/0\",\n description=\"Redis connection URL\",\n )\n enabled: bool = Field(\n default=True,\n description=\"Whether Redis caching is enabled\",\n )\n cache_ttl_embedding: int = Field(\n default=86400,\n ge=1,\n description=\"TTL in seconds for cached embeddings (default 24h)\",\n )\n cache_ttl_rewrite: int = Field(\n default=86400,\n ge=1,\n description=\"TTL in seconds for cached query rewrites (default 24h)\",\n )\n cache_ttl_retrieve: int = Field(\n default=3600,\n ge=1,\n description=\"TTL in seconds for cached retrieval results (default 1h)\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"REDIS_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n redis: RedisSettings = RedisSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n evaluation: EvaluationSettings = EvaluationSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "cbdd35e4a37de2e5bf2bafed4d695fceedde2f62": {"t": 1785768443.1251416, "n": 12699, "h": "451ca0951ff3e316", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=10,\n ge=1,\n le=10,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n catalog_collection: str = Field(\n default=\"doc_catalog\",\n description=\"Side collection used by DocCatalog to embed each \"\n \"uploaded document's file_name. Lives on the same \"\n \"persist_dir as the main knowledge_base collection.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n semantic_chunking_enabled: bool = Field(default=True)\n child_chunk_size: int = Field(default=800, ge=100, le=2000)\n child_chunk_overlap: int = Field(default=150, ge=0)\n parent_chunk_max_chars: int = Field(default=3000, ge=1000, le=5000)\n semantic_threshold: float = Field(default=0.5, ge=0.0, le=1.0)\n\n # Hybrid search (BM25 + vector)\n hybrid_search_enabled: bool = Field(default=True)\n hybrid_rrf_k: int = Field(\n default=60, ge=1, le=200,\n description=\"K constant for Reciprocal Rank Fusion\",\n )\n\n # Doc-name semantic catalog\n doc_catalog_enabled: bool = Field(\n default=True,\n description=\"Whether to use the doc-name semantic catalog for \"\n \"doc_filter resolution. When False, falls back to \"\n \"the regex-based extractor in extract_target_doc_names.\",\n )\n doc_catalog_threshold: float = Field(\n default=0.75, ge=0.0, le=1.0,\n description=\"Cosine-similarity floor (0..1) for the doc-name \"\n \"catalog. Documents below this similarity are NOT \"\n \"used as a doc_filter.\",\n )\n doc_catalog_min_gap: float = Field(\n default=0.03, ge=0.0, le=1.0,\n description=\"Minimum gap between top-1 and top-2 catalog hits. \"\n \"If the gap is smaller, the question is treated as \"\n \"non-specific and no doc_filter is applied (this \"\n \"prevents generic questions like '什么是知识图谱' \"\n \"from being routed to the highest-baseline doc).\",\n )\n doc_catalog_top_k: int = Field(\n default=3, ge=1, le=20,\n description=\"Number of catalog candidates to consider when \"\n \"resolving a doc_filter from a question.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass EvaluationSettings(BaseSettings):\n \"\"\"RAGAS evaluation configuration.\"\"\"\n\n enabled: bool = Field(default=True)\n default_metrics: list[str] = Field(\n default=[\"faithfulness\", \"answer_relevancy\", \"context_precision\", \"context_recall\"],\n )\n max_samples_per_eval: int = Field(default=100, ge=1, le=500)\n use_parent_chunks_for_eval: bool = Field(default=True)\n store_reports: bool = Field(default=True)\n report_retention_days: int = Field(default=90)\n\n model_config = SettingsConfigDict(env_prefix=\"EVAL_\", extra=\"ignore\")\n\n\nclass RedisSettings(BaseSettings):\n \"\"\"Redis cache configuration.\"\"\"\n\n url: str = Field(\n default=\"redis://localhost:6379/0\",\n description=\"Redis connection URL\",\n )\n enabled: bool = Field(\n default=True,\n description=\"Whether Redis caching is enabled\",\n )\n cache_ttl_embedding: int = Field(\n default=86400,\n ge=1,\n description=\"TTL in seconds for cached embeddings (default 24h)\",\n )\n cache_ttl_rewrite: int = Field(\n default=86400,\n ge=1,\n description=\"TTL in seconds for cached query rewrites (default 24h)\",\n )\n cache_ttl_retrieve: int = Field(\n default=3600,\n ge=1,\n description=\"TTL in seconds for cached retrieval results (default 1h)\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"REDIS_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n redis: RedisSettings = RedisSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n evaluation: EvaluationSettings = EvaluationSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "490c6614fe0e4430a351b23a4d9c6ebf53d34e81": {"t": 1785768446.5695682, "n": 11164, "h": "fa9697af2a320282", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=10,\n ge=1,\n le=10,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n semantic_chunking_enabled: bool = Field(default=True)\n child_chunk_size: int = Field(default=800, ge=100, le=2000)\n child_chunk_overlap: int = Field(default=150, ge=0)\n parent_chunk_max_chars: int = Field(default=3000, ge=1000, le=5000)\n semantic_threshold: float = Field(default=0.5, ge=0.0, le=1.0)\n\n # Hybrid search (BM25 + vector)\n hybrid_search_enabled: bool = Field(default=True)\n hybrid_rrf_k: int = Field(\n default=60, ge=1, le=200,\n description=\"K constant for Reciprocal Rank Fusion\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass EvaluationSettings(BaseSettings):\n \"\"\"RAGAS evaluation configuration.\"\"\"\n\n enabled: bool = Field(default=True)\n default_metrics: list[str] = Field(\n default=[\"faithfulness\", \"answer_relevancy\", \"context_precision\", \"context_recall\"],\n )\n max_samples_per_eval: int = Field(default=100, ge=1, le=500)\n use_parent_chunks_for_eval: bool = Field(default=True)\n store_reports: bool = Field(default=True)\n report_retention_days: int = Field(default=90)\n\n model_config = SettingsConfigDict(env_prefix=\"EVAL_\", extra=\"ignore\")\n\n\nclass RedisSettings(BaseSettings):\n \"\"\"Redis cache configuration.\"\"\"\n\n url: str = Field(\n default=\"redis://localhost:6379/0\",\n description=\"Redis connection URL\",\n )\n enabled: bool = Field(\n default=True,\n description=\"Whether Redis caching is enabled\",\n )\n cache_ttl_embedding: int = Field(\n default=86400,\n ge=1,\n description=\"TTL in seconds for cached embeddings (default 24h)\",\n )\n cache_ttl_rewrite: int = Field(\n default=86400,\n ge=1,\n description=\"TTL in seconds for cached query rewrites (default 24h)\",\n )\n cache_ttl_retrieve: int = Field(\n default=3600,\n ge=1,\n description=\"TTL in seconds for cached retrieval results (default 1h)\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"REDIS_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n redis: RedisSettings = RedisSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n evaluation: EvaluationSettings = EvaluationSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "24de9e0496253ca9358ea4992c0a6dc69a6d5cf9": {"t": 1785768450.0911005, "n": 10049, "h": "813c90a98e4ba31a", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=10,\n ge=1,\n le=10,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n semantic_chunking_enabled: bool = Field(default=True)\n child_chunk_size: int = Field(default=800, ge=100, le=2000)\n child_chunk_overlap: int = Field(default=150, ge=0)\n parent_chunk_max_chars: int = Field(default=3000, ge=1000, le=5000)\n semantic_threshold: float = Field(default=0.5, ge=0.0, le=1.0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass EvaluationSettings(BaseSettings):\n \"\"\"RAGAS evaluation configuration.\"\"\"\n\n enabled: bool = Field(default=True)\n default_metrics: list[str] = Field(\n default=[\"faithfulness\", \"answer_relevancy\", \"context_precision\", \"context_recall\"],\n )\n max_samples_per_eval: int = Field(default=100, ge=1, le=500)\n use_parent_chunks_for_eval: bool = Field(default=True)\n store_reports: bool = Field(default=True)\n report_retention_days: int = Field(default=90)\n\n model_config = SettingsConfigDict(env_prefix=\"EVAL_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n evaluation: EvaluationSettings = EvaluationSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "3d69ba9045f099ce3351661dda5ff874adb70821": {"t": 1785768454.4158182, "n": 9431, "h": "ebdc93e08efddd67", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=10,\n ge=1,\n le=10,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n semantic_chunking_enabled: bool = Field(default=True)\n child_chunk_size: int = Field(default=800, ge=100, le=2000)\n child_chunk_overlap: int = Field(default=150, ge=0)\n parent_chunk_max_chars: int = Field(default=3000, ge=1000, le=5000)\n semantic_threshold: float = Field(default=0.5, ge=0.0, le=1.0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "1a573297e58d762c20c3fd76a0958dbcc7bd49b6": {"t": 1785768456.4884043, "n": 9113, "h": "bf75197a114864f4", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=10,\n ge=1,\n le=10,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "f531595388b18fed702aa9f0a1d524709064a68d": {"t": 1785768459.0939615, "n": 9114, "h": "db9a9c63af8ae4e7", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=25,\n ge=1,\n le=100,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n modelscope_cache_dir: str = Field(\n default=\"\",\n description=\"ModelScope local cache directory (e.g. ~/.cache/modelscope). \"\n \"When set, the reranker will prefer a locally cached copy \"\n \"of the model over downloading from HuggingFace.\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "4d450adce087a9a2be20721391633d51cb4937cc": {"t": 1785768460.5194805, "n": 8814, "h": "77fea2128a832b7c", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"Embedding endpoint base URL (OpenAI-compatible)\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n batch_size: int = Field(\n default=25,\n ge=1,\n le=100,\n description=\"Max texts per batch API call\",\n )\n max_retries: int = Field(\n default=3,\n ge=0,\n le=10,\n description=\"Max retries on transient failures\",\n )\n timeout_seconds: int = Field(\n default=60,\n ge=5,\n description=\"HTTP request timeout in seconds\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "dbeecf276de7627c4d83236228217829cf6072da": {"t": 1785768462.1846583, "n": 8215, "h": "774729cb478313cd", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n version: str = Field(default=\"0.1.0\")\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}, "49a2e574763708008474ba21fdb7a73525ed591c": {"t": 1785768463.8573961, "n": 8173, "h": "3210003ee93a04da", "c": "\"\"\"Application configuration via pydantic-settings.\n\nLoads all settings from environment variables / .env file with validation.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\nfrom dotenv import load_dotenv\nfrom pydantic import (\n AnyHttpUrl,\n Field,\n SecretStr,\n field_validator,\n)\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n# Directory containing .env (the backend/ package root)\n_ROOT_DIR = Path(__file__).resolve().parent.parent\n\n# Load .env into os.environ so all nested BaseSettings sub-models can read it.\n# KeyError=False: don't fail if .env is missing (e.g. in CI, vars are set directly).\n_ENV_PATH = _ROOT_DIR / \".env\"\nif _ENV_PATH.exists():\n load_dotenv(_ENV_PATH, override=False)\nelse:\n load_dotenv() # fallback: look in CWD\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _parse_comma_list(v: str | list[str]) -> list[str]:\n \"\"\"Parse a comma-separated string into a list of trimmed strings.\n\n Accepts both ``\"a,b,c\"`` and ``[\"a\",\"b\",\"c\"]``.\n \"\"\"\n if isinstance(v, list):\n return v\n return [item.strip() for item in v.split(\",\") if item.strip()]\n\n\ndef _resolve_dir(v: str) -> str:\n \"\"\"Normalise a directory path to an absolute path.\"\"\"\n import os\n\n return os.path.abspath(v)\n\n\n# ---------------------------------------------------------------------------\n# Sub-models (grouped by concern)\n# ---------------------------------------------------------------------------\n\n\nclass LLMSettings(BaseSettings):\n \"\"\"LLM provider configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | deepseek | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"LLM API Key\",\n )\n model: str = Field(\n default=\"qwen-plus\",\n description=\"Model name\",\n )\n base_url: AnyHttpUrl = Field(\n default=AnyHttpUrl(\"https://dashscope.aliyuncs.com/compatible-mode/v1\"),\n description=\"LLM endpoint base URL\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"LLM_\", extra=\"ignore\")\n\n\nclass EmbeddingSettings(BaseSettings):\n \"\"\"Embedding model configuration.\"\"\"\n\n provider: str = Field(\n default=\"dashscope\",\n description=\"Provider: dashscope | openai | custom\",\n )\n api_key: SecretStr = Field(\n default=SecretStr(\"sk-xxxxxxxx\"),\n description=\"Embedding API Key\",\n )\n model: str = Field(\n default=\"text-embedding-v3\",\n description=\"Embedding model name\",\n )\n dimensions: int = Field(\n default=1024,\n description=\"Output embedding dimensions\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"EMBEDDING_\", extra=\"ignore\")\n\n\nclass RerankerSettings(BaseSettings):\n \"\"\"Reranker model configuration.\"\"\"\n\n model: str = Field(\n default=\"BAAI/bge-reranker-v2-m3\",\n )\n device: str = Field(\n default=\"cpu\",\n description=\"cpu | cuda\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"RERANKER_\", extra=\"ignore\")\n\n\nclass ChromaSettings(BaseSettings):\n \"\"\"Chroma vector store configuration.\"\"\"\n\n persist_dir: str = Field(\n default=\"./data/chroma_data\",\n )\n collection: str = Field(\n default=\"knowledge_base\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"CHROMA_\", extra=\"ignore\")\n\n @field_validator(\"persist_dir\", mode=\"before\")\n @classmethod\n def _abs_persist_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass MySQLSettings(BaseSettings):\n \"\"\"MySQL database configuration.\"\"\"\n\n host: str = Field(default=\"127.0.0.1\")\n port: int = Field(default=3306)\n user: str = Field(default=\"root\")\n password: SecretStr = Field(default=SecretStr(\"your_password\"))\n database: str = Field(default=\"agentic_rag\")\n\n model_config = SettingsConfigDict(env_prefix=\"MYSQL_\", extra=\"ignore\")\n\n @property\n def async_dsn(self) -> str:\n \"\"\"Async connection string (aiomysql).\"\"\"\n return (\n f\"mysql+aiomysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n @property\n def sync_dsn(self) -> str:\n \"\"\"Sync connection string for Alembic / admin tasks.\"\"\"\n return (\n f\"mysql+pymysql://{self.user}:{self.password.get_secret_value()}\"\n f\"@{self.host}:{self.port}/{self.database}\"\n \"?charset=utf8mb4\"\n )\n\n\nclass UploadSettings(BaseSettings):\n \"\"\"File upload configuration.\"\"\"\n\n upload_dir: str = Field(default=\"./data/uploads\")\n max_upload_size_mb: int = Field(default=50)\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"upload_dir\", mode=\"before\")\n @classmethod\n def _abs_upload_dir(cls, v: str) -> str:\n return _resolve_dir(v)\n\n\nclass AgentSettings(BaseSettings):\n \"\"\"Agent behaviour configuration.\"\"\"\n\n max_rounds: int = Field(default=3, ge=1, le=10)\n top_k_recall: int = Field(default=20, ge=1, le=100)\n top_k_rerank: int = Field(default=5, ge=1, le=50)\n chunk_size: int = Field(default=800, ge=100, le=4096)\n chunk_overlap: int = Field(default=150, ge=0)\n\n model_config = SettingsConfigDict(env_prefix=\"AGENT_\", extra=\"ignore\")\n\n\nclass ServerSettings(BaseSettings):\n \"\"\"HTTP server configuration.\"\"\"\n\n host: str = Field(default=\"0.0.0.0\")\n port: int = Field(default=8000)\n cors_origins: Any = Field(\n default=\"http://localhost:5173\",\n description=\"Comma-separated allowed origins (stored as list after validation)\",\n )\n log_level: str = Field(\n default=\"INFO\",\n description=\"DEBUG | INFO | WARNING | ERROR\",\n )\n\n model_config = SettingsConfigDict(env_prefix=\"\", extra=\"ignore\")\n\n @field_validator(\"cors_origins\", mode=\"before\")\n @classmethod\n def _parse_cors(cls, v: str | list[str]) -> list[str]:\n \"\"\"Parse comma-separated origins into a list.\"\"\"\n return _parse_comma_list(v)\n\n @field_validator(\"log_level\", mode=\"before\")\n @classmethod\n def _upper_log_level(cls, v: str) -> str:\n return v.upper()\n\n\n# ---------------------------------------------------------------------------\n# Root settings aggregator\n# ---------------------------------------------------------------------------\n\n\nclass Settings(BaseSettings):\n \"\"\"Root settings — aggregates all sub-settings.\n\n Usage::\n\n from app.config import settings\n\n print(settings.llm.model)\n print(settings.mysql.async_dsn)\n \"\"\"\n\n model_config = SettingsConfigDict(\n extra=\"ignore\",\n )\n\n # Sub-models — each reads its own prefixed env vars\n llm: LLMSettings = LLMSettings()\n embedding: EmbeddingSettings = EmbeddingSettings()\n reranker: RerankerSettings = RerankerSettings()\n chroma: ChromaSettings = ChromaSettings()\n mysql: MySQLSettings = MySQLSettings()\n upload: UploadSettings = UploadSettings()\n agent: AgentSettings = AgentSettings()\n server: ServerSettings = ServerSettings()\n\n def model_post_init(self, __context) -> None:\n \"\"\"Ensure critical secrets are set.\"\"\"\n super().model_post_init(__context)\n if self.llm.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"LLM_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.embedding.api_key.get_secret_value() in (\"sk-xxxxxxxx\", \"\"):\n raise ValueError(\n \"EMBEDDING_API_KEY is required. Set it in .env or the environment.\"\n )\n if self.mysql.password.get_secret_value() in (\"your_password\", \"\"):\n raise ValueError(\n \"MYSQL_PASSWORD is required. Set it in .env or the environment.\"\n )\n\n\n# ---------------------------------------------------------------------------\n# Singleton\n# ---------------------------------------------------------------------------\n\nsettings = Settings()\n"}}}}