250 lines
8.4 KiB
Python
250 lines
8.4 KiB
Python
# ruff: noqa: BLE001
|
|
from pathlib import Path
|
|
from typing import TypedDict
|
|
|
|
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
|
|
from tht.ports.vector import VectorWriteRecord
|
|
from tht.vectorstore.store import SyncStats, content_hash
|
|
|
|
vector_app = typer.Typer(help="Indice semantico Qdrant (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.vectors is None:
|
|
missing.append("vectors")
|
|
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_searcher(cfg):
|
|
"""Searcher per la lettura semantic search sul runtime vettoriale attivo."""
|
|
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 sync_canonical_records(collection, records, *, store, embedder):
|
|
kinds = sorted({record.kind for record in records})
|
|
existing = store.existing_hashes(collection, kinds)
|
|
pending = []
|
|
stats = SyncStats()
|
|
changed = []
|
|
for record in records:
|
|
hashed = content_hash(record.content)
|
|
current = existing.get(record.id)
|
|
if current == hashed:
|
|
stats.unchanged += 1
|
|
continue
|
|
changed.append((record, hashed, current is None))
|
|
if changed:
|
|
embeddings = embedder.embed_documents([record.content for record, *_ in changed])
|
|
for (record, hashed, is_added), embedding in zip(changed, embeddings, strict=True):
|
|
pending.append(VectorWriteRecord(record=record, embedding=embedding, content_hash=hashed))
|
|
if is_added:
|
|
stats.added += 1
|
|
else:
|
|
stats.updated += 1
|
|
store.upsert(collection, pending)
|
|
return stats
|
|
|
|
|
|
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:
|
|
"""Verifica il runtime Qdrant e la raggiungibilita' dell'embedder configurato."""
|
|
from tht.adapters.factory import build_vector_store
|
|
from tht.vectorstore.embeddings import EmbeddingsError
|
|
|
|
cfg = _load_config_or_exit(config)
|
|
require_server_profile(cfg, "vector init")
|
|
require_vector_cfg(cfg)
|
|
health = build_vector_store(cfg, require_write=True).health()
|
|
if not health.ok:
|
|
typer.secho(
|
|
f"ERRORE runtime vettoriale: {health.detail or 'Qdrant non raggiungibile o incompatibile'}",
|
|
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: runtime Qdrant pronto per la collezione {cfg.vectors.collection}",
|
|
fg=typer.colors.GREEN,
|
|
)
|
|
|
|
|
|
class _MachineVectorError(Exception):
|
|
"""Expected vector CLI failure with a stable machine-readable code."""
|
|
|
|
def __init__(self, code: str):
|
|
self.code = code
|
|
super().__init__(code)
|
|
|
|
|
|
class IndexCounts(TypedDict):
|
|
added: int
|
|
updated: int
|
|
deleted: int
|
|
unchanged: int
|
|
|
|
|
|
class IndexSchemaResult(TypedDict):
|
|
status: str
|
|
code: str
|
|
counts: IndexCounts
|
|
|
|
|
|
def _vector_cfg_or_error(cfg) -> None:
|
|
missing = []
|
|
if cfg.embeddings is None:
|
|
missing.append("embeddings")
|
|
if cfg.vectors is None:
|
|
missing.append("vectors")
|
|
if missing:
|
|
raise _MachineVectorError("vector_configuration_missing")
|
|
|
|
|
|
def _vector_write_or_error(cfg) -> None:
|
|
if cfg.profile == "workstation" and not has_vector_write_rest(cfg):
|
|
raise _MachineVectorError("vector_write_not_allowed")
|
|
|
|
|
|
def index_schema_data(config: Path) -> IndexSchemaResult:
|
|
"""Synchronize schema records and return a bounded machine result."""
|
|
from tht.cli.schema_cmd import _machine_config
|
|
from tht.mschema.models import Annotations, PhysicalSchema
|
|
from tht.vectorstore.records import schema_records
|
|
|
|
try:
|
|
cfg = _machine_config(config)
|
|
except Exception:
|
|
# Keep this helper free of Typer rendering while preserving a stable code.
|
|
raise _MachineVectorError("invalid_configuration") from None
|
|
_vector_write_or_error(cfg)
|
|
_vector_cfg_or_error(cfg)
|
|
phys_file = physical_path(cfg)
|
|
if not phys_file.exists():
|
|
raise _MachineVectorError("physical_schema_missing")
|
|
try:
|
|
physical = PhysicalSchema.from_yaml(phys_file)
|
|
annotations = Annotations.from_yaml(annotations_path(cfg))
|
|
except Exception:
|
|
raise _MachineVectorError("schema_artifacts_invalid") from None
|
|
records = schema_records(physical, annotations)
|
|
from tht.adapters.factory import build_vector_store
|
|
|
|
stats = sync_canonical_records(
|
|
"schema_records",
|
|
records,
|
|
store=build_vector_store(cfg, require_write=True),
|
|
embedder=make_embedder(cfg.embeddings),
|
|
)
|
|
return {
|
|
"status": "succeeded",
|
|
"code": "ok",
|
|
"counts": {
|
|
"added": stats.added,
|
|
"updated": stats.updated,
|
|
"deleted": stats.deleted,
|
|
"unchanged": stats.unchanged,
|
|
},
|
|
}
|
|
|
|
|
|
@vector_app.command("index-schema")
|
|
def index_schema_cmd(
|
|
config: Path = CONFIG_OPT,
|
|
json_output: bool = typer.Option(False, "--json", help="Emetti JSON puro su stdout."),
|
|
) -> None:
|
|
"""Embedda e sincronizza i record schema (tabelle e colonne) nel semantic store."""
|
|
import json
|
|
|
|
if json_output:
|
|
try:
|
|
payload = index_schema_data(config)
|
|
except _MachineVectorError as error:
|
|
typer.echo(json.dumps({"status": "failed", "code": error.code}, sort_keys=True, separators=(",", ":")))
|
|
raise typer.Exit(code=1) from None
|
|
except Exception:
|
|
typer.echo(json.dumps({"status": "failed", "code": "schema_index_failed"}, sort_keys=True, separators=(",", ":")))
|
|
raise typer.Exit(code=1) from None
|
|
typer.echo(json.dumps(payload, sort_keys=True, separators=(",", ":")))
|
|
return
|
|
try:
|
|
payload = index_schema_data(config)
|
|
except _MachineVectorError as error:
|
|
_render_index_schema_error(error, config)
|
|
except Exception:
|
|
typer.secho("ERRORE: impossibile indicizzare lo schema.", fg=typer.colors.RED, err=True)
|
|
raise typer.Exit(code=1) from None
|
|
_print_stats(type("Stats", (), payload["counts"])())
|
|
|
|
|
|
def _render_index_schema_error(error: _MachineVectorError, config: Path) -> None:
|
|
"""Render expected failures without changing the old human CLI messages."""
|
|
cfg = _load_config_or_exit(config)
|
|
if error.code == "physical_schema_missing":
|
|
typer.secho(
|
|
f"ERRORE: {physical_path(cfg)} non trovato. Esegui prima `tht schema introspect`.",
|
|
fg=typer.colors.RED,
|
|
err=True,
|
|
)
|
|
elif error.code == "vector_configuration_missing":
|
|
require_vector_cfg(cfg)
|
|
elif error.code == "vector_write_not_allowed":
|
|
require_vector_write_allowed(cfg, "vector index-schema")
|
|
elif error.code == "schema_artifacts_invalid":
|
|
typer.secho("ERRORE: schema artifacts non validi.", fg=typer.colors.RED, err=True)
|
|
else:
|
|
typer.secho("ERRORE: impossibile indicizzare lo schema.", fg=typer.colors.RED, err=True)
|
|
raise typer.Exit(code=1) from None
|