Skip to content

Commit 56619f4

Browse files
authored
fix(config): read the KB metadata database path from config instead of hardcoding it (#33)
1 parent 9d72a3c commit 56619f4

6 files changed

Lines changed: 127 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
constant score of 0.5 while the API reported `success: true`.
1919
- `add_documents` rejects a provider that returns fewer vectors than texts, which otherwise
2020
assigned each embedding to the wrong chunk. `screening.py` gained the same guard.
21+
- The KB-metadata SQLite path now honours `database.path` (and `PERSPICACITE_DB_PATH`) in the
22+
web app, the MCP server and the CLI. It was hardcoded to a working-directory-relative
23+
`./data/perspicacite.db` while the vector store already honoured `database.chroma_path`, so a
24+
second instance listed the main hub's knowledge bases and wrote its vectors elsewhere. When
25+
`database.path` is not set, the legacy location is kept, so existing deployments are unaffected.
26+
Both resolved paths are now logged at startup.
2127

2228
### Added
2329
- `GET /api/kb/{name}/stats` reports an `embedding_health` block (`probed_chunks`,

src/perspicacite/cli.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from perspicacite import __version__
1212
from perspicacite.config import load_config
13+
from perspicacite.config.paths import resolve_session_db_path
1314
from perspicacite.logging import get_logger, setup_logging
1415
from perspicacite.pipeline.asb.run_ingest import ingest_asb_run as ingest_asb_run_pipeline
1516
from perspicacite.pipeline.github.bundle import ContentSpec
@@ -177,7 +178,7 @@ def serve(
177178
"--session-db",
178179
type=click.Path(path_type=Path),
179180
default=None,
180-
help="SQLite DB for KB metadata (default: data/perspicacite.db, same as web app)",
181+
help="SQLite DB for KB metadata (default: config database.path, else data/perspicacite.db)",
181182
)
182183
@click.option(
183184
"--chroma-dir",
@@ -259,7 +260,7 @@ async def _create_empty() -> dict[str, Any]:
259260
)
260261
return
261262

262-
session_db = session_db or Path("data/perspicacite.db")
263+
session_db = session_db or resolve_session_db_path(config)
263264
chroma_dir = chroma_dir or config.database.chroma_path
264265

265266
click.echo(f"Creating knowledge base '{name}' from {from_bibtex}...")
@@ -313,7 +314,7 @@ async def _create_empty() -> dict[str, Any]:
313314
"--session-db",
314315
type=click.Path(path_type=Path),
315316
default=None,
316-
help="SQLite DB for KB metadata (default: data/perspicacite.db)",
317+
help="SQLite DB for KB metadata (default: config database.path, else data/perspicacite.db)",
317318
)
318319
@click.option(
319320
"--chroma-dir",
@@ -345,7 +346,7 @@ def add_to_kb(
345346
if ingest_mode is not None:
346347
config.knowledge_base.ingest_mode = ingest_mode
347348

348-
session_db = session_db or Path("data/perspicacite.db")
349+
session_db = session_db or resolve_session_db_path(config)
349350
chroma_dir = chroma_dir or config.database.chroma_path
350351

351352
click.echo(f"Adding papers from {from_bibtex} to knowledge base '{name}'...")

src/perspicacite/config/paths.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Filesystem paths derived from configuration.
2+
3+
The vector store has always honoured ``database.chroma_path`` while the KB
4+
metadata SQLite was hardcoded to a working-directory-relative path. A second
5+
instance therefore read the main hub's KB listing and wrote its vectors
6+
elsewhere, producing knowledge bases that exist in the listing but return
7+
nothing. These helpers give every entry point (web, MCP, CLI) one answer.
8+
"""
9+
10+
from pathlib import Path
11+
12+
from perspicacite.config.schema import Config
13+
14+
# Where the web app, MCP server and CLI historically kept KB metadata, relative
15+
# to the process working directory.
16+
LEGACY_SESSION_DB_PATH = Path("./data/perspicacite.db")
17+
18+
19+
def resolve_session_db_path(config: Config) -> Path:
20+
"""Return the KB-metadata SQLite path for this configuration.
21+
22+
``database.path`` is honoured only when it was actually supplied, by config
23+
file or by ``PERSPICACITE_DB_PATH``. Applying the schema default instead
24+
would silently relocate the metadata of every existing deployment, whose
25+
knowledge bases live in the legacy working-directory path.
26+
"""
27+
if "path" in config.database.model_fields_set:
28+
return config.database.path.expanduser()
29+
return LEGACY_SESSION_DB_PATH

src/perspicacite/mcp/server.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from pathlib import Path
3434
from typing import Any
3535

36+
from perspicacite.config.paths import resolve_session_db_path
3637
from perspicacite.logging import get_logger
3738
from perspicacite.pipeline.asb.response import build_asb_response_metadata
3839
from perspicacite.pipeline.asb.run_ingest import ingest_asb_run as ingest_asb_run_pipeline
@@ -95,8 +96,6 @@ async def initialize(self, config: Any) -> None:
9596
if self.initialized:
9697
return
9798

98-
from pathlib import Path
99-
10099
from perspicacite.llm import AsyncLLMClient
101100
from perspicacite.llm.embeddings import create_embedding_provider
102101
from perspicacite.memory.session_store import SessionStore
@@ -125,15 +124,20 @@ async def initialize(self, config: Any) -> None:
125124
)
126125

127126
# Session store
128-
db_path = Path("./data/perspicacite.db")
127+
db_path = resolve_session_db_path(config)
129128
db_path.parent.mkdir(parents=True, exist_ok=True)
130129
self.session_store = SessionStore(db_path)
131130
await self.session_store.init_db()
131+
logger.info(
132+
"mcp_stores_initialized",
133+
session_db=str(db_path),
134+
chroma_path=str(config.database.chroma_path),
135+
)
132136

133137
# Provenance store (shares the same DB as the session store)
134138
from perspicacite.provenance.store import ProvenanceStore
135139

136-
sidecar_dir = Path("./data/provenance")
140+
sidecar_dir = db_path.parent / "provenance"
137141
sidecar_dir.mkdir(parents=True, exist_ok=True)
138142
self.provenance_store = ProvenanceStore(db_path=db_path, sidecar_dir=sidecar_dir)
139143

src/perspicacite/web/state.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
import logging
1111
import os
12-
from pathlib import Path
1312
from typing import Any
1413

14+
from perspicacite.config.paths import resolve_session_db_path
1515
from perspicacite.jobs.registry import JobRegistry
1616
from perspicacite.memory.session_store import SessionStore
1717
from perspicacite.provenance.store import ProvenanceStore
@@ -192,11 +192,17 @@ async def initialize(self):
192192
logger.info("Agentic orchestrator initialized")
193193

194194
# Initialize session store FIRST so RAGEngine can receive it
195-
db_path = Path("./data/perspicacite.db")
195+
db_path = resolve_session_db_path(config)
196196
db_path.parent.mkdir(parents=True, exist_ok=True)
197197
self.session_store = SessionStore(db_path)
198198
await self.session_store.init_db()
199-
logger.info("Session store initialized")
199+
# Log both stores: an instance whose metadata and vectors live in
200+
# different places lists knowledge bases that return nothing.
201+
logger.info(
202+
"Session store initialized at %s (chroma: %s)",
203+
db_path,
204+
config.database.chroma_path,
205+
)
200206

201207
# Initialize RAG engine for multi-mode support
202208
from perspicacite.rag.engine import RAGEngine
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Regression tests: the KB-metadata SQLite must honour `database.path`.
2+
3+
The vector store has always read `database.chroma_path`, while the session
4+
store was hardcoded to a working-directory-relative `./data/perspicacite.db`.
5+
A second instance therefore listed the main hub's knowledge bases while writing
6+
its vectors elsewhere, so every KB it created "existed" but retrieved nothing.
7+
"""
8+
9+
from pathlib import Path
10+
11+
import pytest
12+
13+
from perspicacite.config.paths import LEGACY_SESSION_DB_PATH, resolve_session_db_path
14+
from perspicacite.config.schema import Config
15+
16+
17+
def test_unset_path_keeps_the_legacy_location():
18+
"""Existing deployments keep their metadata where it already lives."""
19+
assert resolve_session_db_path(Config()) == LEGACY_SESSION_DB_PATH
20+
21+
22+
def test_setting_only_chroma_path_keeps_the_legacy_location():
23+
"""Touching the vector-store path must not move the metadata store."""
24+
config = Config(database={"chroma_path": "/tmp/isolated-chroma"})
25+
assert resolve_session_db_path(config) == LEGACY_SESSION_DB_PATH
26+
27+
28+
def test_explicit_path_is_honoured_and_home_is_expanded():
29+
"""`~` must be expanded; SessionStore would otherwise create a `~` directory."""
30+
resolved = resolve_session_db_path(Config(database={"path": "~/isolated/memory.db"}))
31+
assert resolved == Path.home() / "isolated" / "memory.db"
32+
assert "~" not in str(resolved)
33+
34+
35+
def test_env_var_override_reaches_the_resolver(monkeypatch, tmp_path):
36+
"""PERSPICACITE_DB_PATH is documented as the isolation switch."""
37+
from perspicacite.config.loader import load_config
38+
39+
target = tmp_path / "instance" / "memory.db"
40+
monkeypatch.setenv("PERSPICACITE_DB_PATH", str(target))
41+
assert resolve_session_db_path(load_config(None)) == target
42+
43+
44+
@pytest.mark.asyncio
45+
async def test_app_state_opens_the_configured_database(monkeypatch, tmp_path):
46+
"""AppState.initialize must hand the configured path to SessionStore."""
47+
from perspicacite.web import state as state_module
48+
49+
target = tmp_path / "instance" / "memory.db"
50+
monkeypatch.setenv("PERSPICACITE_DB_PATH", str(target))
51+
monkeypatch.setenv("PERSPICACITE_ALLOW_MISSING_LLM_KEYS", "1")
52+
monkeypatch.setenv("PERSPICACITE_DB_CHROMA_PATH", str(tmp_path / "chroma"))
53+
54+
opened: list[Path] = []
55+
56+
class _RecordingSessionStore:
57+
def __init__(self, db_path):
58+
opened.append(Path(db_path))
59+
self.db_path = Path(db_path)
60+
61+
async def init_db(self):
62+
return None
63+
64+
monkeypatch.setattr(state_module, "SessionStore", _RecordingSessionStore)
65+
66+
app_state = state_module.AppState()
67+
await app_state.initialize()
68+
69+
assert opened == [target]
70+
assert opened[0] != LEGACY_SESSION_DB_PATH

0 commit comments

Comments
 (0)