From 01e6975d20c7cae035a3b7a6a868b03a0149d9b7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 26 Apr 2026 11:28:40 -0400 Subject: [PATCH] feat: config loader with toml + env override --- chat/config.py | 40 ++++++++++++++++++++++++++++++++++++++++ data/config.example.toml | 6 ++++++ tests/test_config.py | 26 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 chat/config.py create mode 100644 data/config.example.toml create mode 100644 tests/test_config.py diff --git a/chat/config.py b/chat/config.py new file mode 100644 index 0000000..90d6175 --- /dev/null +++ b/chat/config.py @@ -0,0 +1,40 @@ +from __future__ import annotations +import os +import tomllib +from pathlib import Path +from pydantic import BaseModel, Field + +REPO_ROOT = Path(__file__).resolve().parent.parent +DEFAULT_CONFIG = REPO_ROOT / "data" / "config.toml" +DEFAULT_DB = REPO_ROOT / "data" / "chat.db" + +class Settings(BaseModel): + featherless_api_key: str + featherless_base_url: str = "https://api.featherless.ai/v1" + narrative_model: str = "dphn/Dolphin-Mistral-24B-Venice-Edition" + classifier_model: str = "NousResearch/Hermes-3-Llama-3.1-8B" + classifier_fallbacks: list[str] = Field( + default_factory=lambda: [ + "cognitivecomputations/dolphin-2.9.4-llama3-8b", + "mlabonne/Meta-Llama-3.1-8B-Instruct-abliterated", + ] + ) + ooc_marker: str = "((" + retrieval_k: int = 4 + narrative_budget_hard: int = 8000 + narrative_budget_soft: int = 6000 + classifier_budget_hard: int = 4000 + classifier_timeout_s: float = 10.0 + db_path: Path = DEFAULT_DB + data_dir: Path = REPO_ROOT / "data" + bind_host: str = "127.0.0.1" + bind_port: int = 8000 + +def load_settings() -> Settings: + config_path = Path(os.environ.get("CHAT_CONFIG_PATH", DEFAULT_CONFIG)) + raw: dict = {} + if config_path.exists(): + raw = tomllib.loads(config_path.read_text()) + if "CHAT_DB_PATH" in os.environ: + raw["db_path"] = Path(os.environ["CHAT_DB_PATH"]) + return Settings(**raw) diff --git a/data/config.example.toml b/data/config.example.toml new file mode 100644 index 0000000..1da91f2 --- /dev/null +++ b/data/config.example.toml @@ -0,0 +1,6 @@ +# Copy this file to data/config.toml and fill in your API key. +featherless_api_key = "REPLACE_ME" +narrative_model = "dphn/Dolphin-Mistral-24B-Venice-Edition" +classifier_model = "NousResearch/Hermes-3-Llama-3.1-8B" +ooc_marker = "((" +retrieval_k = 4 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..abffd57 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path +import pytest +from chat.config import load_settings + +def test_load_settings_reads_toml(tmp_path, monkeypatch): + cfg = tmp_path / "config.toml" + cfg.write_text(""" + featherless_api_key = "sk-test" + narrative_model = "dphn/Dolphin-Mistral-24B-Venice-Edition" + classifier_model = "NousResearch/Hermes-3-Llama-3.1-8B" + ooc_marker = "((" + retrieval_k = 4 + """) + monkeypatch.setenv("CHAT_CONFIG_PATH", str(cfg)) + s = load_settings() + assert s.featherless_api_key == "sk-test" + assert s.narrative_model.startswith("dphn/") + assert s.retrieval_k == 4 + +def test_chat_db_path_env_overrides_default(tmp_path, monkeypatch): + monkeypatch.setenv("CHAT_DB_PATH", str(tmp_path / "alt.db")) + monkeypatch.setenv("CHAT_CONFIG_PATH", str(tmp_path / "config.toml")) + (tmp_path / "config.toml").write_text('featherless_api_key = "x"\n') + s = load_settings() + assert s.db_path == tmp_path / "alt.db"