diff --git a/src/rag/embedder.py b/src/rag/embedder.py index 4ddf7d4..471a744 100644 --- a/src/rag/embedder.py +++ b/src/rag/embedder.py @@ -84,5 +84,8 @@ def get_embedder() -> Embedder: """Singleton embedder.""" global _embedder if _embedder is None: + from .env import ensure_rag_env + + ensure_rag_env() _embedder = Embedder() return _embedder diff --git a/src/rag/env.py b/src/rag/env.py new file mode 100644 index 0000000..0208a21 --- /dev/null +++ b/src/rag/env.py @@ -0,0 +1,29 @@ +"""Load Meridian .env for RAG indexing and retrieval.""" + +from __future__ import annotations + +from pathlib import Path + +from dotenv import load_dotenv + +REPO_ROOT = Path(__file__).resolve().parents[2] +_env_loaded = False + + +def ensure_rag_env() -> Path: + """Load repo-root ``.env`` once without overriding existing env vars.""" + global _env_loaded + if not _env_loaded: + load_dotenv(REPO_ROOT / ".env") + _env_loaded = True + return REPO_ROOT + + +def reset_rag_env_state() -> None: + """Reset env-load flag and RAG singletons (tests only).""" + global _env_loaded + _env_loaded = False + from . import embedder, qdrant_client + + embedder._embedder = None # type: ignore[attr-defined] + qdrant_client._store = None # type: ignore[attr-defined] diff --git a/src/rag/indexing.py b/src/rag/indexing.py index dca7970..4707dcc 100644 --- a/src/rag/indexing.py +++ b/src/rag/indexing.py @@ -8,10 +8,12 @@ import structlog from .collections import RagCollection, upsert_documents +from .env import REPO_ROOT, ensure_rag_env logger = structlog.get_logger(__name__) -ROOT = Path(__file__).resolve().parents[2] +ensure_rag_env() +ROOT = REPO_ROOT METRICS_PATH = ROOT / "docs" / "METRICS.md" LIMITATIONS_PATH = ROOT / "docs" / "LIMITATIONS.md" CAUSAL_SCOPE_PATH = ROOT / "docs" / "CAUSAL_SCOPE.md" diff --git a/src/rag/qdrant_client.py b/src/rag/qdrant_client.py index d75629c..0c54a60 100644 --- a/src/rag/qdrant_client.py +++ b/src/rag/qdrant_client.py @@ -137,5 +137,8 @@ def get_qdrant_store() -> QdrantStore: """Singleton Qdrant store.""" global _store if _store is None: + from .env import ensure_rag_env + + ensure_rag_env() _store = QdrantStore() return _store diff --git a/tests/unit/test_rag_indexing_env.py b/tests/unit/test_rag_indexing_env.py new file mode 100644 index 0000000..fa36e19 --- /dev/null +++ b/tests/unit/test_rag_indexing_env.py @@ -0,0 +1,88 @@ +"""Tests for RAG indexing env loading.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from src.rag.env import REPO_ROOT, ensure_rag_env, reset_rag_env_state + + +@pytest.fixture(autouse=True) +def _clean_rag_env(monkeypatch: pytest.MonkeyPatch) -> None: + """Isolate env loading and singleton state between tests.""" + reset_rag_env_state() + monkeypatch.delenv("QDRANT_URL", raising=False) + monkeypatch.delenv("RAG_EMBED_MODE", raising=False) + yield + reset_rag_env_state() + + +def test_ensure_rag_env_loads_dotenv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + env_file = tmp_path / ".env" + env_file.write_text( + "QDRANT_URL=http://qdrant-from-dotenv:6333\nRAG_EMBED_MODE=hash\n", + encoding="utf-8", + ) + monkeypatch.setattr("src.rag.env.REPO_ROOT", tmp_path) + reset_rag_env_state() + + ensure_rag_env() + + assert os.getenv("QDRANT_URL") == "http://qdrant-from-dotenv:6333" + assert os.getenv("RAG_EMBED_MODE") == "hash" + + +def test_ensure_rag_env_does_not_override_existing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + env_file = tmp_path / ".env" + env_file.write_text("QDRANT_URL=http://from-file:6333\n", encoding="utf-8") + monkeypatch.setenv("QDRANT_URL", "http://from-shell:6333") + monkeypatch.setattr("src.rag.env.REPO_ROOT", tmp_path) + reset_rag_env_state() + + ensure_rag_env() + + assert os.getenv("QDRANT_URL") == "http://from-shell:6333" + + +def test_get_qdrant_store_reads_dotenv_before_singleton( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + env_file = tmp_path / ".env" + env_file.write_text("QDRANT_URL=http://qdrant-singleton:6333\n", encoding="utf-8") + monkeypatch.setattr("src.rag.env.REPO_ROOT", tmp_path) + reset_rag_env_state() + + from src.rag.qdrant_client import get_qdrant_store + + store = get_qdrant_store() + + assert store.url == "http://qdrant-singleton:6333" + + +def test_get_embedder_reads_dotenv_before_singleton( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + env_file = tmp_path / ".env" + env_file.write_text("RAG_EMBED_MODE=hash\n", encoding="utf-8") + monkeypatch.setattr("src.rag.env.REPO_ROOT", tmp_path) + reset_rag_env_state() + + from src.rag.embedder import get_embedder + + embedder = get_embedder() + + assert embedder.force_hash is True + assert embedder.mode == "hash" + + +def test_indexing_module_resolves_repo_root_docs() -> None: + from src.rag import indexing + + assert indexing.ROOT == REPO_ROOT + assert indexing.METRICS_PATH == REPO_ROOT / "docs" / "METRICS.md" + assert indexing.METRICS_PATH.exists()