Files
ThothII/harness/tht/cli/vector_cmd.py
T
marcopan 40b3bac6c6 feat(harness): port vector_cmd (Onda 2) — helper vector condivisi
Esporta make_embedder/open_store/open_searcher/require_vector_cfg usati da
memory/search/evidence cmd (Onda 3). 3 residui nsp nei messaggi fixati (grep-per-file).
2026-06-27 14:08:54 +02:00

147 lines
5.2 KiB
Python

from pathlib import Path
import typer
from tht.cli._guards import has_vector_write_rest, require_server_profile, require_vector_write_allowed
from tht.cli.config_cmd import CONFIG_OPT
from tht.cli.schema_cmd import _load_config_or_exit, annotations_path, physical_path
vector_app = typer.Typer(help="Indice semantico pgvector (derivato, rigenerabile)")
def make_embedder(embeddings_cfg):
"""Factory del client embeddings (monkeypatchabile nei test)."""
from tht.vectorstore.embeddings import OllamaEmbeddings
return OllamaEmbeddings(embeddings_cfg)
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 missing:
typer.secho(
f"ERRORE: sezioni mancanti nel workspace yaml: {', '.join(missing)}.",
fg=typer.colors.RED, err=True,
)
raise typer.Exit(code=1)
def require_direct_vector_cfg(cfg):
missing = [k for k in ("vector_db", "embeddings") if getattr(cfg, k) is None]
if missing:
typer.secho(
f"ERRORE: sezioni mancanti nel workspace yaml: {', '.join(missing)}.",
fg=typer.colors.RED, err=True,
)
raise typer.Exit(code=1)
def open_store(cfg, table: str):
"""Writer table-scoped per il LOADING.
Sul server preferisce la connessione diretta. In profilo workstation usa `vector_write_rest`
se configurato, con upsert remoto non distruttivo.
"""
if has_vector_write_rest(cfg) and (cfg.profile == "workstation" or cfg.vector_db is None):
from tht.vectorstore.rest_client import VectorRestClient
from tht.vectorstore.rest_writer import RestVectorWriter
return RestVectorWriter(VectorRestClient(cfg.vector_write_rest), table=table)
from tht.db.connection import make_engine
from tht.vectorstore.store import VectorStore
engine = make_engine(cfg.vector_db)
return VectorStore(
engine, schema=cfg.vector_db.db_schema, table=table, dim=cfg.embeddings.dim
)
def open_searcher(cfg):
"""Searcher per la LETTURA (similarity search): via REST se `vector_rest` è configurato,
altrimenti connessione diretta (dev/test)."""
if cfg.vector_rest is not None:
from tht.vectorstore.reader import RestSearcher
from tht.vectorstore.rest_client import VectorRestClient
return RestSearcher(VectorRestClient(cfg.vector_rest))
from tht.db.connection import make_engine
from tht.vectorstore.reader import DirectSearcher
return DirectSearcher(
make_engine(cfg.vector_db), schema=cfg.vector_db.db_schema, dim=cfg.embeddings.dim
)
def _print_stats(stats) -> None:
typer.secho(
f"OK: {stats.added} nuovi, {stats.updated} aggiornati, "
f"{stats.deleted} rimossi, {stats.unchanged} invariati",
fg=typer.colors.GREEN,
)
@vector_app.command("init")
def init_cmd(
config: Path = CONFIG_OPT,
skip_ollama_check: bool = typer.Option(
False, "--skip-ollama-check", help="Non verificare la raggiungibilita' di Ollama."
),
) -> None:
"""Crea schema e tabella pgvector (idempotente) e verifica le connessioni."""
from sqlalchemy.exc import OperationalError
from tht.vectorstore.embeddings import EmbeddingsError
from tht.vectorstore.reader import ALL_TABLES
cfg = _load_config_or_exit(config)
require_server_profile(cfg, "vector init")
require_direct_vector_cfg(cfg)
try:
for table in ALL_TABLES:
open_store(cfg, table).init_schema()
except OperationalError as e:
typer.secho(f"ERRORE connessione pgvector: {e.orig}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
if not skip_ollama_check:
try:
make_embedder(cfg.embeddings).embed_query("ping")
except EmbeddingsError as e:
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(code=1)
typer.secho(
f"OK: schema {cfg.vector_db.db_schema} pronto (tabelle: {', '.join(ALL_TABLES)}) su "
f"{cfg.vector_db.host}:{cfg.vector_db.port}", fg=typer.colors.GREEN,
)
@vector_app.command("index-schema")
def index_schema_cmd(config: Path = CONFIG_OPT) -> None:
"""Embedda e sincronizza i record schema (tabelle e colonne) da mschema."""
from tht.mschema.models import Annotations, PhysicalSchema
from tht.vectorstore.records import schema_records
cfg = _load_config_or_exit(config)
require_vector_write_allowed(cfg, "vector index-schema")
require_vector_cfg(cfg)
phys_file = physical_path(cfg)
if not phys_file.exists():
typer.secho(
f"ERRORE: {phys_file} non trovato. Esegui prima `tht schema introspect`.",
fg=typer.colors.RED, err=True,
)
raise typer.Exit(code=1)
physical = PhysicalSchema.from_yaml(phys_file)
annotations = Annotations.from_yaml(annotations_path(cfg))
records = schema_records(physical, annotations)
store = open_store(cfg, "schema_records")
stats = store.sync(
records, make_embedder(cfg.embeddings), kinds={"schema_table", "schema_column"}
)
_print_stats(stats)