Skip to content

Commit 73769f2

Browse files
authored
feat(lc_ask): support index flag without key (#204)
1 parent 550bea6 commit 73769f2

2 files changed

Lines changed: 93 additions & 19 deletions

File tree

src/langchain/lc_ask.py

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,27 @@
2727

2828
from src.langchain.retriever_factory import make_retriever
2929
from src.langchain.trace import configure_emitter
30+
_FAISS_DIR_PATTERN = re.compile(r"^faiss_(?P<key>.+?)__(?P<embed>.+)$")
31+
32+
3033
def _fs_safe(value: str) -> str:
3134
"""Return a filesystem-safe slug for embedding model names."""
3235

3336
return re.sub(r"[^a-zA-Z0-9._-]+", "-", value)
3437

3538

39+
def _infer_key_from_faiss_dir(path: Path) -> str | None:
40+
"""Attempt to infer the sanitized key from a FAISS directory name."""
41+
42+
name = path.name
43+
if name.endswith("_repacked"):
44+
name = name[: -len("_repacked")]
45+
match = _FAISS_DIR_PATTERN.match(name)
46+
if match:
47+
return match.group("key")
48+
return None
49+
50+
3651
ROOT = project_root
3752

3853
MODES_REQUIRING_CHUNKS = {"bm25", "hybrid", "parent", "hybrid+compression"}
@@ -239,6 +254,7 @@ def main():
239254
key_group.add_argument(
240255
"--index",
241256
dest="index_path",
257+
242258
help="Path to FAISS index directory (faiss_<key>__<embed_model>)",
243259
)
244260
parser.add_argument("--embed-model", default="BAAI/bge-small-en-v1.5")
@@ -314,8 +330,10 @@ def main():
314330
index_dir = Path(args.index_dir).expanduser()
315331

316332
docs: list[Document] | None = None
333+
334+
key_safe: str | None = _fs_safe(args.key) if args.key else None
335+
317336
key_arg = args.key
318-
key_safe = _fs_safe(key_arg) if key_arg else None
319337
expected_chunks, base_dir, repacked_dir, key_safe = _prepare_index_locations(
320338
key_safe=key_safe,
321339
index_path=args.index_path,
@@ -325,27 +343,56 @@ def main():
325343
)
326344

327345
# Prefer a repacked/merged index if available
346+
328347
faiss_dir: Path | None = None
329-
for cand in (repacked_dir, base_dir):
330-
if (cand / "index.faiss").exists():
331-
faiss_dir = cand
332-
break
348+
expected_chunks: Path | None = None
333349

334-
if faiss_dir is None:
335-
if base_dir.exists():
336-
shards = [p for p in base_dir.iterdir() if p.is_dir()]
337-
if shards:
338-
raise SystemExit(
339-
f"[lc_ask] FAISS shards found but no merged index: {base_dir}\n"
340-
" • Merge shards before querying (merge step not completed)"
341-
)
342-
raise SystemExit(
343-
"[lc_ask] FAISS dir not found: "
344-
f"{base_dir} (or repacked: {repacked_dir}).\n"
345-
f" • If you upgraded LangChain, try: make repack-faiss KEY={args.key} EMBED_MODEL={args.embed_model}\n"
346-
f" • Or rebuild the index: python src/langchain/lc_build_index.py {args.key}"
350+
if args.index_path:
351+
faiss_dir = Path(args.index_path).expanduser()
352+
if not faiss_dir.exists():
353+
raise SystemExit(f"[lc_ask] FAISS dir not found: {faiss_dir}")
354+
if not (faiss_dir / "index.faiss").exists():
355+
raise SystemExit(
356+
f"[lc_ask] index.faiss not found in {faiss_dir}. Provide a merged FAISS directory"
357+
)
358+
inferred_key = _infer_key_from_faiss_dir(faiss_dir)
359+
if inferred_key:
360+
key_safe = inferred_key
361+
if key_safe:
362+
expected_chunks = chunks_dir / f"lc_chunks_{key_safe}.jsonl"
363+
else:
364+
key_safe = _fs_safe(args.key)
365+
expected_chunks, base_dir, repacked_dir = _resolve_paths(
366+
key=key_safe,
367+
embed_model=args.embed_model,
368+
chunks_dir=chunks_dir,
369+
index_dir=index_dir,
347370
)
348371

372+
# Prefer a repacked/merged index if available
373+
for cand in (repacked_dir, base_dir):
374+
if (cand / "index.faiss").exists():
375+
faiss_dir = cand
376+
break
377+
378+
if faiss_dir is None:
379+
if base_dir.exists():
380+
shards = [p for p in base_dir.iterdir() if p.is_dir()]
381+
if shards:
382+
raise SystemExit(
383+
f"[lc_ask] FAISS shards found but no merged index: {base_dir}\n"
384+
" • Merge shards before querying (merge step not completed)"
385+
)
386+
raise SystemExit(
387+
"[lc_ask] FAISS dir not found: "
388+
f"{base_dir} (or repacked: {repacked_dir}).\n"
389+
f" • If you upgraded LangChain, try: make repack-faiss KEY={args.key} EMBED_MODEL={args.embed_model}\n"
390+
f" • Or rebuild the index: python src/langchain/lc_build_index.py {args.key}"
391+
)
392+
393+
if faiss_dir is None:
394+
raise SystemExit("[lc_ask] Unable to resolve FAISS directory")
395+
349396
chunks_path = _locate_chunks_file(
350397
explicit_path=args.chunks_file,
351398
chunks_dir=chunks_dir,
@@ -360,10 +407,16 @@ def main():
360407
f"[lc_ask] chunks not found: {expected_chunks} – run lc_build_index for KEY={args.key}"
361408
)
362409
else:
410+
if expected_chunks is None:
411+
raise SystemExit(
412+
f"[lc_ask] chunks not found for index at {faiss_dir}. Provide --chunks-file"
413+
)
363414
key_hint = key_arg or (key_safe if key_safe is not None else str(faiss_dir))
415+
display_key = args.key if args.key else key_safe
364416
raise SystemExit(
365417
"[lc_ask] chunks not found: "
366418
f"{expected_chunks} – run lc_build_index for {key_hint}"
419+
367420
)
368421

369422

tests/langchain/test_lc_ask_cli.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,18 @@ class DummyLLM:
391391
assert faiss_call["path"] == faiss_dir
392392

393393

394+
def test_lc_ask_accepts_index_argument(monkeypatch, tmp_path):
395+
_install_dummy_langchain_modules(monkeypatch)
396+
lc_ask = importlib.import_module("src.langchain.lc_ask")
397+
398+
key = "custom/index"
399+
safe_key = "custom-index"
400+
question = "How does neuroplasticity work?"
401+
402+
chunks_dir = tmp_path / "chunks"
403+
chunks_dir.mkdir()
404+
chunk_file = chunks_dir / f"lc_chunks_{safe_key}.jsonl"
405+
394406
def test_lc_ask_accepts_explicit_index_directory(monkeypatch, tmp_path):
395407
_install_dummy_langchain_modules(monkeypatch)
396408
lc_ask = importlib.import_module("src.langchain.lc_ask")
@@ -407,18 +419,26 @@ def test_lc_ask_accepts_explicit_index_directory(monkeypatch, tmp_path):
407419
faiss_dir = index_dir / f"faiss_{safe_key}__{embed_safe}"
408420
faiss_dir.mkdir(parents=True, exist_ok=True)
409421
(faiss_dir / "index.faiss").write_text("", encoding="utf-8")
410-
chunk_file = faiss_dir / f"lc_chunks_{safe_key}.jsonl"
411422
chunk_file.write_text(
412423
json.dumps({"text": "Sample", "metadata": {}}) + "\n",
413424
encoding="utf-8",
414425
)
415426

427+
428+
storage_dir = tmp_path / "storage"
429+
faiss_dir = storage_dir / f"faiss_{safe_key}__BAAI-bge-small-en-v1.5"
430+
faiss_dir.mkdir(parents=True, exist_ok=True)
431+
(faiss_dir / "index.faiss").write_text("", encoding="utf-8")
432+
416433
class DummyEmbeddings:
417434
pass
418435

419436
class DummyVectorStore:
420437
pass
421438

439+
440+
monkeypatch.setattr(lc_ask, "HuggingFaceEmbeddings", lambda model_name: DummyEmbeddings())
441+
422442
chunk_call: dict[str, Path] = {}
423443
faiss_call: dict[str, Path] = {}
424444

@@ -432,6 +452,7 @@ def fake_load_chunks(path):
432452
chunk_call["path"] = Path(path)
433453
return [lc_ask.Document(page_content="Sample", metadata={})]
434454

455+
435456
def fake_load_local(path, *args, **kwargs):
436457
faiss_call["path"] = Path(path)
437458
return DummyVectorStore()

0 commit comments

Comments
 (0)