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. """ from tht.adapters.factory import build_vector_loader return build_vector_loader(cfg, table) def open_searcher(cfg): """Searcher per la LETTURA (similarity search): via REST se `vector_rest` รจ configurato, altrimenti connessione diretta (dev/test).""" from tht.adapters.factory import build_vector_store from tht.vectorstore.reader import tables_for_kinds store = build_vector_store(cfg) class AdapterSearcher: def search(self, query_vec, top_n=10, kinds=None, metadata_filter=None): return store.search( tables_for_kinds(kinds), query_vec, limit=top_n, kinds=kinds, metadata_filter=metadata_filter, ) return AdapterSearcher() 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)