feat(evidence): contribute to semantic stages (#42)

This commit is contained in:
2026-08-24 22:05:30 +02:00
parent 3420c57c8b
commit f1a9b567ba
17 changed files with 619 additions and 23 deletions
+105 -6
View File
@@ -14,6 +14,14 @@ KIND_MAP = {
"formula": [], # solo formula store (D14b), niente LSH/vector
}
_STAGE_PURPOSES = {
"clarification": "disambiguation",
"rewriting": "rewriting",
"schema_linking": "schema_linking",
"cte": "sql_generation",
"final_sql": "sql_generation",
}
# Default di `--top` per le famiglie diverse da `schema` (numero di risultati). Per `schema`
# `--top` indica il numero di TABELLE candidate ed e' configurabile via `search.top_schema_tables`
# (recupero ancorato alle tabelle: di ognuna si rendono tutte le colonne + FK).
@@ -31,6 +39,79 @@ def _leased_dwh_snapshot(cfg, context: typer.Context):
return snapshot
@search_app.command("evidence")
def evidence_search_cmd(
ctx: typer.Context,
query: str = typer.Argument(..., help="Domanda o contesto dello stage."),
stage: str = typer.Option(..., "--stage", help="Stage semantico chiamante."),
config: Path = CONFIG_OPT,
session: str | None = typer.Option(None, "--session", help="Sessione per la ricevuta minima."),
concept: list[str] = typer.Option([], "--concept"),
table: list[str] = typer.Option([], "--table"),
column: list[str] = typer.Option([], "--column"),
require_kind: list[str] = typer.Option([], "--require-kind"),
require_concept: list[str] = typer.Option([], "--require-concept"),
require_table: list[str] = typer.Option([], "--require-table"),
require_column: list[str] = typer.Option([], "--require-column"),
top: int = typer.Option(10, "--top"),
json_out: bool = typer.Option(False, "--json"),
) -> None:
"""Run one typed, purpose-bound Evidence search for a semantic workflow stage."""
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
from tht.evidence import (
EvidenceReceipt,
EvidenceSearchContext,
active_searcher,
replace_evidence_receipt,
search_evidence,
validate_corpus_workspace,
)
purpose = _STAGE_PURPOSES.get(stage)
if purpose is None:
raise typer.BadParameter("stage must be clarification, rewriting, schema_linking, cte, or final_sql")
cfg = _load_config_or_exit(config)
workspace_id = workspace_id_for_config(cfg, config)
validate_corpus_workspace(cfg, workspace_id)
require_vector_cfg(cfg)
outcome = search_evidence(
query, purpose,
EvidenceSearchContext(
concepts=tuple(concept), tables=tuple(table), columns=tuple(column),
required_kinds=tuple(require_kind), required_concepts=tuple(require_concept),
required_tables=tuple(require_table), required_columns=tuple(require_column),
),
searcher=active_searcher(cfg, open_searcher(cfg), workspace_id=workspace_id),
embedder=make_embedder(cfg.embeddings), top_n=top,
)
if outcome.status == "unavailable":
payload = {"status": outcome.status, "code": outcome.code, "message": outcome.message}
if json_out:
typer.echo(json.dumps(payload, ensure_ascii=False))
else:
typer.secho(f"ERRORE: {outcome.message}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
if session:
from tht.cli.session_cmd import load_session_or_exit, session_repository
load_session_or_exit(cfg, session)
replace_evidence_receipt(session_repository(cfg), session, EvidenceReceipt(
stage=stage, purpose=purpose, vector_generation=outcome.vector_generation or "",
evidence_ids=tuple(result.evidence_id for result in outcome.results),
))
payload = {
"status": "available", "vector_generation": outcome.vector_generation,
"results": [
{"evidence_id": result.evidence_id, "title": result.title, "kind": result.kind,
"excerpts": list(result.excerpts), "provenance": result.provenance,
"citation": result.citation, "document_id": result.document_id}
for result in outcome.results
],
}
if json_out:
typer.echo(json.dumps(payload, ensure_ascii=False, indent=2))
@search_app.command("find")
def search_cmd(
ctx: typer.Context,
@@ -254,8 +335,10 @@ def pack_cmd(
from tht.cli.vector_cmd import make_embedder, open_searcher, require_vector_cfg
from tht.evidence import (
EvidenceSearchContext,
active_searcher,
build_retrieval_entries,
search_evidence,
validate_corpus_workspace,
)
from tht.memory import SOLVED_KIND
@@ -274,6 +357,7 @@ def pack_cmd(
evidence: list[dict] = []
solved: list[dict] = []
warnings: list[str] = []
evidence_outcome = None
degrade = (VectorStoreError, VectorReadUnavailable, EmbeddingsError, OperationalError)
vec = None
@@ -309,12 +393,19 @@ def pack_cmd(
except degrade as e:
warnings.append(f"ricerca schema fallita ({e})")
try:
ev = combined_search(
keyword=question, lsh_hits=None, store=searcher, embedder=embedder,
top=PACK_EVIDENCE_TOP, rrf_k=cfg.search.rrf_k,
kinds=KIND_MAP["evidence"], query_vec=vec,
evidence_outcome = search_evidence(
question, "disambiguation", EvidenceSearchContext(), searcher=searcher,
embedder=embedder, top_n=PACK_EVIDENCE_TOP,
)
evidence = build_retrieval_entries(ev, excerpt_chars=PACK_EXCERPT_CHARS)
if evidence_outcome.status == "available":
evidence = build_retrieval_entries(evidence_outcome.results, excerpt_chars=PACK_EXCERPT_CHARS)
else:
typer.secho(
f"ERRORE: Evidence non disponibile ({evidence_outcome.code})",
fg=typer.colors.RED,
err=True,
)
raise typer.Exit(code=1)
except degrade as e:
warnings.append(f"ricerca evidence fallita ({e})")
try:
@@ -367,9 +458,17 @@ def pack_cmd(
if session:
from tht.cli.session_cmd import load_session_or_exit, session_repository
from tht.evidence import EvidenceReceipt, replace_evidence_receipt
load_session_or_exit(cfg, session)
session_repository(cfg).write_artifact(session, "retrieval_pack", md)
repository = session_repository(cfg)
repository.write_artifact(session, "retrieval_pack", md)
if evidence_outcome is not None and evidence_outcome.status == "available":
replace_evidence_receipt(repository, session, EvidenceReceipt(
stage="clarification", purpose="disambiguation",
vector_generation=evidence_outcome.vector_generation or "",
evidence_ids=tuple(result.evidence_id for result in evidence_outcome.results),
))
if not json_out:
typer.secho(
"OK: retrieval pack scritto "