OpenMC/.claude/tools/rag/indexer.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

137 lines
4.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Build the RAG vector index for the OpenMC codebase.
This is the index-building half of the RAG pipeline. All operations are local
once the embedding model has been downloaded and cached (see embeddings.py for
details on model download, caching, and network behavior). It walks the repo,
chunks every
C++/Python/RST file (via chunker.py), embeds all chunks into 384-dim vectors
(via embeddings.py), and stores them in a local LanceDB database on disk. The
result is a .claude/cache/rag_index/ directory containing two tables "code"
and "docs" that openmc_search.py queries at search time.
Building the full index takes ~5 minutes on a 10-core machine. The bottleneck
is the embedding step (running all chunks through the MiniLM model on CPU).
Can be run standalone: python indexer.py
Or called programmatically: from indexer import build_index; build_index()
The MCP server (openmc_mcp_server.py) uses the latter when the agent calls
openmc_rag_rebuild.
"""
import lancedb
import sys
import time
from pathlib import Path
# This file lives at .claude/tools/rag/indexer.py. The sys.path insert lets
# us import sibling modules (embeddings, chunker) when run as a standalone
# script. When imported from the MCP server, the server has already done this.
TOOLS_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(TOOLS_DIR / "rag"))
from embeddings import EmbeddingProvider
from chunker import chunk_file
OPENMC_ROOT = Path(__file__).resolve().parents[3]
CACHE_DIR = OPENMC_ROOT / ".claude" / "cache"
INDEX_DIR = CACHE_DIR / "rag_index"
CODE_PATTERNS = [
"src/**/*.cpp",
"include/openmc/**/*.h",
"openmc/**/*.py",
"tests/**/*.py",
"examples/**/*.py",
]
DOC_PATTERNS = [
"docs/**/*.rst",
]
def collect_chunks(patterns, openmc_root):
"""Collect all chunks from files matching the given patterns."""
chunks = []
for pattern in patterns:
for filepath in sorted(openmc_root.glob(pattern)):
if "__pycache__" in str(filepath):
continue
file_chunks = chunk_file(filepath, openmc_root)
chunks.extend(file_chunks)
return chunks
def build_index():
"""Build or rebuild the complete vector index."""
start = time.time()
# Collect all chunks
print("Collecting code chunks...")
code_chunks = collect_chunks(CODE_PATTERNS, OPENMC_ROOT)
print(f" {len(code_chunks)} code chunks")
print("Collecting doc chunks...")
doc_chunks = collect_chunks(DOC_PATTERNS, OPENMC_ROOT)
print(f" {len(doc_chunks)} doc chunks")
all_chunks = code_chunks + doc_chunks
if not all_chunks:
print("ERROR: No chunks collected!", file=sys.stderr)
sys.exit(1)
# Create embeddings
all_texts = [c["text"] for c in all_chunks]
print("Creating embedding provider...")
embedder = EmbeddingProvider()
print(f" dim={embedder.dim}")
print("Embedding chunks...")
all_embeddings = embedder.embed(all_texts)
# Build LanceDB tables
INDEX_DIR.mkdir(parents=True, exist_ok=True)
db = lancedb.connect(str(INDEX_DIR))
# Separate code vs doc records by index (code_chunks come first in all_chunks)
n_code = len(code_chunks)
code_records = []
doc_records = []
for i, (chunk, emb) in enumerate(zip(all_chunks, all_embeddings)):
record = {
"text": chunk["text"],
"filepath": chunk["filepath"],
"kind": chunk["kind"],
"symbol": chunk.get("symbol", ""),
"start_line": chunk.get("start_line", 0),
"end_line": chunk.get("end_line", 0),
"vector": emb,
}
if i < n_code:
code_records.append(record)
else:
doc_records.append(record)
# Create tables (drop existing)
result = db.table_names() if hasattr(db, "table_names") else db.list_tables()
existing = result.tables if hasattr(result, "tables") else list(result)
for table_name in ("code", "docs"):
if table_name in existing:
db.drop_table(table_name)
if code_records:
db.create_table("code", code_records)
print(f" Created 'code' table: {len(code_records)} rows")
if doc_records:
db.create_table("docs", doc_records)
print(f" Created 'docs' table: {len(doc_records)} rows")
elapsed = time.time() - start
print(f"Done in {elapsed:.1f}s")
if __name__ == "__main__":
build_index()