From 40b3bac6c6bcc359ccce1ff720e271ef9ccbd96d Mon Sep 17 00:00:00 2001 From: mptyl Date: Sat, 27 Jun 2026 14:08:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(harness):=20port=20vector=5Fcmd=20(Onda=20?= =?UTF-8?q?2)=20=E2=80=94=20helper=20vector=20condivisi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- harness/tht/cli/__init__.py | 2 + harness/tht/cli/vector_cmd.py | 146 ++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 harness/tht/cli/vector_cmd.py diff --git a/harness/tht/cli/__init__.py b/harness/tht/cli/__init__.py index c706643a..913bf9f2 100644 --- a/harness/tht/cli/__init__.py +++ b/harness/tht/cli/__init__.py @@ -40,8 +40,10 @@ from tht.cli.config_cmd import config_app # noqa: E402 from tht.cli.phase_cmd import phase_app # noqa: E402 from tht.cli.schema_cmd import schema_app # noqa: E402 from tht.cli.session_cmd import session_app # noqa: E402 +from tht.cli.vector_cmd import vector_app # noqa: E402 app.add_typer(phase_app, name="phase") app.add_typer(config_app, name="config") app.add_typer(schema_app, name="schema") app.add_typer(session_app, name="session") +app.add_typer(vector_app, name="vector") diff --git a/harness/tht/cli/vector_cmd.py b/harness/tht/cli/vector_cmd.py new file mode 100644 index 00000000..102cf0f9 --- /dev/null +++ b/harness/tht/cli/vector_cmd.py @@ -0,0 +1,146 @@ +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)