1 line
27 KiB
JSON
1 line
27 KiB
JSON
{"repo": "black0011/academic-assistant", "files": {"backend/settings.py": {"f96c4b7cbee70c53083f9b866a28e857ac702569": {"t": 1785745423.2758152, "n": 12971, "h": "e192962e94a762f2", "c": "\"\"\"Application-wide configuration loaded from environment variables.\n\nAll AAF modules that need config read it from `get_settings()` — never from\n`os.environ` directly. Settings are validated at app startup and cached.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom pydantic import Field, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass ProviderConfig(BaseSettings):\n \"\"\"One LLM provider's configuration.\n\n Populated from environment variables per-provider, e.g.:\n OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_DEFAULT_MODEL\n \"\"\"\n\n api_key: str = \"\"\n base_url: str = \"\"\n default_model: str = \"\"\n timeout_s: int = 120\n verify_ssl: bool = True\n\n\nclass Settings(BaseSettings):\n model_config = SettingsConfigDict(\n env_file=\".env\",\n env_file_encoding=\"utf-8\",\n extra=\"ignore\",\n case_sensitive=False,\n )\n\n env: Literal[\"development\", \"production\", \"test\"] = Field(\"development\", alias=\"aaf_env\")\n workdir: Path = Field(Path(\"./data\"), alias=\"aaf_workdir\")\n log_level: str = Field(\"INFO\", alias=\"aaf_log_level\")\n secret_key: str = Field(\"change-me\", alias=\"aaf_secret_key\")\n\n # When `auth_disabled` is true (default), every request is treated as\n # an anonymous local user — this keeps zero-config local dev painless.\n # Flip it to false in production AND set a strong `secret_key`.\n auth_disabled: bool = True\n auth_allow_signup: bool = False\n jwt_expire_seconds: int = 86400\n users_dir: Path = Path(\"./data/users\")\n\n database_url: str = \"sqlite+aiosqlite:///./data/aaf.db\"\n redis_url: str = \"redis://localhost:6379/0\"\n chroma_persist_dir: Path = Path(\"./data/chroma\")\n\n # Memory backends. \"auto\" picks a sensible backend per store based on\n # whether credentials / URLs are configured; explicit values (\"memory\",\n # \"yaml\", \"chroma\", \"sql\", \"redis\") override the heuristic.\n memory_vector_backend: Literal[\"auto\", \"memory\", \"chroma\"] = \"auto\"\n memory_knowledge_backend: Literal[\"auto\", \"memory\", \"yaml\"] = \"auto\"\n memory_heuristic_backend: Literal[\"auto\", \"memory\", \"yaml\"] = \"auto\"\n memory_episodic_backend: Literal[\"auto\", \"memory\", \"sql\"] = \"auto\"\n memory_session_backend: Literal[\"auto\", \"memory\", \"redis\"] = \"auto\"\n # M7.3 — DocumentStore for free-form RAG over uploaded blobs.\n memory_documents_backend: Literal[\"auto\", \"memory\", \"yaml\"] = \"auto\"\n memory_knowledge_dir: Path = Path(\"./data/knowledge\")\n memory_skills_dir: Path = Path(\"./data/skills\")\n memory_documents_dir: Path = Path(\"./data/documents\")\n memory_collection: str = \"aaf_memory\"\n\n # M8.1 — gated proposals subsystem.\n proposals_backend: Literal[\"auto\", \"memory\", \"yaml\"] = Field(\n \"auto\", alias=\"aaf_proposals_backend\"\n )\n proposals_dir: Path = Field(Path(\"./data/proposals\"), alias=\"aaf_proposals_dir\")\n\n # M8.2 — Planner DAG (compile / validate / execute).\n planner_default_max_nodes: int = Field(30, alias=\"aaf_planner_default_max_nodes\")\n planner_default_retry: int = Field(1, alias=\"aaf_planner_default_retry\")\n planner_max_parallel: int = Field(4, alias=\"aaf_planner_max_parallel\")\n\n default_llm_provider: str = \"openai\"\n embedding_provider: str = \"openai\"\n embedding_model: str = \"text-embedding-3-small\"\n\n # Embedding backend selection. ``provider`` (default) reuses the chat\n # LLM's ``embed()``; ``local`` swaps in a sentence-transformers model\n # so the vector store + skill matcher can run fully offline. ``local``\n # requires the optional ``offline`` extra (`uv sync --extra offline`).\n # See backend/core/llm/local_embedder.py.\n embedding_backend: Literal[\"provider\", \"local\"] = Field(\n \"provider\", alias=\"aaf_embedding_backend\"\n )\n local_embedding_model: str = Field(\"BAAI/bge-small-en-v1.5\", alias=\"aaf_local_embedding_model\")\n local_embedding_device: str | None = Field(None, alias=\"aaf_local_embedding_device\")\n local_embedding_cache_folder: Path | None = Field(\n None, alias=\"aaf_local_embedding_cache_folder\"\n )\n\n # Optional per-task LLM routing. When the file exists, AAF builds a\n # `RoutingLLMProvider` that lets workflows opt into a different\n # provider/model via `ctx.llm.for_route(\"<name>\")`. Absent file =\n # zero-config single-provider behaviour (backward-compatible).\n model_routing_config: Path = Field(\n Path(\"./config/model_routing.yaml\"), alias=\"aaf_model_routing_config\"\n )\n\n # Optional context auto-compaction. Wraps the LLM provider in\n # `CompactingLLMProvider` (see backend/core/llm/compactor.py): when\n # incoming messages would occupy more than `autocompact_threshold`\n # of the model's context window, AAF auto-summarises the middle of\n # the history (via the `autocompact_summariser_route` sub-provider\n # when routing is wired). Off by default — opt-in.\n autocompact_enabled: bool = Field(False, alias=\"aaf_autocompact_enabled\")\n autocompact_threshold: float = Field(0.7, alias=\"aaf_autocompact_threshold\")\n autocompact_keep_recent_n: int = Field(6, alias=\"aaf_autocompact_keep_recent_n\")\n autocompact_summariser_route: str = Field(\"fast\", alias=\"aaf_autocompact_summariser_route\")\n\n # Self-evolution: when enabled, the runner asks `EvolverAgent` to\n # draft a heuristic Proposal after every successful workflow run.\n # Drafts go to the gated ProposalStore (status = `draft`) and only\n # take effect after human review via `/api/v1/proposals/*`. Off by\n # default — opt-in to avoid surprising users with a growing queue\n # of un-reviewed proposals.\n evolver_enabled: bool = Field(False, alias=\"aaf_evolver_enabled\")\n\n # MCP servers: when enabled and `mcp_config` points at an existing\n # YAML, AAF builds an MCPClient per server, lists their tools, and\n # registers each one into the global ToolRegistry under the\n # namespaced name `mcp__<server>__<tool>`. Off by default — opt-in\n # because launching N MCP child processes on a laptop has a real\n # boot-time cost. See backend/tools/mcp_*.py for the implementation\n # and config/mcp_servers.example.yaml for the schema.\n mcp_enabled: bool = Field(False, alias=\"aaf_mcp_enabled\")\n mcp_config: Path = Field(Path(\"./config/mcp_servers.yaml\"), alias=\"aaf_mcp_config\")\n\n openai_api_key: str = \"\"\n openai_base_url: str = \"https://api.openai.com/v1\"\n openai_default_model: str = \"gpt-4o-mini\"\n openai_verify_ssl: bool = True\n\n anthropic_api_key: str = \"\"\n anthropic_base_url: str = \"https://api.anthropic.com/v1\"\n anthropic_default_model: str = \"claude-3-5-sonnet-latest\"\n\n deepseek_api_key: str = \"\"\n deepseek_base_url: str = \"https://api.deepseek.com/v1\"\n deepseek_default_model: str = \"deepseek-chat\"\n\n ollama_base_url: str = \"\"\n\n max_parallel_tasks: int = Field(4, alias=\"aaf_max_parallel_tasks\")\n default_budget_usd: float = Field(2.0, alias=\"aaf_default_budget_usd\")\n skill_exec_timeout_s: int = Field(120, alias=\"aaf_skill_exec_timeout_s\")\n skill_dry_run_timeout_s: int = Field(5, alias=\"aaf_skill_dry_run_timeout_s\")\n checkpoint_enabled: bool = Field(False, alias=\"aaf_checkpoint_enabled\")\n\n # L1 skills: directory of `<name>/SKILL.md` folders. Default\n # ``./skills`` lives next to the project root in dev so the existing\n # bundled skills are picked up without further config.\n skills_root: Path = Field(Path(\"./skills\"), alias=\"aaf_skills_root\")\n skill_workdir_root: Path = Field(Path(\"./data/skill_runs\"), alias=\"aaf_skill_workdir_root\")\n\n # Tool registry defaults — honoured by ToolRegistry.call(). A run can\n # still override per-call for sandbox / paid-API gating.\n allow_network: bool = Field(True, alias=\"aaf_allow_network\")\n allow_paid_apis: bool = Field(True, alias=\"aaf_allow_paid_apis\")\n\n # Long-running task infra. \"auto\" picks `arq` when Redis is configured\n # and the ARQ library is importable, else `inmemory`.\n task_queue_backend: Literal[\"auto\", \"inmemory\", \"arq\"] = Field(\n \"auto\", alias=\"aaf_task_queue_backend\"\n )\n task_store_backend: Literal[\"auto\", \"inmemory\", \"sql\"] = Field(\n \"auto\", alias=\"aaf_task_store_backend\"\n )\n\n # P7 — Manuscript bundles (project-shaped manuscripts: overleaf/ +\n # plan/ + experiments/ + …). Copy mode lives under ``manuscript_root``;\n # link mode points at a user-managed directory. Caps protect the\n # laptop disk and bound the worst-case write per request.\n manuscript_root: Path = Field(Path(\"./data/manuscripts\"), alias=\"aaf_manuscript_root\")\n manuscript_max_file_mb: int = Field(50, alias=\"aaf_manuscript_max_file_mb\")\n manuscript_max_bundle_mb: int = Field(500, alias=\"aaf_manuscript_max_bundle_mb\")\n\n @model_validator(mode=\"after\")\n def _ensure_workdir(self) -> Settings:\n self.workdir = self.workdir.resolve()\n return self\n\n def provider(self, name: str) -> ProviderConfig:\n \"\"\"Return a ProviderConfig for a registered provider name.\n\n Providers are discovered by convention: `<name>_api_key`,\n `<name>_base_url`, `<name>_default_model` attrs on Settings.\n \"\"\"\n cfg: dict[str, Any] = {\n \"api_key\": getattr(self, f\"{name}_api_key\", \"\"),\n \"base_url\": getattr(self, f\"{name}_base_url\", \"\"),\n \"default_model\": getattr(self, f\"{name}_default_model\", \"\"),\n }\n verify = getattr(self, f\"{name}_verify_ssl\", None)\n if verify is not None:\n cfg[\"verify_ssl\"] = verify\n return ProviderConfig(**cfg)\n\n # ---- derived configs for MemoryFactory / LLMRegistry ----------------\n\n def has_llm_credentials(self, name: str) -> bool:\n \"\"\"True when the named provider has enough config to make real calls.\"\"\"\n if name == \"mock\":\n return True\n key = getattr(self, f\"{name}_api_key\", \"\") or \"\"\n base = getattr(self, f\"{name}_base_url\", \"\") or \"\"\n # Ollama doesn't need a key; a base URL alone is enough.\n if name == \"ollama\":\n return bool(base)\n return bool(key)\n\n def memory_config(self) -> dict[str, dict]:\n \"\"\"Build the config dict consumed by `MemoryFactory`.\n\n ``*_backend = \"auto\"`` resolves to a best-guess per store:\n * vector: \"chroma\" iff persist dir set *and* chromadb importable,\n else \"memory\"\n * knowledge / heuristic: \"yaml\" (durable) — always safe locally\n * episodic: \"sql\" (sqlite works with zero config)\n * session: \"redis\" iff `redis_url` set, else \"memory\"\n \"\"\"\n vec = _resolve_vector_backend(self.memory_vector_backend, self.chroma_persist_dir)\n know = self.memory_knowledge_backend if self.memory_knowledge_backend != \"auto\" else \"yaml\"\n heur = self.memory_heuristic_backend if self.memory_heuristic_backend != \"auto\" else \"yaml\"\n epi = self.memory_episodic_backend if self.memory_episodic_backend != \"auto\" else \"sql\"\n sess = self.memory_session_backend\n if sess == \"auto\":\n sess = \"redis\" if self.redis_url else \"memory\"\n docs = self.memory_documents_backend\n if docs == \"auto\":\n docs = \"yaml\"\n\n cfg: dict[str, dict] = {\n \"vector\": {\"backend\": vec},\n \"knowledge\": {\"backend\": know},\n \"heuristic\": {\"backend\": heur},\n \"episodic\": {\"backend\": epi},\n \"session\": {\"backend\": sess},\n \"documents\": {\"backend\": docs},\n }\n if vec == \"chroma\":\n cfg[\"vector\"].update(\n persist_dir=str(self.chroma_persist_dir),\n collection=self.memory_collection,\n embedding_model=self.embedding_model,\n )\n if know == \"yaml\":\n cfg[\"knowledge\"][\"root\"] = str(self.memory_knowledge_dir)\n if heur == \"yaml\":\n cfg[\"heuristic\"][\"root\"] = str(self.memory_skills_dir)\n if epi == \"sql\":\n cfg[\"episodic\"][\"url\"] = self.database_url\n if sess == \"redis\":\n cfg[\"session\"][\"url\"] = self.redis_url\n cfg[\"session\"][\"namespace\"] = \"aaf\"\n if docs == \"yaml\":\n cfg[\"documents\"][\"root\"] = str(self.memory_documents_dir)\n return cfg\n\n\ndef _resolve_vector_backend(choice: str, persist_dir: Path) -> str:\n if choice != \"auto\":\n return choice\n try:\n import chromadb # noqa: F401\n except ImportError:\n return \"memory\"\n # We have chromadb available — honour it when a persist dir is set.\n return \"chroma\" if persist_dir else \"memory\"\n\n\n@lru_cache(maxsize=1)\ndef get_settings() -> Settings:\n # Settings fields all have defaults + aliases; mypy cannot see aliases.\n return Settings() # type: ignore[call-arg]\n\n\ndef reload_settings() -> Settings:\n \"\"\"Reset the cache and re-read env (useful in tests).\"\"\"\n get_settings.cache_clear()\n return get_settings()\n"}, "cbaf9c29874e4916fcf0241d5746607b3e18fee6": {"t": 1785753019.8385274, "n": 12971, "h": "e192962e94a762f2", "c": "\"\"\"Application-wide configuration loaded from environment variables.\n\nAll AAF modules that need config read it from `get_settings()` — never from\n`os.environ` directly. Settings are validated at app startup and cached.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import Literal\n\nfrom pydantic import Field, model_validator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass ProviderConfig(BaseSettings):\n \"\"\"One LLM provider's configuration.\n\n Populated from environment variables per-provider, e.g.:\n OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_DEFAULT_MODEL\n \"\"\"\n\n api_key: str = \"\"\n base_url: str = \"\"\n default_model: str = \"\"\n timeout_s: int = 120\n verify_ssl: bool = True\n\n\nclass Settings(BaseSettings):\n model_config = SettingsConfigDict(\n env_file=\".env\",\n env_file_encoding=\"utf-8\",\n extra=\"ignore\",\n case_sensitive=False,\n )\n\n env: Literal[\"development\", \"production\", \"test\"] = Field(\"development\", alias=\"aaf_env\")\n workdir: Path = Field(Path(\"./data\"), alias=\"aaf_workdir\")\n log_level: str = Field(\"INFO\", alias=\"aaf_log_level\")\n secret_key: str = Field(\"change-me\", alias=\"aaf_secret_key\")\n\n # When `auth_disabled` is true (default), every request is treated as\n # an anonymous local user — this keeps zero-config local dev painless.\n # Flip it to false in production AND set a strong `secret_key`.\n auth_disabled: bool = True\n auth_allow_signup: bool = False\n jwt_expire_seconds: int = 86400\n users_dir: Path = Path(\"./data/users\")\n\n database_url: str = \"sqlite+aiosqlite:///./data/aaf.db\"\n redis_url: str = \"redis://localhost:6379/0\"\n chroma_persist_dir: Path = Path(\"./data/chroma\")\n\n # Memory backends. \"auto\" picks a sensible backend per store based on\n # whether credentials / URLs are configured; explicit values (\"memory\",\n # \"yaml\", \"chroma\", \"sql\", \"redis\") override the heuristic.\n memory_vector_backend: Literal[\"auto\", \"memory\", \"chroma\"] = \"auto\"\n memory_knowledge_backend: Literal[\"auto\", \"memory\", \"yaml\"] = \"auto\"\n memory_heuristic_backend: Literal[\"auto\", \"memory\", \"yaml\"] = \"auto\"\n memory_episodic_backend: Literal[\"auto\", \"memory\", \"sql\"] = \"auto\"\n memory_session_backend: Literal[\"auto\", \"memory\", \"redis\"] = \"auto\"\n # M7.3 — DocumentStore for free-form RAG over uploaded blobs.\n memory_documents_backend: Literal[\"auto\", \"memory\", \"yaml\"] = \"auto\"\n memory_knowledge_dir: Path = Path(\"./data/knowledge\")\n memory_skills_dir: Path = Path(\"./data/skills\")\n memory_documents_dir: Path = Path(\"./data/documents\")\n memory_collection: str = \"aaf_memory\"\n\n # M8.1 — gated proposals subsystem.\n proposals_backend: Literal[\"auto\", \"memory\", \"yaml\"] = Field(\n \"auto\", alias=\"aaf_proposals_backend\"\n )\n proposals_dir: Path = Field(Path(\"./data/proposals\"), alias=\"aaf_proposals_dir\")\n\n # M8.2 — Planner DAG (compile / validate / execute).\n planner_default_max_nodes: int = Field(30, alias=\"aaf_planner_default_max_nodes\")\n planner_default_retry: int = Field(1, alias=\"aaf_planner_default_retry\")\n planner_max_parallel: int = Field(4, alias=\"aaf_planner_max_parallel\")\n\n default_llm_provider: str = \"openai\"\n embedding_provider: str = \"openai\"\n embedding_model: str = \"text-embedding-3-small\"\n\n # Embedding backend selection. ``provider`` (default) reuses the chat\n # LLM's ``embed()``; ``local`` swaps in a sentence-transformers model\n # so the vector store + skill matcher can run fully offline. ``local``\n # requires the optional ``offline`` extra (`uv sync --extra offline`).\n # See backend/core/llm/local_embedder.py.\n embedding_backend: Literal[\"provider\", \"local\"] = Field(\n \"provider\", alias=\"aaf_embedding_backend\"\n )\n local_embedding_model: str = Field(\"BAAI/bge-small-en-v1.5\", alias=\"aaf_local_embedding_model\")\n local_embedding_device: str | None = Field(None, alias=\"aaf_local_embedding_device\")\n local_embedding_cache_folder: Path | None = Field(\n None, alias=\"aaf_local_embedding_cache_folder\"\n )\n\n # Optional per-task LLM routing. When the file exists, AAF builds a\n # `RoutingLLMProvider` that lets workflows opt into a different\n # provider/model via `ctx.llm.for_route(\"<name>\")`. Absent file =\n # zero-config single-provider behaviour (backward-compatible).\n model_routing_config: Path = Field(\n Path(\"./config/model_routing.yaml\"), alias=\"aaf_model_routing_config\"\n )\n\n # Optional context auto-compaction. Wraps the LLM provider in\n # `CompactingLLMProvider` (see backend/core/llm/compactor.py): when\n # incoming messages would occupy more than `autocompact_threshold`\n # of the model's context window, AAF auto-summarises the middle of\n # the history (via the `autocompact_summariser_route` sub-provider\n # when routing is wired). Off by default — opt-in.\n autocompact_enabled: bool = Field(False, alias=\"aaf_autocompact_enabled\")\n autocompact_threshold: float = Field(0.7, alias=\"aaf_autocompact_threshold\")\n autocompact_keep_recent_n: int = Field(6, alias=\"aaf_autocompact_keep_recent_n\")\n autocompact_summariser_route: str = Field(\"fast\", alias=\"aaf_autocompact_summariser_route\")\n\n # Self-evolution: when enabled, the runner asks `EvolverAgent` to\n # draft a heuristic Proposal after every successful workflow run.\n # Drafts go to the gated ProposalStore (status = `draft`) and only\n # take effect after human review via `/api/v1/proposals/*`. Off by\n # default — opt-in to avoid surprising users with a growing queue\n # of un-reviewed proposals.\n evolver_enabled: bool = Field(False, alias=\"aaf_evolver_enabled\")\n\n # MCP servers: when enabled and `mcp_config` points at an existing\n # YAML, AAF builds an MCPClient per server, lists their tools, and\n # registers each one into the global ToolRegistry under the\n # namespaced name `mcp__<server>__<tool>`. Off by default — opt-in\n # because launching N MCP child processes on a laptop has a real\n # boot-time cost. See backend/tools/mcp_*.py for the implementation\n # and config/mcp_servers.example.yaml for the schema.\n mcp_enabled: bool = Field(False, alias=\"aaf_mcp_enabled\")\n mcp_config: Path = Field(Path(\"./config/mcp_servers.yaml\"), alias=\"aaf_mcp_config\")\n\n openai_api_key: str = \"\"\n openai_base_url: str = \"https://api.openai.com/v1\"\n openai_default_model: str = \"gpt-4o-mini\"\n openai_verify_ssl: bool = True\n\n anthropic_api_key: str = \"\"\n anthropic_base_url: str = \"https://api.anthropic.com/v1\"\n anthropic_default_model: str = \"claude-3-5-sonnet-latest\"\n\n deepseek_api_key: str = \"\"\n deepseek_base_url: str = \"https://api.deepseek.com/v1\"\n deepseek_default_model: str = \"deepseek-chat\"\n\n ollama_base_url: str = \"\"\n\n max_parallel_tasks: int = Field(4, alias=\"aaf_max_parallel_tasks\")\n default_budget_usd: float = Field(2.0, alias=\"aaf_default_budget_usd\")\n skill_exec_timeout_s: int = Field(120, alias=\"aaf_skill_exec_timeout_s\")\n skill_dry_run_timeout_s: int = Field(5, alias=\"aaf_skill_dry_run_timeout_s\")\n checkpoint_enabled: bool = Field(False, alias=\"aaf_checkpoint_enabled\")\n\n # L1 skills: directory of `<name>/SKILL.md` folders. Default\n # ``./skills`` lives next to the project root in dev so the existing\n # bundled skills are picked up without further config.\n skills_root: Path = Field(Path(\"./skills\"), alias=\"aaf_skills_root\")\n skill_workdir_root: Path = Field(Path(\"./data/skill_runs\"), alias=\"aaf_skill_workdir_root\")\n\n # Tool registry defaults — honoured by ToolRegistry.call(). A run can\n # still override per-call for sandbox / paid-API gating.\n allow_network: bool = Field(True, alias=\"aaf_allow_network\")\n allow_paid_apis: bool = Field(True, alias=\"aaf_allow_paid_apis\")\n\n # Long-running task infra. \"auto\" picks `arq` when Redis is configured\n # and the ARQ library is importable, else `inmemory`.\n task_queue_backend: Literal[\"auto\", \"inmemory\", \"arq\"] = Field(\n \"auto\", alias=\"aaf_task_queue_backend\"\n )\n task_store_backend: Literal[\"auto\", \"inmemory\", \"sql\"] = Field(\n \"auto\", alias=\"aaf_task_store_backend\"\n )\n\n # P7 — Manuscript bundles (project-shaped manuscripts: overleaf/ +\n # plan/ + experiments/ + …). Copy mode lives under ``manuscript_root``;\n # link mode points at a user-managed directory. Caps protect the\n # laptop disk and bound the worst-case write per request.\n manuscript_root: Path = Field(Path(\"./data/manuscripts\"), alias=\"aaf_manuscript_root\")\n manuscript_max_file_mb: int = Field(50, alias=\"aaf_manuscript_max_file_mb\")\n manuscript_max_bundle_mb: int = Field(500, alias=\"aaf_manuscript_max_bundle_mb\")\n\n @model_validator(mode=\"after\")\n def _ensure_workdir(self) -> Settings:\n self.workdir = self.workdir.resolve()\n return self\n\n def provider(self, name: str) -> ProviderConfig:\n \"\"\"Return a ProviderConfig for a registered provider name.\n\n Providers are discovered by convention: `<name>_api_key`,\n `<name>_base_url`, `<name>_default_model` attrs on Settings.\n \"\"\"\n cfg: dict[str, Any] = {\n \"api_key\": getattr(self, f\"{name}_api_key\", \"\"),\n \"base_url\": getattr(self, f\"{name}_base_url\", \"\"),\n \"default_model\": getattr(self, f\"{name}_default_model\", \"\"),\n }\n verify = getattr(self, f\"{name}_verify_ssl\", None)\n if verify is not None:\n cfg[\"verify_ssl\"] = verify\n return ProviderConfig(**cfg)\n\n # ---- derived configs for MemoryFactory / LLMRegistry ----------------\n\n def has_llm_credentials(self, name: str) -> bool:\n \"\"\"True when the named provider has enough config to make real calls.\"\"\"\n if name == \"mock\":\n return True\n key = getattr(self, f\"{name}_api_key\", \"\") or \"\"\n base = getattr(self, f\"{name}_base_url\", \"\") or \"\"\n # Ollama doesn't need a key; a base URL alone is enough.\n if name == \"ollama\":\n return bool(base)\n return bool(key)\n\n def memory_config(self) -> dict[str, dict]:\n \"\"\"Build the config dict consumed by `MemoryFactory`.\n\n ``*_backend = \"auto\"`` resolves to a best-guess per store:\n * vector: \"chroma\" iff persist dir set *and* chromadb importable,\n else \"memory\"\n * knowledge / heuristic: \"yaml\" (durable) — always safe locally\n * episodic: \"sql\" (sqlite works with zero config)\n * session: \"redis\" iff `redis_url` set, else \"memory\"\n \"\"\"\n vec = _resolve_vector_backend(self.memory_vector_backend, self.chroma_persist_dir)\n know = self.memory_knowledge_backend if self.memory_knowledge_backend != \"auto\" else \"yaml\"\n heur = self.memory_heuristic_backend if self.memory_heuristic_backend != \"auto\" else \"yaml\"\n epi = self.memory_episodic_backend if self.memory_episodic_backend != \"auto\" else \"sql\"\n sess = self.memory_session_backend\n if sess == \"auto\":\n sess = \"redis\" if self.redis_url else \"memory\"\n docs = self.memory_documents_backend\n if docs == \"auto\":\n docs = \"yaml\"\n\n cfg: dict[str, dict] = {\n \"vector\": {\"backend\": vec},\n \"knowledge\": {\"backend\": know},\n \"heuristic\": {\"backend\": heur},\n \"episodic\": {\"backend\": epi},\n \"session\": {\"backend\": sess},\n \"documents\": {\"backend\": docs},\n }\n if vec == \"chroma\":\n cfg[\"vector\"].update(\n persist_dir=str(self.chroma_persist_dir),\n collection=self.memory_collection,\n embedding_model=self.embedding_model,\n )\n if know == \"yaml\":\n cfg[\"knowledge\"][\"root\"] = str(self.memory_knowledge_dir)\n if heur == \"yaml\":\n cfg[\"heuristic\"][\"root\"] = str(self.memory_skills_dir)\n if epi == \"sql\":\n cfg[\"episodic\"][\"url\"] = self.database_url\n if sess == \"redis\":\n cfg[\"session\"][\"url\"] = self.redis_url\n cfg[\"session\"][\"namespace\"] = \"aaf\"\n if docs == \"yaml\":\n cfg[\"documents\"][\"root\"] = str(self.memory_documents_dir)\n return cfg\n\n\ndef _resolve_vector_backend(choice: str, persist_dir: Path) -> str:\n if choice != \"auto\":\n return choice\n try:\n import chromadb # noqa: F401\n except ImportError:\n return \"memory\"\n # We have chromadb available — honour it when a persist dir is set.\n return \"chroma\" if persist_dir else \"memory\"\n\n\n@lru_cache(maxsize=1)\ndef get_settings() -> Settings:\n # Settings fields all have defaults + aliases; mypy cannot see aliases.\n return Settings() # type: ignore[call-arg]\n\n\ndef reload_settings() -> Settings:\n \"\"\"Reset the cache and re-read env (useful in tests).\"\"\"\n get_settings.cache_clear()\n return get_settings()\n"}}}} |