Skip to content

Commit ed69efe

Browse files
committed
feat: enhance system robustness, diagnostics, and retrieval precision
- Implement MockVectorStore with .npy persistence as a fallback for FAISS-less environments. - Enhance 'knowcode doctor' with comprehensive native and optional dependency checks. - Fix Reranker initialization to respect AppConfig model settings regardless of VoyageAI status. - Improve tokenizer to preserve compound identifiers (CamelCase/snake_case) for superior BM25 precision. - Integrate SqliteChunkRepository for robust, scalable metadata persistence. - Add comprehensive unit tests for Reranker fallbacks, Search Engine error handling, and MockVectorStore persistence. - Improve telemetry reliability in test environments with synchronous flushing.
1 parent 1a02dc7 commit ed69efe

20 files changed

Lines changed: 1646 additions & 114 deletions

conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ def pytest_addoption(parser: pytest.Parser) -> None:
1919
default=False,
2020
help="Skip the eval golden SHA guard for local exploration.",
2121
)
22+
23+
24+
def pytest_configure(config: pytest.Config) -> None:
25+
"""Configure repository-wide settings before tests run."""
26+
import os
27+
os.environ["KNOWCODE_TESTING"] = "1"

src/knowcode/doctor.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import asyncio
66
import contextlib
7+
import importlib
78
import json
89
import os
910
import sys
@@ -80,6 +81,8 @@ def run_doctor(
8081
store_root = store_file.parent
8182
_check_knowledge_store(store_file, checks)
8283

84+
_check_dependencies(checks)
85+
8386
resolved_index = Path(index_path) if index_path is not None else store_root / "knowcode_index"
8487
_check_semantic_index(resolved_index, config, checks)
8588

@@ -208,6 +211,73 @@ def _check_api_keys(config: AppConfig, checks: list[DoctorCheck]) -> None:
208211
)
209212

210213

214+
def _check_dependencies(checks: list[DoctorCheck]) -> None:
215+
"""Check for optional and native dependencies."""
216+
from importlib.util import find_spec
217+
218+
deps = [
219+
("faiss", "knowcode[search]", "Required for fast dense vector retrieval."),
220+
("mcp", "knowcode[mcp]", "Required for Model Context Protocol server."),
221+
("voyageai", "knowcode[voyageai]", "Required for VoyageAI embeddings and reranking."),
222+
("openai", "knowcode[llm]", "Required for OpenAI embeddings."),
223+
("google.genai", "knowcode[llm]", "Required for Google Gemini integration."),
224+
("watchdog", "knowcode[watch]", "Required for background file indexing."),
225+
]
226+
227+
missing = []
228+
for module, extra, reason in deps:
229+
try:
230+
if find_spec(module) is None:
231+
missing.append((module, extra, reason))
232+
except (ModuleNotFoundError, ImportError):
233+
missing.append((module, extra, reason))
234+
235+
# FAISS is special - it's a native binary
236+
faiss_installed = True
237+
try:
238+
importlib.import_module("faiss")
239+
except (ImportError, RuntimeError):
240+
faiss_installed = False
241+
242+
if not faiss_installed:
243+
checks.append(
244+
DoctorCheck(
245+
name="Native dependencies",
246+
status="warn",
247+
message="FAISS native binaries not found. Using MockVectorStore fallback.",
248+
hint='Install with `pip install "faiss-cpu>=1.7.0"`.',
249+
)
250+
)
251+
else:
252+
checks.append(
253+
DoctorCheck(
254+
name="Native dependencies",
255+
status="pass",
256+
message="FAISS native binaries are installed and functional.",
257+
)
258+
)
259+
260+
if missing:
261+
msg = f"Missing {len(missing)} optional dependencies."
262+
hint = "Install them using: pip install " + " ".join(f'"{extra}"' for _, extra, _ in missing)
263+
checks.append(
264+
DoctorCheck(
265+
name="Optional dependencies",
266+
status="warn",
267+
message=msg,
268+
hint=hint,
269+
)
270+
)
271+
else:
272+
checks.append(
273+
DoctorCheck(
274+
name="Optional dependencies",
275+
status="pass",
276+
message="All optional dependencies are installed.",
277+
)
278+
)
279+
280+
211281
def _check_knowledge_store(store_file: Path, checks: list[DoctorCheck]) -> None:
212282
"""Validate knowledge store presence and schema."""
213283
if not store_file.exists():

src/knowcode/indexing/indexer.py

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66
from typing import Any, Optional
77

8-
from knowcode.storage.chunk_repository import InMemoryChunkRepository
8+
from knowcode.storage.chunk_repository import ChunkRepository, InMemoryChunkRepository
99
from knowcode.indexing.chunker import Chunker
1010
from knowcode.indexing.graph_builder import GraphBuilder
1111
from knowcode.indexing.scanner import Scanner
@@ -26,7 +26,7 @@ class Indexer:
2626
def __init__(
2727
self,
2828
embedding_provider: EmbeddingProviderProtocol,
29-
chunk_repo: Optional[InMemoryChunkRepository] = None,
29+
chunk_repo: Optional[ChunkRepository] = None,
3030
vector_store: Optional[VectorStoreProtocol] = None,
3131
) -> None:
3232
"""Initialize an indexer with optional storage backends.
@@ -37,7 +37,7 @@ def __init__(
3737
vector_store: Optional vector store (defaults to FAISS-backed store).
3838
"""
3939
self.embedding_provider = embedding_provider
40-
self.chunk_repo = chunk_repo or InMemoryChunkRepository()
40+
self.chunk_repo: ChunkRepository = chunk_repo or InMemoryChunkRepository()
4141
self.vector_store = vector_store or VectorStore(dimension=embedding_provider.config.dimension)
4242
self.chunker = Chunker()
4343
self.manifest: dict[str, Any] = {}
@@ -93,34 +93,20 @@ def save(self, path: str | Path) -> None:
9393
Args:
9494
path: Directory path to write index files into.
9595
"""
96+
import json
97+
import time
98+
from dataclasses import asdict
99+
96100
path = Path(path)
97101
path.mkdir(parents=True, exist_ok=True)
98-
102+
99103
# Save vector store
100104
self.vector_store.save(path / "vectors")
101-
102-
# Save chunk metadata (BM25 tokens and content)
103-
import json
104-
metadata = {
105-
"schema_version": self.SCHEMA_VERSION,
106-
"chunks": [
107-
{
108-
"id": c.id,
109-
"entity_id": c.entity_id,
110-
"content": c.content,
111-
"tokens": c.tokens,
112-
"metadata": c.metadata
113-
}
114-
for c in self.chunk_repo._chunks.values()
115-
]
116-
}
117-
with open(path / "chunks.json", "w") as f:
118-
json.dump(metadata, f)
119105

120-
# Save index manifest for compatibility checks at query time.
121-
from dataclasses import asdict
122-
import time
106+
# Save chunk metadata via repository
107+
self.chunk_repo.save(path)
123108

109+
# Save index manifest for compatibility checks at query time.
124110
self.manifest = {
125111
"schema_version": self.SCHEMA_VERSION,
126112
"version": 1,
@@ -160,22 +146,8 @@ def load(self, path: str | Path) -> None:
160146
"version": self.LEGACY_MANIFEST_VERSION,
161147
}
162148

163-
# Load chunks
164-
from knowcode.models import CodeChunk # type: ignore
165-
166-
chunks_file = path / "chunks.json"
167-
if chunks_file.exists():
168-
with open(chunks_file) as f:
169-
data = json.load(f)
170-
if isinstance(data, dict):
171-
validated_chunks = self._validate_and_migrate_chunks_payload(data)
172-
chunk_entries = validated_chunks.get("chunks", [])
173-
if isinstance(chunk_entries, list):
174-
for c_data in chunk_entries:
175-
if not isinstance(c_data, dict):
176-
continue
177-
chunk = CodeChunk(**c_data)
178-
self.chunk_repo.add(chunk)
149+
# Load chunks via repository
150+
self.chunk_repo.load(path)
179151

180152
@classmethod
181153
def _validate_and_migrate_manifest(cls, manifest: dict[str, Any]) -> dict[str, Any]:
@@ -260,12 +232,13 @@ def remove_file(self, file_path: str | Path) -> None:
260232
file_path_str = str(Path(file_path).resolve())
261233
removed_ids = self.chunk_repo.remove_by_file(file_path_str)
262234
if removed_ids:
263-
# Rebuild vector store with remaining chunks
264-
remaining_chunks = list(self.chunk_repo._chunks.values())
265-
self.vector_store.clear()
266-
for c in remaining_chunks:
267-
if c.embedding:
268-
self.vector_store.add(c.id, c.embedding)
235+
# Rebuild the vector store from remaining chunks if embeddings are available.
236+
remaining_chunks = self.chunk_repo.get_all()
237+
if any(c.embedding for c in remaining_chunks):
238+
self.vector_store.clear()
239+
for c in remaining_chunks:
240+
if c.embedding:
241+
self.vector_store.add(c.id, c.embedding)
269242

270243
def index_file(self, file_path: str | Path) -> int:
271244

src/knowcode/retrieval/reranker.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ def __init__(
3030
"""
3131
self.voyage_client = None
3232
self.model = "rerank-2.5"
33+
34+
if config and config.reranking_models:
35+
self.model = config.reranking_models[0].name
36+
if not api_key_env:
37+
api_key_env = config.reranking_models[0].api_key_env
3338

3439
if use_voyageai:
35-
# Determine API key env from config or default
36-
if config and config.reranking_models:
37-
api_key_env = config.reranking_models[0].api_key_env
38-
self.model = config.reranking_models[0].name
39-
else:
40-
api_key_env = api_key_env or "VOYAGE_API_KEY_1"
40+
if not api_key_env:
41+
api_key_env = "VOYAGE_API_KEY_1"
4142

4243
# Try to initialize VoyageAI client
4344
try:

src/knowcode/service.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,17 @@ def get_indexer(self, index_path: Optional[str | Path] = None) -> "Indexer":
139139
if self._indexer is None:
140140
from knowcode.llm.embedding import create_embedding_provider
141141
from knowcode.indexing.indexer import Indexer
142+
from knowcode.storage.sqlite_chunk_repository import SqliteChunkRepository
142143

143144
provider = create_embedding_provider(app_config=self.app_config)
144-
self._indexer = Indexer(provider)
145+
resolved_index_path = Path(index_path) if index_path else self._index_path()
146+
db_path = resolved_index_path / "chunks.db"
147+
chunk_repo = SqliteChunkRepository(db_path)
145148

146-
if index_path:
147-
self._indexer.load(Path(index_path))
148-
else:
149-
default_index = self._index_path()
150-
if default_index.exists():
151-
self._indexer.load(default_index)
149+
self._indexer = Indexer(provider, chunk_repo=chunk_repo)
150+
151+
if resolved_index_path.exists():
152+
self._indexer.load(resolved_index_path)
152153

153154
return self._indexer
154155

@@ -294,11 +295,23 @@ def _build_index(self, directory: str | Path, index_path: str | Path) -> int:
294295
"""Build a semantic index for a directory and persist it."""
295296
from knowcode.llm.embedding import create_embedding_provider
296297
from knowcode.indexing.indexer import Indexer
298+
from knowcode.storage.sqlite_chunk_repository import SqliteChunkRepository
297299

298300
provider = create_embedding_provider(app_config=self.app_config)
299-
indexer = Indexer(provider)
301+
resolved_index_path = Path(index_path)
302+
303+
# Clear/initialize directory
304+
import shutil
305+
if resolved_index_path.exists():
306+
shutil.rmtree(resolved_index_path)
307+
resolved_index_path.mkdir(parents=True, exist_ok=True)
308+
309+
db_path = resolved_index_path / "chunks.db"
310+
chunk_repo = SqliteChunkRepository(db_path)
311+
312+
indexer = Indexer(provider, chunk_repo=chunk_repo)
300313
count = indexer.index_directory(directory)
301-
indexer.save(index_path)
314+
indexer.save(resolved_index_path)
302315
self._indexer = indexer
303316
return count
304317

@@ -538,7 +551,7 @@ def get_stats(self) -> dict[str, Any]:
538551

539552
# Add index stats if indexer is loaded
540553
if self._indexer:
541-
stats["total_chunks"] = len(self._indexer.chunk_repo._chunks)
554+
stats["total_chunks"] = self._indexer.chunk_repo.count()
542555
if (
543556
hasattr(self._indexer.vector_store, "index")
544557
and self._indexer.vector_store.index

0 commit comments

Comments
 (0)