Skip to content

Commit c41aa39

Browse files
committed
chore: treat chatbot faiss index as generated state
1 parent c17e396 commit c41aa39

7 files changed

Lines changed: 207 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ __pycache__
1313
sql_app.db
1414
uploads
1515
pytorch_connectomics
16+
server_api/chatbot/faiss_index/

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ PYTC_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,null
3737
PYTC_NEUROGLANCER_PUBLIC_BASE=http://localhost:4244
3838
```
3939

40+
## Chatbot Docs Index
41+
42+
The chatbot's FAISS index is generated locally from the markdown files in
43+
`server_api/chatbot/file_summaries/` and should not be committed to git.
44+
45+
When you update those markdown docs, rebuild the generated index with:
46+
47+
```
48+
uv run python server_api/chatbot/update_faiss.py
49+
```
50+
51+
You can override the embeddings endpoint if needed:
52+
53+
```
54+
OLLAMA_BASE_URL=http://localhost:11434 uv run python server_api/chatbot/update_faiss.py
55+
```
56+
4057
If restarting after a crash or interrupted session, kill any lingering processes first:
4158

4259
```

server_api/chatbot/chatbot.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from langchain_core.tools import tool
1515
from langchain.agents import create_agent
1616
from server_api.utils.utils import process_path
17+
from server_api.chatbot.update_faiss import ensure_faiss_index
1718
from server_api.chatbot.tools import (
1819
list_training_configs,
1920
read_config,
@@ -124,6 +125,11 @@ def build_chain():
124125
llm = ChatOllama(model=ollama_model, base_url=ollama_base_url, temperature=0)
125126
embeddings = OllamaEmbeddings(model=ollama_embed_model, base_url=ollama_base_url)
126127
faiss_path = process_path("server_api/chatbot/faiss_index")
128+
if ensure_faiss_index(
129+
model=ollama_embed_model,
130+
base_url=ollama_base_url,
131+
):
132+
print(f"[SEARCH] Generated chatbot FAISS index at {faiss_path}")
127133
vectorstore = FAISS.load_local(
128134
faiss_path,
129135
embeddings,
@@ -301,6 +307,11 @@ def build_helper_chain():
301307
llm = ChatOllama(model=ollama_model, base_url=ollama_base_url, temperature=0)
302308
embeddings = OllamaEmbeddings(model=ollama_embed_model, base_url=ollama_base_url)
303309
faiss_path = process_path("server_api/chatbot/faiss_index")
310+
if ensure_faiss_index(
311+
model=ollama_embed_model,
312+
base_url=ollama_base_url,
313+
):
314+
print(f"[SEARCH] Generated chatbot FAISS index at {faiss_path}")
304315
vectorstore = FAISS.load_local(
305316
faiss_path,
306317
embeddings,
-544 KB
Binary file not shown.
-30.6 KB
Binary file not shown.

server_api/chatbot/update_faiss.py

Lines changed: 92 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,49 @@
1-
# How to update faiss_index:
2-
# 1. Edit the markdown files in server_api/chatbot/file_summaries/ as needed.
3-
# These are end-user-focused guides (one per application page/feature) that
4-
# serve as the knowledge base for the RAG chatbot.
5-
# 2. Run this script:
6-
# python server_api/chatbot/update_faiss.py
7-
#
8-
# You can override the embeddings model and Ollama base URL via:
9-
# - Environment variables: OLLAMA_EMBED_MODEL, OLLAMA_BASE_URL
10-
# - CLI arguments: --model, --base-url
11-
12-
import os
131
import argparse
2+
import os
143
from pathlib import Path
15-
from langchain_core.documents import Document
16-
from langchain_text_splitters import RecursiveCharacterTextSplitter
17-
from langchain_community.vectorstores import FAISS
18-
from langchain_ollama import OllamaEmbeddings
4+
from typing import Optional, Tuple
195

6+
DEFAULT_OLLAMA_BASE_URL = "http://cscigpu08.bc.edu:4443"
7+
DEFAULT_OLLAMA_EMBED_MODEL = "qwen3-embedding:8b"
8+
INDEX_FILENAMES = ("index.faiss", "index.pkl")
9+
10+
11+
def get_chatbot_paths(base_dir: Optional[Path] = None) -> Tuple[Path, Path]:
12+
root = (base_dir or Path(__file__).parent).resolve()
13+
return root / "file_summaries", root / "faiss_index"
2014

21-
def main():
22-
# Parse CLI arguments
23-
parser = argparse.ArgumentParser(
24-
description="Update FAISS index for RAG chatbot documentation search"
25-
)
26-
parser.add_argument(
27-
"--model",
28-
default=None,
29-
help="Ollama embeddings model (default: from OLLAMA_EMBED_MODEL env or 'qwen3-embedding:8b')",
30-
)
31-
parser.add_argument(
32-
"--base-url",
33-
default=None,
34-
help="Ollama base URL (default: from OLLAMA_BASE_URL env or 'http://cscigpu08.bc.edu:4443')",
35-
)
36-
args = parser.parse_args()
3715

38-
# Use same defaults as build_chain() in chatbot.py
39-
embed_model = args.model or os.getenv("OLLAMA_EMBED_MODEL", "qwen3-embedding:8b")
40-
base_url = args.base_url or os.getenv(
41-
"OLLAMA_BASE_URL", "http://cscigpu08.bc.edu:4443"
16+
def resolve_ollama_settings(
17+
model: Optional[str] = None, base_url: Optional[str] = None
18+
) -> Tuple[str, str]:
19+
embed_model = model or os.getenv("OLLAMA_EMBED_MODEL", DEFAULT_OLLAMA_EMBED_MODEL)
20+
resolved_base_url = base_url or os.getenv(
21+
"OLLAMA_BASE_URL", DEFAULT_OLLAMA_BASE_URL
4222
)
23+
return embed_model, resolved_base_url
4324

44-
print(f"Using embeddings model: {embed_model}")
45-
print(f"Using Ollama base URL: {base_url}")
4625

47-
script_directory = Path(__file__).parent.resolve()
48-
summaries_directory = script_directory / "file_summaries"
49-
faiss_directory = script_directory / "faiss_index"
26+
def faiss_index_exists(faiss_directory: Path) -> bool:
27+
return all((faiss_directory / name).is_file() for name in INDEX_FILENAMES)
28+
29+
30+
def build_faiss_index(
31+
summaries_directory: Path,
32+
faiss_directory: Path,
33+
*,
34+
model: Optional[str] = None,
35+
base_url: Optional[str] = None,
36+
):
37+
from langchain_core.documents import Document
38+
from langchain_text_splitters import RecursiveCharacterTextSplitter
39+
from langchain_community.vectorstores import FAISS
40+
from langchain_ollama import OllamaEmbeddings
41+
42+
embed_model, resolved_base_url = resolve_ollama_settings(model, base_url)
43+
44+
print(f"Using embeddings model: {embed_model}")
45+
print(f"Using Ollama base URL: {resolved_base_url}")
5046

51-
# Load full documents
5247
documents = []
5348
for md_file in summaries_directory.rglob("*.md"):
5449
summary = md_file.read_text(encoding="utf-8")
@@ -60,7 +55,6 @@ def main():
6055
)
6156
)
6257

63-
# Split into chunks for better embedding quality
6458
text_splitter = RecursiveCharacterTextSplitter(
6559
chunk_size=1000,
6660
chunk_overlap=200,
@@ -73,12 +67,66 @@ def main():
7367
f" - {c.metadata['source']} (start={c.metadata.get('start_index', '?')}, {len(c.page_content)} chars)"
7468
)
7569

76-
embeddings = OllamaEmbeddings(model=embed_model, base_url=base_url)
70+
embeddings = OllamaEmbeddings(model=embed_model, base_url=resolved_base_url)
7771
vectorstore = FAISS.from_documents(chunks, embeddings)
7872
faiss_directory.mkdir(parents=True, exist_ok=True)
7973
vectorstore.save_local(str(faiss_directory))
8074
print(f"FAISS index saved with {vectorstore.index.ntotal} vectors")
8175

8276

77+
def ensure_faiss_index(
78+
*,
79+
summaries_directory: Optional[Path] = None,
80+
faiss_directory: Optional[Path] = None,
81+
model: Optional[str] = None,
82+
base_url: Optional[str] = None,
83+
) -> bool:
84+
default_summaries_directory, default_faiss_directory = get_chatbot_paths()
85+
summaries_directory = summaries_directory or default_summaries_directory
86+
faiss_directory = faiss_directory or default_faiss_directory
87+
88+
if faiss_index_exists(faiss_directory):
89+
return False
90+
91+
build_faiss_index(
92+
summaries_directory,
93+
faiss_directory,
94+
model=model,
95+
base_url=base_url,
96+
)
97+
return True
98+
99+
100+
def main():
101+
parser = argparse.ArgumentParser(
102+
description="Rebuild the generated FAISS index for chatbot documentation search"
103+
)
104+
parser.add_argument(
105+
"--model",
106+
default=None,
107+
help=(
108+
"Ollama embeddings model "
109+
f"(default: OLLAMA_EMBED_MODEL or '{DEFAULT_OLLAMA_EMBED_MODEL}')"
110+
),
111+
)
112+
parser.add_argument(
113+
"--base-url",
114+
default=None,
115+
help=(
116+
"Ollama base URL "
117+
f"(default: OLLAMA_BASE_URL or '{DEFAULT_OLLAMA_BASE_URL}')"
118+
),
119+
)
120+
args = parser.parse_args()
121+
122+
summaries_directory, faiss_directory = get_chatbot_paths()
123+
build_faiss_index(
124+
summaries_directory,
125+
faiss_directory,
126+
model=args.model,
127+
base_url=args.base_url,
128+
)
129+
130+
83131
if __name__ == "__main__":
84132
main()
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from server_api.chatbot import update_faiss
2+
3+
4+
def test_faiss_index_exists_requires_both_files(tmp_path):
5+
faiss_dir = tmp_path / "faiss_index"
6+
faiss_dir.mkdir()
7+
8+
assert update_faiss.faiss_index_exists(faiss_dir) is False
9+
10+
(faiss_dir / "index.faiss").write_text("stub")
11+
assert update_faiss.faiss_index_exists(faiss_dir) is False
12+
13+
(faiss_dir / "index.pkl").write_text("stub")
14+
assert update_faiss.faiss_index_exists(faiss_dir) is True
15+
16+
17+
def test_ensure_faiss_index_builds_when_missing(tmp_path, monkeypatch):
18+
summaries_dir = tmp_path / "summaries"
19+
faiss_dir = tmp_path / "faiss_index"
20+
summaries_dir.mkdir()
21+
calls = []
22+
23+
def fake_build(summaries_directory, target_directory, *, model=None, base_url=None):
24+
calls.append((summaries_directory, target_directory, model, base_url))
25+
target_directory.mkdir(parents=True, exist_ok=True)
26+
(target_directory / "index.faiss").write_text("stub")
27+
(target_directory / "index.pkl").write_text("stub")
28+
29+
monkeypatch.setattr(update_faiss, "build_faiss_index", fake_build)
30+
31+
generated = update_faiss.ensure_faiss_index(
32+
summaries_directory=summaries_dir,
33+
faiss_directory=faiss_dir,
34+
model="embed-model",
35+
base_url="http://example.test:11434",
36+
)
37+
38+
assert generated is True
39+
assert calls == [
40+
(summaries_dir, faiss_dir, "embed-model", "http://example.test:11434")
41+
]
42+
assert update_faiss.faiss_index_exists(faiss_dir) is True
43+
44+
45+
def test_ensure_faiss_index_skips_when_present(tmp_path, monkeypatch):
46+
summaries_dir = tmp_path / "summaries"
47+
faiss_dir = tmp_path / "faiss_index"
48+
summaries_dir.mkdir()
49+
faiss_dir.mkdir()
50+
(faiss_dir / "index.faiss").write_text("stub")
51+
(faiss_dir / "index.pkl").write_text("stub")
52+
53+
def fail_build(*args, **kwargs):
54+
raise AssertionError("build_faiss_index should not be called")
55+
56+
monkeypatch.setattr(update_faiss, "build_faiss_index", fail_build)
57+
58+
generated = update_faiss.ensure_faiss_index(
59+
summaries_directory=summaries_dir,
60+
faiss_directory=faiss_dir,
61+
)
62+
63+
assert generated is False
64+
65+
66+
def test_resolve_ollama_settings_uses_env_defaults(monkeypatch):
67+
monkeypatch.delenv("OLLAMA_EMBED_MODEL", raising=False)
68+
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
69+
70+
model, base_url = update_faiss.resolve_ollama_settings()
71+
72+
assert model == update_faiss.DEFAULT_OLLAMA_EMBED_MODEL
73+
assert base_url == update_faiss.DEFAULT_OLLAMA_BASE_URL
74+
75+
76+
def test_resolve_ollama_settings_prefers_explicit_values(monkeypatch):
77+
monkeypatch.setenv("OLLAMA_EMBED_MODEL", "env-model")
78+
monkeypatch.setenv("OLLAMA_BASE_URL", "http://env.test:9999")
79+
80+
model, base_url = update_faiss.resolve_ollama_settings(
81+
model="cli-model",
82+
base_url="http://cli.test:8888",
83+
)
84+
85+
assert model == "cli-model"
86+
assert base_url == "http://cli.test:8888"

0 commit comments

Comments
 (0)