Files
llm/scripts/build_1c_rag_vector_index.py

208 lines
7.9 KiB
Python

from __future__ import annotations
import argparse
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from common import corpus_content_hash, pack_float_vector, read_jsonl
from rag_embedding_providers import LOCAL_HASHING_MODEL, LOCAL_HASHING_PROVIDER, embed_texts, provider_metadata
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CORPUS = ROOT / "plugins" / "1c" / "datasets" / "prepared" / "rag_corpus.jsonl"
DEFAULT_OUTPUT = ROOT / "plugins" / "1c" / "datasets" / "prepared" / "rag_vector_index.sqlite"
SCHEMA_VERSION = 1
DEFAULT_EMBEDDING_MODEL = LOCAL_HASHING_MODEL
def connect_index(path: Path) -> sqlite3.Connection:
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
return conn
def reset_schema(conn: sqlite3.Connection) -> None:
conn.executescript(
"""
DROP TABLE IF EXISTS vector_documents;
DROP TABLE IF EXISTS vector_meta;
CREATE TABLE vector_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE vector_documents (
id TEXT PRIMARY KEY,
document_id TEXT,
source_path TEXT,
source_type TEXT,
title TEXT,
chunk_index INTEGER,
content TEXT NOT NULL,
metadata_json TEXT NOT NULL,
vector BLOB NOT NULL
);
CREATE INDEX idx_vector_documents_source_type ON vector_documents(source_type);
CREATE INDEX idx_vector_documents_source_path ON vector_documents(source_path);
"""
)
def write_meta(conn: sqlite3.Connection, metadata: dict[str, object]) -> None:
conn.executemany(
"INSERT INTO vector_meta(key, value) VALUES(?, ?)",
[(key, json.dumps(value, ensure_ascii=False, sort_keys=True)) for key, value in metadata.items()],
)
def document_embedding_text(record: dict) -> str:
title = str(record.get("title") or "").strip()
content = str(record.get("content") or "").strip()
metadata = record.get("metadata") if isinstance(record.get("metadata"), dict) else {}
headings = metadata.get("headings") if isinstance(metadata.get("headings"), list) else []
heading_text = "\n".join(str(item) for item in headings if str(item).strip())
return "\n\n".join(part for part in (title, heading_text, content) if part)
def batched(items: list[dict], size: int) -> list[list[dict]]:
return [items[index : index + size] for index in range(0, len(items), size)]
def build_vector_index(
corpus_path: Path,
output_path: Path,
*,
dimensions: int,
embedding_model: str,
embedding_provider: str = LOCAL_HASHING_PROVIDER,
embedding_base_url: str = "",
embedding_api_key_env: str = "OPENAI_API_KEY",
batch_size: int = 16,
) -> dict:
records = read_jsonl(corpus_path)
corpus_hash = corpus_content_hash(records)
if not records:
inferred_dimensions = dimensions
else:
sample_vector = embed_texts(
[document_embedding_text(records[0])],
provider=embedding_provider,
model=embedding_model,
dimensions=dimensions,
base_url=embedding_base_url,
api_key_env=embedding_api_key_env,
)[0]
inferred_dimensions = len(sample_vector)
conn = connect_index(output_path)
try:
with conn:
reset_schema(conn)
embedding_meta = provider_metadata(
provider=embedding_provider,
model=embedding_model,
dimensions=inferred_dimensions,
base_url=embedding_base_url,
)
write_meta(
conn,
{
"schema": "onec_rag_vector_index.v1",
"schema_version": SCHEMA_VERSION,
"type": "sqlite-vector-scan",
**embedding_meta,
"corpus_path": str(corpus_path),
"corpus_hash": corpus_hash,
"doc_count": len(records),
"built_at": datetime.now(timezone.utc).isoformat(),
},
)
rows = []
for batch in batched(records, max(int(batch_size or 1), 1)):
texts = [document_embedding_text(record) for record in batch]
vectors = embed_texts(
texts,
provider=embedding_provider,
model=embedding_model,
dimensions=inferred_dimensions,
base_url=embedding_base_url,
api_key_env=embedding_api_key_env,
)
for record, vector in zip(batch, vectors):
if len(vector) != inferred_dimensions:
raise ValueError(f"Embedding dimensions changed within the build: {len(vector)} != {inferred_dimensions}")
metadata = record.get("metadata") if isinstance(record.get("metadata"), dict) else {}
rows.append(
(
str(record.get("id") or ""),
str(record.get("document_id") or ""),
str(record.get("source_path") or ""),
str(metadata.get("source_type") or ""),
str(record.get("title") or ""),
int(record.get("chunk_index") or 0),
str(record.get("content") or ""),
json.dumps(metadata, ensure_ascii=False, sort_keys=True),
pack_float_vector(vector),
)
)
conn.executemany(
"""
INSERT INTO vector_documents(
id, document_id, source_path, source_type, title, chunk_index,
content, metadata_json, vector
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
rows,
)
finally:
conn.close()
return {
"status": "ok",
"index": str(output_path),
"doc_count": len(records),
"corpus_hash": corpus_hash,
"embedding_provider": provider_metadata(provider=embedding_provider, model=embedding_model, dimensions=inferred_dimensions, base_url=embedding_base_url)["embedding_provider"],
"embedding_model": embedding_model,
"embedding_dimensions": inferred_dimensions,
}
def main() -> int:
parser = argparse.ArgumentParser(description="Build a local SQLite vector index for the 1C RAG corpus.")
parser.add_argument("--corpus", type=Path, default=DEFAULT_CORPUS)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
parser.add_argument("--dimensions", type=int, default=384)
parser.add_argument("--embedding-provider", default=LOCAL_HASHING_PROVIDER, choices=[LOCAL_HASHING_PROVIDER, "openai-compatible"])
parser.add_argument("--embedding-model", default=DEFAULT_EMBEDDING_MODEL)
parser.add_argument("--embedding-base-url", default="")
parser.add_argument("--embedding-api-key-env", default="OPENAI_API_KEY")
parser.add_argument("--batch-size", type=int, default=16)
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
result = build_vector_index(
args.corpus,
args.output,
dimensions=args.dimensions,
embedding_model=args.embedding_model,
embedding_provider=args.embedding_provider,
embedding_base_url=args.embedding_base_url,
embedding_api_key_env=args.embedding_api_key_env,
batch_size=args.batch_size,
)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"Wrote vector index with {result['doc_count']} document chunk(s) to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())