This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
"""[llm] configuration — stack.toml section + env overrides.
|
"""[llm] configuration — stack.toml section + env overrides.
|
||||||
|
|
||||||
Env contract:
|
Env contract:
|
||||||
LLM_OLLAMA_HOSTS comma-separated Ollama base URLs (beats [llm].ollama)
|
LLM_OLLAMA_HOSTS comma-separated Ollama base URLs (beats [llm].ollama)
|
||||||
LLM_PG_HOST beats [llm].pg_host (containers set this to "postgres")
|
LLM_PG_HOST beats [llm].pg_host (containers set this to "postgres")
|
||||||
LLM_DB_PASSWORD required for pg_url(); lives in .env, never stack.toml
|
LLM_DB_PASSWORD required for pg_url(); lives in .env, never stack.toml
|
||||||
|
LLM_BUILD_ANN_INDEX beats [llm].build_ann_index ("1"/"true"/"yes" = True)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -18,6 +19,7 @@ class LlmConfig:
|
|||||||
embed_model: str
|
embed_model: str
|
||||||
instruct_model: str
|
instruct_model: str
|
||||||
embed_dim: int
|
embed_dim: int
|
||||||
|
build_ann_index: bool
|
||||||
pg_host: str
|
pg_host: str
|
||||||
pg_port: int
|
pg_port: int
|
||||||
pg_db: str
|
pg_db: str
|
||||||
@@ -34,11 +36,17 @@ def load() -> LlmConfig:
|
|||||||
hosts = tuple(h.strip() for h in hosts_env.split(",") if h.strip())
|
hosts = tuple(h.strip() for h in hosts_env.split(",") if h.strip())
|
||||||
else:
|
else:
|
||||||
hosts = (str(section.ollama),)
|
hosts = (str(section.ollama),)
|
||||||
|
ann_env = os.environ.get("LLM_BUILD_ANN_INDEX")
|
||||||
|
if ann_env is not None:
|
||||||
|
build_ann_index = ann_env.strip().lower() in ("1", "true", "yes")
|
||||||
|
else:
|
||||||
|
build_ann_index = bool(section.build_ann_index)
|
||||||
return LlmConfig(
|
return LlmConfig(
|
||||||
ollama_hosts=hosts,
|
ollama_hosts=hosts,
|
||||||
embed_model=str(section.embed_model),
|
embed_model=str(section.embed_model),
|
||||||
instruct_model=str(section.instruct_model),
|
instruct_model=str(section.instruct_model),
|
||||||
embed_dim=int(section.embed_dim),
|
embed_dim=int(section.embed_dim),
|
||||||
|
build_ann_index=build_ann_index,
|
||||||
pg_host=os.environ.get("LLM_PG_HOST", str(section.pg_host)),
|
pg_host=os.environ.get("LLM_PG_HOST", str(section.pg_host)),
|
||||||
pg_port=int(section.pg_port),
|
pg_port=int(section.pg_port),
|
||||||
pg_db=str(section.pg_db),
|
pg_db=str(section.pg_db),
|
||||||
|
|||||||
@@ -134,5 +134,8 @@ def index_docs(
|
|||||||
if stats["indexed"] % 100 == 0:
|
if stats["indexed"] % 100 == 0:
|
||||||
log.info("indexed %(indexed)s (+%(chunks)s chunks)", stats)
|
log.info("indexed %(indexed)s (+%(chunks)s chunks)", stats)
|
||||||
|
|
||||||
ensure_hnsw(engine, cfg.embed_dim)
|
if cfg.build_ann_index:
|
||||||
|
ensure_hnsw(engine, cfg.embed_dim)
|
||||||
|
else:
|
||||||
|
log.info("ANN index skipped (build_ann_index=false); using exact search")
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ ollama = "http://127.0.0.1:11434" # host-side default; containers override v
|
|||||||
embed_model = "nomic-embed-text" # 768-dim; bake-off (P33 #564) may revise
|
embed_model = "nomic-embed-text" # 768-dim; bake-off (P33 #564) may revise
|
||||||
instruct_model = "llama3.1:8b" # bake-off may revise
|
instruct_model = "llama3.1:8b" # bake-off may revise
|
||||||
embed_dim = 768
|
embed_dim = 768
|
||||||
|
# ANN index build (hnsw/ivfflat) SIGILLs on this Zen2 host — the mirrored
|
||||||
|
# postgres image's pgvector is AVX-512. Leave false until the image is
|
||||||
|
# rebuilt; exact search works regardless.
|
||||||
|
build_ann_index = false
|
||||||
pg_host = "127.0.0.1" # host-side default; containers set LLM_PG_HOST=postgres
|
pg_host = "127.0.0.1" # host-side default; containers set LLM_PG_HOST=postgres
|
||||||
pg_port = 5432
|
pg_port = 5432
|
||||||
pg_db = "llm"
|
pg_db = "llm"
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ class TestLoad:
|
|||||||
def test_defaults_from_stack_toml(self, monkeypatch):
|
def test_defaults_from_stack_toml(self, monkeypatch):
|
||||||
monkeypatch.delenv("LLM_OLLAMA_HOSTS", raising=False)
|
monkeypatch.delenv("LLM_OLLAMA_HOSTS", raising=False)
|
||||||
monkeypatch.delenv("LLM_PG_HOST", raising=False)
|
monkeypatch.delenv("LLM_PG_HOST", raising=False)
|
||||||
|
monkeypatch.delenv("LLM_BUILD_ANN_INDEX", raising=False)
|
||||||
cfg = llm_config.load()
|
cfg = llm_config.load()
|
||||||
assert cfg.ollama_hosts == ("http://127.0.0.1:11434",)
|
assert cfg.ollama_hosts == ("http://127.0.0.1:11434",)
|
||||||
assert cfg.embed_model == "nomic-embed-text"
|
assert cfg.embed_model == "nomic-embed-text"
|
||||||
assert cfg.embed_dim == 768
|
assert cfg.embed_dim == 768
|
||||||
assert cfg.pg_host == "127.0.0.1"
|
assert cfg.pg_host == "127.0.0.1"
|
||||||
assert cfg.pg_db == "llm"
|
assert cfg.pg_db == "llm"
|
||||||
|
assert cfg.build_ann_index is False
|
||||||
|
|
||||||
def test_env_overrides(self, monkeypatch):
|
def test_env_overrides(self, monkeypatch):
|
||||||
monkeypatch.setenv(
|
monkeypatch.setenv(
|
||||||
@@ -26,6 +28,16 @@ class TestLoad:
|
|||||||
assert cfg.ollama_hosts == ("http://rig:11434", "http://laptop:11434")
|
assert cfg.ollama_hosts == ("http://rig:11434", "http://laptop:11434")
|
||||||
assert cfg.pg_host == "postgres"
|
assert cfg.pg_host == "postgres"
|
||||||
|
|
||||||
|
def test_build_ann_index_env_override_true(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("LLM_BUILD_ANN_INDEX", "1")
|
||||||
|
cfg = llm_config.load()
|
||||||
|
assert cfg.build_ann_index is True
|
||||||
|
|
||||||
|
def test_build_ann_index_env_override_false(self, monkeypatch):
|
||||||
|
monkeypatch.setenv("LLM_BUILD_ANN_INDEX", "no")
|
||||||
|
cfg = llm_config.load()
|
||||||
|
assert cfg.build_ann_index is False
|
||||||
|
|
||||||
|
|
||||||
class TestPgUrl:
|
class TestPgUrl:
|
||||||
def test_url_includes_password_from_env(self, monkeypatch):
|
def test_url_includes_password_from_env(self, monkeypatch):
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ CFG = LlmConfig(
|
|||||||
embed_model="m",
|
embed_model="m",
|
||||||
instruct_model="g",
|
instruct_model="g",
|
||||||
embed_dim=768,
|
embed_dim=768,
|
||||||
|
build_ann_index=False,
|
||||||
pg_host="x",
|
pg_host="x",
|
||||||
pg_port=5432,
|
pg_port=5432,
|
||||||
pg_db="llm",
|
pg_db="llm",
|
||||||
@@ -20,7 +21,7 @@ CFG = LlmConfig(
|
|||||||
DOC = Doc(key="K1", text="Some body text.", metadata={"docket": "D"})
|
DOC = Doc(key="K1", text="Some body text.", metadata={"docket": "D"})
|
||||||
|
|
||||||
|
|
||||||
def _run(docs, state_rows, force=False):
|
def _run(docs, state_rows, force=False, cfg=CFG):
|
||||||
"""Run index_docs with everything external mocked; return mocks."""
|
"""Run index_docs with everything external mocked; return mocks."""
|
||||||
store = MagicMock()
|
store = MagicMock()
|
||||||
engine = MagicMock()
|
engine = MagicMock()
|
||||||
@@ -30,22 +31,23 @@ def _run(docs, state_rows, force=False):
|
|||||||
patch("llm.index._engine", return_value=engine),
|
patch("llm.index._engine", return_value=engine),
|
||||||
patch("llm.index.vectorstore", return_value=store),
|
patch("llm.index.vectorstore", return_value=store),
|
||||||
patch("llm.index.embed_texts", return_value=[[0.0] * 3]),
|
patch("llm.index.embed_texts", return_value=[[0.0] * 3]),
|
||||||
|
patch("llm.index.ensure_hnsw") as mock_ensure_hnsw,
|
||||||
patch("llm.index.HostPool") as MockPool,
|
patch("llm.index.HostPool") as MockPool,
|
||||||
):
|
):
|
||||||
MockPool.return_value.check.return_value = ["http://h1:11434"]
|
MockPool.return_value.check.return_value = ["http://h1:11434"]
|
||||||
stats = index_docs(
|
stats = index_docs(
|
||||||
docs,
|
docs,
|
||||||
collection="comments",
|
collection="comments",
|
||||||
cfg=CFG,
|
cfg=cfg,
|
||||||
pool=MockPool.return_value,
|
pool=MockPool.return_value,
|
||||||
force=force,
|
force=force,
|
||||||
)
|
)
|
||||||
return stats, store, conn
|
return stats, store, conn, mock_ensure_hnsw
|
||||||
|
|
||||||
|
|
||||||
class TestIndexDocs:
|
class TestIndexDocs:
|
||||||
def test_new_doc_embedded_and_recorded(self):
|
def test_new_doc_embedded_and_recorded(self):
|
||||||
stats, store, conn = _run([DOC], state_rows=[])
|
stats, store, conn, _ensure_hnsw = _run([DOC], state_rows=[])
|
||||||
assert stats == {"indexed": 1, "skipped": 0, "chunks": 1}
|
assert stats == {"indexed": 1, "skipped": 0, "chunks": 1}
|
||||||
store.add_embeddings.assert_called_once()
|
store.add_embeddings.assert_called_once()
|
||||||
kwargs = store.add_embeddings.call_args.kwargs
|
kwargs = store.add_embeddings.call_args.kwargs
|
||||||
@@ -53,12 +55,12 @@ class TestIndexDocs:
|
|||||||
|
|
||||||
def test_unchanged_doc_skipped(self):
|
def test_unchanged_doc_skipped(self):
|
||||||
h = content_hash(DOC.text)
|
h = content_hash(DOC.text)
|
||||||
stats, store, _ = _run([DOC], state_rows=[("K1", h)])
|
stats, store, _, _ensure_hnsw = _run([DOC], state_rows=[("K1", h)])
|
||||||
assert stats["skipped"] == 1
|
assert stats["skipped"] == 1
|
||||||
store.add_embeddings.assert_not_called()
|
store.add_embeddings.assert_not_called()
|
||||||
|
|
||||||
def test_changed_doc_deletes_old_chunks_first(self):
|
def test_changed_doc_deletes_old_chunks_first(self):
|
||||||
stats, store, conn = _run([DOC], state_rows=[("K1", "stalehash")])
|
stats, store, conn, _ensure_hnsw = _run([DOC], state_rows=[("K1", "stalehash")])
|
||||||
assert stats["indexed"] == 1
|
assert stats["indexed"] == 1
|
||||||
deletes = [
|
deletes = [
|
||||||
c
|
c
|
||||||
@@ -77,10 +79,31 @@ class TestIndexDocs:
|
|||||||
|
|
||||||
def test_force_reembeds_unchanged(self):
|
def test_force_reembeds_unchanged(self):
|
||||||
h = content_hash(DOC.text)
|
h = content_hash(DOC.text)
|
||||||
stats, store, _ = _run([DOC], state_rows=[("K1", h)], force=True)
|
stats, store, _, _ensure_hnsw = _run([DOC], state_rows=[("K1", h)], force=True)
|
||||||
assert stats["indexed"] == 1
|
assert stats["indexed"] == 1
|
||||||
|
|
||||||
def test_empty_doc_counts_skipped(self):
|
def test_empty_doc_counts_skipped(self):
|
||||||
empty = Doc(key="K2", text=" ", metadata={})
|
empty = Doc(key="K2", text=" ", metadata={})
|
||||||
stats, store, _ = _run([empty], state_rows=[])
|
stats, store, _, _ensure_hnsw = _run([empty], state_rows=[])
|
||||||
assert stats == {"indexed": 0, "skipped": 1, "chunks": 0}
|
assert stats == {"indexed": 0, "skipped": 1, "chunks": 0}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnnIndexGating:
|
||||||
|
def test_build_ann_index_false_skips_ensure_hnsw(self):
|
||||||
|
_stats, _store, _conn, mock_ensure_hnsw = _run([DOC], state_rows=[], cfg=CFG)
|
||||||
|
mock_ensure_hnsw.assert_not_called()
|
||||||
|
|
||||||
|
def test_build_ann_index_true_calls_ensure_hnsw(self):
|
||||||
|
cfg = LlmConfig(
|
||||||
|
ollama_hosts=("http://h1:11434",),
|
||||||
|
embed_model="m",
|
||||||
|
instruct_model="g",
|
||||||
|
embed_dim=768,
|
||||||
|
build_ann_index=True,
|
||||||
|
pg_host="x",
|
||||||
|
pg_port=5432,
|
||||||
|
pg_db="llm",
|
||||||
|
pg_user="llm",
|
||||||
|
)
|
||||||
|
_stats, _store, _conn, mock_ensure_hnsw = _run([DOC], state_rows=[], cfg=cfg)
|
||||||
|
assert mock_ensure_hnsw.called
|
||||||
|
|||||||
Reference in New Issue
Block a user