fix: support qdrant-only vector maintenance

This commit is contained in:
2026-08-08 18:14:03 +02:00
parent 5e39cfa347
commit 32cd2165eb
9 changed files with 261 additions and 28 deletions
+164
View File
@@ -0,0 +1,164 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from pathlib import Path
from types import SimpleNamespace
from typer.testing import CliRunner
from tht.cli import app
from tht.memory import MemoryRecord, save_registry
class _FakeEmbedder:
def embed_documents(self, documents):
return [[0.1] * 4 for _ in documents]
class _FakeVectorStore:
def __init__(self):
self.upserts = []
self.deleted = []
def existing_hashes(self, collection, kinds):
return {}
def upsert(self, collection, records):
self.upserts.append((collection, records))
return len(records)
def delete_kinds(self, collection, kinds):
self.deleted.append((collection, list(kinds)))
return 3
def _qdrant_runtime_config(tmp_path: Path) -> Path:
cfg = tmp_path / "workspace.yaml"
cfg.write_text(
f"""
runtime_identity:
workspace_id: psd-clinical
workspace_revision: {'a' * 40}
dwh:
type: postgres_direct
connection: {{database: analytics, schema: mart, user: reader, password: secret}}
vectors:
type: qdrant
base_url: http://qdrant:6333
collection: psd-clinical
roots:
sessions: {tmp_path / 'sessions'}
artifacts: {tmp_path / 'artifacts'}
indexes: {tmp_path / 'indexes'}
embeddings:
provider: ollama_internal
base_url: http://embedding:11434
model: qwen3-embedding:0.6b
dim: 1024
"""
)
return cfg
def _write_schema_artifacts(tmp_path: Path) -> None:
(tmp_path / "artifacts" / "mschema").mkdir(parents=True, exist_ok=True)
(tmp_path / "artifacts" / "mschema" / "physical.yaml").write_text(
"""
database: analytics
schema: mart
introspected_at: 2026-01-01T00:00:00+00:00
tables:
fact_patient:
comment: Patients
columns:
id:
type: bigint
"""
)
(tmp_path / "artifacts" / "mschema" / "annotations.yaml").write_text(
"tables: {}\n"
)
def _memory_record() -> MemoryRecord:
return MemoryRecord(
id="mem-0001",
ts=datetime(2026, 1, 1, tzinfo=UTC),
session_id="s1",
decision_seq=7,
type="concept_clarified",
subject="paziente attivo",
detail="flag_attivo = TRUE",
rationale="r",
question_context="dammi i pazienti attivi",
tables=[],
concepts=["paziente attivo"],
)
def test_vector_index_schema_accepts_qdrant_only_runtime_config(tmp_path, monkeypatch):
cfg = _qdrant_runtime_config(tmp_path)
_write_schema_artifacts(tmp_path)
store = _FakeVectorStore()
monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: _FakeEmbedder())
res = CliRunner().invoke(app, ["vector", "index-schema", "-c", str(cfg)])
assert res.exit_code == 0, res.output
assert store.upserts
def test_memory_promote_accepts_qdrant_only_runtime_config(tmp_path, monkeypatch):
cfg = _qdrant_runtime_config(tmp_path)
store = _FakeVectorStore()
promoted = [_memory_record()]
snapshot = SimpleNamespace(manifest=SimpleNamespace(id="s1"))
monkeypatch.setattr("tht.cli.memory_cmd.load_snapshot_or_exit", lambda cfg, session: snapshot)
monkeypatch.setattr("tht.memory.promote_snapshot", lambda *args, **kwargs: promoted)
monkeypatch.setattr("tht.memory.load_registry", lambda path: promoted)
monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: _FakeEmbedder())
res = CliRunner().invoke(
app,
["memory", "promote", "--session", "s1", "--decision", "7", "--json", "-c", str(cfg)],
)
assert res.exit_code == 0, res.output
assert json.loads(res.stdout)["indexed"] is True
assert store.upserts
def test_memory_index_accepts_qdrant_only_runtime_config(tmp_path, monkeypatch):
cfg = _qdrant_runtime_config(tmp_path)
store = _FakeVectorStore()
records = [_memory_record()]
save_registry(records, tmp_path / "artifacts" / "memory" / "registry.jsonl")
monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: _FakeEmbedder())
res = CliRunner().invoke(app, ["memory", "index", "-c", str(cfg)])
assert res.exit_code == 0, res.output
assert "OK:" in res.output
assert store.upserts
def test_memory_clear_accepts_qdrant_only_runtime_config(tmp_path, monkeypatch):
cfg = _qdrant_runtime_config(tmp_path)
store = _FakeVectorStore()
records = [_memory_record()]
registry = tmp_path / "artifacts" / "memory" / "registry.jsonl"
save_registry(records, registry)
monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store)
res = CliRunner().invoke(app, ["memory", "clear", "--yes", "-c", str(cfg)])
assert res.exit_code == 0, res.output
assert not registry.exists()
+11 -11
View File
@@ -7,7 +7,7 @@ puro (`[]` in modalita' --json) ed exit 0, cosi' il modello prosegue senza
exemplar. Il finalize-hook gestisce gia' lo stesso scenario in modo analogo.
"""
import json
from datetime import datetime
from datetime import UTC, datetime
from typer.testing import CliRunner
@@ -24,7 +24,7 @@ def _cfg(tmp_path):
"database: {database: d, schema: s, user: u, password: p, transport: direct}\n"
f"paths: {{artifacts: {tmp_path/'a'}, indexes: {tmp_path/'i'}, sessions: {tmp_path/'se'}}}\n"
"vector_db: {database: v, schema: vectors, user: u, password: p, transport: direct}\n"
"embeddings: {base_url: 'http://localhost:11434', model: nomic-embed-text, dim: 8}\n"
"embeddings: {base_url: 'http://localhost:11434', model: qwen3-embedding:0.6b, dim: 1024}\n"
)
return cfg
@@ -103,15 +103,15 @@ def test_solved_search_json_maps_hit_metadata(tmp_path, monkeypatch):
def test_memory_search_excludes_legacy_table_records(tmp_path, monkeypatch):
records = [
MemoryRecord(
id="mem-0001", ts=datetime(2026, 1, 1), session_id="s1",
decision_seq=1, type="table_promoted", subject="fact_pazienti",
),
MemoryRecord(
id="mem-0002", ts=datetime(2026, 1, 1), session_id="s1",
decision_seq=2, type="concept_clarified", subject="paziente attivo",
detail="flag_attivo = TRUE",
),
MemoryRecord(
id="mem-0001", ts=datetime(2026, 1, 1, tzinfo=UTC), session_id="s1",
decision_seq=1, type="table_promoted", subject="fact_pazienti",
),
MemoryRecord(
id="mem-0002", ts=datetime(2026, 1, 1, tzinfo=UTC), session_id="s1",
decision_seq=2, type="concept_clarified", subject="paziente attivo",
detail="flag_attivo = TRUE",
),
]
cfg = _cfg(tmp_path)
save_registry(records, tmp_path / "a" / "memory" / "registry.jsonl")