fix: support qdrant-only vector maintenance
This commit is contained in:
@@ -3,8 +3,10 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
from psycopg2 import Error as PsycopgError
|
||||
from psycopg2 import sql
|
||||
from sqlalchemy import Engine
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from tht.config import DatabaseConfig
|
||||
from tht.db.connection import make_engine
|
||||
@@ -19,7 +21,6 @@ from tht.ports.vector import (
|
||||
)
|
||||
from tht.vectorstore.store import VectorHit, hit_from_metadata
|
||||
|
||||
|
||||
COLLECTION_KINDS = {
|
||||
"schema_records": {"schema_table", "schema_column"},
|
||||
"evidence": {"evidence"},
|
||||
@@ -243,7 +244,7 @@ class PgVectorStore:
|
||||
return True, None, dimensions
|
||||
finally:
|
||||
raw.close()
|
||||
except Exception as exc:
|
||||
except (AttributeError, TypeError, ValueError, PsycopgError, SQLAlchemyError) as exc:
|
||||
return False, f"vector database probe failed: {type(exc).__name__}", set()
|
||||
|
||||
def health(self) -> VectorHealth:
|
||||
@@ -471,6 +472,30 @@ class PgVectorStore:
|
||||
if raw is not None:
|
||||
raw.close()
|
||||
|
||||
def delete_kinds(self, collection: str, kinds: list[str]) -> int:
|
||||
_collection(self._schema, collection)
|
||||
_validate_collection_kinds(collection, kinds)
|
||||
raw = None
|
||||
try:
|
||||
raw = self._require_writer().raw_connection()
|
||||
with raw.cursor() as cursor:
|
||||
cursor.execute(
|
||||
sql.SQL("DELETE FROM {} WHERE kind = ANY(%s)").format(
|
||||
_collection(self._schema, collection)
|
||||
),
|
||||
(kinds,),
|
||||
)
|
||||
count = cursor.rowcount
|
||||
raw.commit()
|
||||
return count
|
||||
except Exception as exc:
|
||||
if raw is not None:
|
||||
raw.rollback()
|
||||
raise VectorWriteUnavailable("Vector kind cleanup unavailable") from exc
|
||||
finally:
|
||||
if raw is not None:
|
||||
raw.close()
|
||||
|
||||
def list_evidence_generations(self, collection: str, workspace_id: str) -> list[str]:
|
||||
if collection != "evidence":
|
||||
raise VectorStoreError("Only exact Evidence generations may be listed")
|
||||
|
||||
@@ -212,6 +212,21 @@ class QdrantVectorStore:
|
||||
)
|
||||
return len(records)
|
||||
|
||||
def delete_kinds(self, collection: str, kinds: list[str]) -> int:
|
||||
_collection("vectors", collection)
|
||||
_validate_collection_kinds(collection, kinds)
|
||||
must = [
|
||||
*self._workspace_filter(),
|
||||
{"key": "record_kind", "match": {"any": sorted(kinds)}},
|
||||
]
|
||||
before = len(self._scroll(must))
|
||||
self._call(
|
||||
"POST",
|
||||
f"/collections/{self._collection}/points/delete?wait=true",
|
||||
{"filter": {"must": must}},
|
||||
)
|
||||
return before
|
||||
|
||||
def delete_generation(self, collection: str, generation: str, workspace_id: str) -> int:
|
||||
if collection != "evidence" or _GENERATION.fullmatch(generation) is None:
|
||||
raise VectorStoreError("Only exact Evidence generations may be deleted")
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
import re
|
||||
|
||||
from tht.adapters.vector.pgvector import (
|
||||
_collection,
|
||||
_validate_collection_kinds,
|
||||
_validate_known_kinds,
|
||||
)
|
||||
from tht.ports.vector import (
|
||||
VectorCapabilities,
|
||||
VectorHealth,
|
||||
@@ -14,11 +19,6 @@ from tht.ports.vector import (
|
||||
)
|
||||
from tht.vectorstore.rest_client import VectorRestClient, VectorRestError
|
||||
from tht.vectorstore.store import hit_from_metadata
|
||||
from tht.adapters.vector.pgvector import (
|
||||
_collection,
|
||||
_validate_collection_kinds,
|
||||
_validate_known_kinds,
|
||||
)
|
||||
|
||||
|
||||
def _merge(hits: list[VectorHit], limit: int) -> list[VectorHit]:
|
||||
@@ -85,7 +85,7 @@ class ThothHttpVectorStore:
|
||||
return None, None, []
|
||||
try:
|
||||
return True, None, client.list_tables()
|
||||
except Exception as exc:
|
||||
except (RuntimeError, VectorRestError) as exc:
|
||||
return False, str(exc), []
|
||||
|
||||
def search(
|
||||
@@ -155,6 +155,14 @@ class ThothHttpVectorStore:
|
||||
except VectorRestError as exc:
|
||||
raise VectorStoreError(str(exc)) from exc
|
||||
|
||||
def delete_kinds(self, collection: str, kinds: list[str]) -> int:
|
||||
_collection("vectors", collection)
|
||||
_validate_collection_kinds(collection, kinds)
|
||||
try:
|
||||
return self._require_writer().delete_kinds(collection, kinds)
|
||||
except VectorRestError as exc:
|
||||
raise VectorStoreError(str(exc)) from exc
|
||||
|
||||
def delete_generation(self, collection: str, generation: str, workspace_id: str) -> int:
|
||||
if collection != "evidence" or re.fullmatch(r"gen:[0-9a-f]{32}", generation) is None:
|
||||
raise VectorStoreError("Only exact Evidence generations may be deleted")
|
||||
|
||||
@@ -43,6 +43,12 @@ def _resync_memory(cfg):
|
||||
)
|
||||
|
||||
|
||||
def clear_memory_index(cfg):
|
||||
from tht.adapters.factory import build_vector_store
|
||||
|
||||
return build_vector_store(cfg, require_write=True).delete_kinds("memory", ["memory"])
|
||||
|
||||
|
||||
@memory_app.command("promote")
|
||||
def promote_cmd(
|
||||
session: str = typer.Option(..., "--session"),
|
||||
@@ -192,7 +198,6 @@ def clear_cmd(
|
||||
config: Path = CONFIG_OPT,
|
||||
) -> None:
|
||||
"""Cancella TUTTA la review memory: registro canonico + indice pgvector (kind=memory)."""
|
||||
from tht.cli.vector_cmd import make_embedder, open_store, require_direct_vector_cfg
|
||||
from tht.memory import load_registry
|
||||
|
||||
cfg = _load_config_or_exit(config)
|
||||
@@ -209,10 +214,7 @@ def clear_cmd(
|
||||
typer.secho("Annullato.", fg=typer.colors.YELLOW)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
# Indice pgvector: rimuove i record kind=memory (sync con insieme vuoto).
|
||||
require_direct_vector_cfg(cfg)
|
||||
store = open_store(cfg, "memory")
|
||||
store.sync([], make_embedder(cfg.embeddings), kinds={"memory"})
|
||||
clear_memory_index(cfg)
|
||||
|
||||
# Registro canonico.
|
||||
registry.unlink()
|
||||
|
||||
@@ -26,8 +26,8 @@ def require_vector_cfg(cfg):
|
||||
missing = []
|
||||
if cfg.embeddings is None:
|
||||
missing.append("embeddings")
|
||||
if cfg.vector_db is None and not has_vector_write_rest(cfg):
|
||||
missing.append("vector_db o vector_write_rest")
|
||||
if cfg.vectors is None and cfg.vector_db is None and not has_vector_write_rest(cfg):
|
||||
missing.append("vectors o vector_db o vector_write_rest")
|
||||
if missing:
|
||||
typer.secho(
|
||||
f"ERRORE: sezioni mancanti nel workspace yaml: {', '.join(missing)}.",
|
||||
|
||||
@@ -80,6 +80,8 @@ class VectorStore(Protocol):
|
||||
|
||||
def upsert(self, collection: str, records: list[VectorWriteRecord]) -> int: ...
|
||||
|
||||
def delete_kinds(self, collection: str, kinds: list[str]) -> int: ...
|
||||
|
||||
def delete_generation(self, collection: str, generation: str, workspace_id: str) -> int: ...
|
||||
|
||||
def list_evidence_generations(self, collection: str, workspace_id: str) -> list[str]: ...
|
||||
|
||||
@@ -5,9 +5,10 @@ Endpoint dedicato (es. https://host/vector/v1/), distinto dal DWH. La lettura us
|
||||
Errori in italiano e azionabili, stile `rest/client.py`.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import re
|
||||
|
||||
import requests
|
||||
|
||||
from tht.config import RestConfig
|
||||
|
||||
|
||||
@@ -46,7 +47,7 @@ class VectorRestClient:
|
||||
try:
|
||||
body = resp.json()
|
||||
detail = body.get("message") or body.get("details") or resp.text
|
||||
except Exception:
|
||||
except ValueError:
|
||||
detail = resp.text
|
||||
return f"Vector REST rpc {fn} → HTTP {resp.status_code}: {detail}"
|
||||
|
||||
@@ -149,6 +150,22 @@ class VectorRestClient:
|
||||
return int(payload.get("deleted", 0))
|
||||
return 0
|
||||
|
||||
def delete_kinds(self, table_name: str, kinds: list[str]) -> int:
|
||||
try:
|
||||
payload = self._call(
|
||||
"delete_vector_kinds",
|
||||
{"table_name": table_name, "kinds": kinds},
|
||||
)
|
||||
except VectorRestError as error:
|
||||
if "HTTP 404" in str(error):
|
||||
raise VectorRestError(
|
||||
"delete_vector_kinds RPC is unavailable; deploy the cleanup migration"
|
||||
) from None
|
||||
raise
|
||||
if isinstance(payload, dict):
|
||||
return int(payload.get("deleted", 0))
|
||||
return 0
|
||||
|
||||
def list_evidence_generations(self, table_name: str, workspace_id: str) -> list[str]:
|
||||
if re.fullmatch(r"[a-z][a-z0-9_-]{0,63}", workspace_id) is None:
|
||||
raise ValueError("workspace namespace must be canonical")
|
||||
|
||||
Reference in New Issue
Block a user