feat(evidence): contribute to semantic stages (#42)
This commit is contained in:
@@ -143,7 +143,13 @@ class QdrantVectorStore:
|
||||
filter_must.append(self._semantic_kind_filter(allowed_record_kinds))
|
||||
filter_must.append({"key": "record_kind", "match": {"any": allowed_record_kinds}})
|
||||
if metadata_filter is not None:
|
||||
if set(metadata_filter) != {"vector_generation", "document_ids", "workspace_id"}:
|
||||
allowed_filters = {
|
||||
"vector_generation", "document_ids", "workspace_id", "purpose",
|
||||
"required_kinds", "required_concepts", "required_tables", "required_columns",
|
||||
}
|
||||
if not {"vector_generation", "document_ids", "workspace_id"} <= set(metadata_filter) or (
|
||||
set(metadata_filter) - allowed_filters
|
||||
):
|
||||
raise VectorStoreError("Unsupported vector metadata filter")
|
||||
generation = metadata_filter["vector_generation"]
|
||||
document_ids = metadata_filter["document_ids"]
|
||||
@@ -160,6 +166,25 @@ class QdrantVectorStore:
|
||||
{"key": "vector_generation", "match": {"value": generation}},
|
||||
{"key": "document_id", "match": {"any": document_ids}},
|
||||
])
|
||||
purpose = metadata_filter.get("purpose")
|
||||
if purpose is not None:
|
||||
if not isinstance(purpose, str):
|
||||
raise VectorStoreError("Invalid vector metadata filter")
|
||||
filter_must.append({"key": "purposes", "match": {"value": purpose}})
|
||||
required_kinds = metadata_filter.get("required_kinds", [])
|
||||
if not isinstance(required_kinds, list) or not all(isinstance(item, str) for item in required_kinds):
|
||||
raise VectorStoreError("Invalid vector metadata filter")
|
||||
if required_kinds:
|
||||
filter_must.append({"key": "evidence_kind", "match": {"any": required_kinds}})
|
||||
for filter_key, payload_key in (
|
||||
("required_concepts", "scope.concepts"),
|
||||
("required_tables", "scope.tables"),
|
||||
("required_columns", "scope.columns"),
|
||||
):
|
||||
values = metadata_filter.get(filter_key, [])
|
||||
if not isinstance(values, list) or not all(isinstance(item, str) for item in values):
|
||||
raise VectorStoreError("Invalid vector metadata filter")
|
||||
filter_must.extend({"key": payload_key, "match": {"value": item}} for item in values)
|
||||
if query_text is None:
|
||||
if allowed_record_kinds == ["evidence"]:
|
||||
raise VectorStoreError("Evidence hybrid query text is required")
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -39,12 +39,18 @@ from tht.evidence.preprocessing import EvidenceEmbedder, build_preprocessing_pip
|
||||
from tht.evidence.search import (
|
||||
ActiveEvidenceSearcher,
|
||||
CorpusWorkspaceMismatchError,
|
||||
EvidenceQueryEmbedder,
|
||||
EvidenceResult,
|
||||
EvidenceSearchContext,
|
||||
EvidenceSearchOutcome,
|
||||
active_searcher,
|
||||
build_retrieval_entries,
|
||||
render_evidence_query,
|
||||
resolve_citation,
|
||||
search_evidence,
|
||||
validate_corpus_workspace,
|
||||
)
|
||||
from tht.evidence.session import project_session
|
||||
from tht.evidence.session import EvidenceReceipt, project_session, replace_evidence_receipt
|
||||
from tht.evidence.sources import build_sources
|
||||
|
||||
__all__ = [
|
||||
@@ -56,8 +62,13 @@ __all__ = [
|
||||
"EvidenceManifest",
|
||||
"EvidencePreparationError",
|
||||
"EvidencePreparationReport",
|
||||
"EvidenceQueryEmbedder",
|
||||
"EvidenceReceipt",
|
||||
"EvidenceResolutionReport",
|
||||
"EvidenceRestructurer",
|
||||
"EvidenceResult",
|
||||
"EvidenceSearchContext",
|
||||
"EvidenceSearchOutcome",
|
||||
"EvidenceSource",
|
||||
"EvidenceSourceError",
|
||||
"EvidenceSourceErrorCategory",
|
||||
@@ -82,8 +93,11 @@ __all__ = [
|
||||
"parse_curated_markdown",
|
||||
"prepare_workspace_evidence",
|
||||
"project_session",
|
||||
"render_evidence_query",
|
||||
"replace_evidence_receipt",
|
||||
"resolve_citation",
|
||||
"resolve_workspace_evidence",
|
||||
"search_evidence",
|
||||
"validate_corpus_workspace",
|
||||
"validate_namespaced_value",
|
||||
"validate_safe_metadata",
|
||||
|
||||
@@ -1,15 +1,175 @@
|
||||
"""Evidence-owned runtime lookup bound to the atomically active corpus generation."""
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal, Protocol
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from tht.evidence.canonical import EvidenceKind, EvidencePurpose
|
||||
from tht.evidence.corpus.store import CorpusStore
|
||||
from tht.ports.vector import VectorStoreError
|
||||
from tht.ports.vector import VectorReadUnavailable, VectorStoreError
|
||||
|
||||
|
||||
class CorpusWorkspaceMismatchError(RuntimeError):
|
||||
"""The configured workspace does not own the persisted corpus."""
|
||||
|
||||
|
||||
class EvidenceQueryEmbedder(Protocol):
|
||||
def embed_query(self, query: str) -> list[float]: ...
|
||||
|
||||
|
||||
class EvidenceSearchContext(BaseModel):
|
||||
"""Optional query enrichment and explicit, server-enforced Evidence constraints."""
|
||||
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
concepts: tuple[str, ...] = ()
|
||||
tables: tuple[str, ...] = ()
|
||||
columns: tuple[str, ...] = ()
|
||||
required_kinds: tuple[EvidenceKind, ...] = ()
|
||||
required_concepts: tuple[str, ...] = ()
|
||||
required_tables: tuple[str, ...] = ()
|
||||
required_columns: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvidenceResult:
|
||||
evidence_id: str
|
||||
title: str
|
||||
kind: str
|
||||
excerpts: tuple[str, ...]
|
||||
provenance: dict
|
||||
citation: str
|
||||
document_id: str
|
||||
score: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EvidenceSearchOutcome:
|
||||
status: Literal["available", "unavailable"]
|
||||
vector_generation: str | None
|
||||
results: tuple[EvidenceResult, ...] = ()
|
||||
code: str | None = None
|
||||
message: str | None = None
|
||||
|
||||
@classmethod
|
||||
def unavailable(cls, code: str, message: str) -> "EvidenceSearchOutcome":
|
||||
return cls("unavailable", None, (), code, message[:240])
|
||||
|
||||
|
||||
def _normalized_values(values: tuple[str, ...]) -> tuple[str, ...]:
|
||||
normalized = {
|
||||
unicodedata.normalize("NFC", value).strip()
|
||||
for value in values
|
||||
if isinstance(value, str) and unicodedata.normalize("NFC", value).strip()
|
||||
}
|
||||
return tuple(sorted(normalized))
|
||||
|
||||
|
||||
def render_evidence_query(query: str, context: EvidenceSearchContext) -> str:
|
||||
"""Build the one exact query text shared by dense and BM25 retrieval."""
|
||||
question = unicodedata.normalize("NFC", query).replace("\r\n", "\n").replace("\r", "\n").strip()
|
||||
if not question:
|
||||
raise ValueError("Evidence query must not be empty")
|
||||
sections = [("Domanda", question)]
|
||||
for label, values in (
|
||||
("Concetti", _normalized_values(context.concepts)),
|
||||
("Tabelle", _normalized_values(context.tables)),
|
||||
("Colonne", _normalized_values(context.columns)),
|
||||
):
|
||||
if values:
|
||||
sections.append((label, ", ".join(values)))
|
||||
return "\n".join(f"{label}: {value}" for label, value in sections)
|
||||
|
||||
|
||||
def _required_metadata_filter(purpose: EvidencePurpose, context: EvidenceSearchContext) -> dict[str, object]:
|
||||
return {
|
||||
"purpose": purpose,
|
||||
"required_kinds": list(_normalized_values(context.required_kinds)),
|
||||
"required_concepts": list(_normalized_values(context.required_concepts)),
|
||||
"required_tables": list(_normalized_values(context.required_tables)),
|
||||
"required_columns": list(_normalized_values(context.required_columns)),
|
||||
}
|
||||
|
||||
|
||||
def _active_vector_generation(searcher) -> str | None:
|
||||
supplied = getattr(searcher, "vector_generation", None)
|
||||
if isinstance(supplied, str) and supplied:
|
||||
return supplied
|
||||
corpus = getattr(searcher, "corpus", None)
|
||||
if corpus is None:
|
||||
return None
|
||||
with corpus.writer_lock():
|
||||
manifest = corpus.active_manifest()
|
||||
return manifest.vector_generation if manifest is not None else None
|
||||
|
||||
|
||||
def _group_evidence_fragments(hits) -> tuple[EvidenceResult, ...]:
|
||||
grouped: dict[str, list] = {}
|
||||
for hit in hits:
|
||||
metadata = getattr(hit, "metadata", {})
|
||||
evidence_id = metadata.get("evidence_id") if isinstance(metadata, dict) else None
|
||||
if not isinstance(evidence_id, str) or not evidence_id:
|
||||
raise VectorStoreError("Evidence search returned malformed payload")
|
||||
grouped.setdefault(evidence_id, []).append(hit)
|
||||
results = []
|
||||
for evidence_id, fragments in grouped.items():
|
||||
ordered = sorted(
|
||||
fragments,
|
||||
key=lambda item: (-float(item.similarity), int(item.metadata.get("ordinal", 0)), item.id),
|
||||
)
|
||||
first = ordered[0]
|
||||
metadata = first.metadata
|
||||
citation = metadata.get("source_uri")
|
||||
document_id = metadata.get("document_id")
|
||||
evidence_kind = metadata.get("evidence_kind")
|
||||
if not all(isinstance(value, str) and value for value in (citation, document_id, evidence_kind)):
|
||||
raise VectorStoreError("Evidence search returned malformed payload")
|
||||
results.append(EvidenceResult(
|
||||
evidence_id=evidence_id,
|
||||
title=str(first.title),
|
||||
kind=evidence_kind,
|
||||
excerpts=tuple(str(item.content) for item in ordered),
|
||||
provenance=dict(metadata.get("provenance", {})),
|
||||
citation=citation,
|
||||
document_id=document_id,
|
||||
score=float(first.similarity),
|
||||
))
|
||||
return tuple(sorted(results, key=lambda item: (-item.score, item.evidence_id)))
|
||||
|
||||
|
||||
def search_evidence(
|
||||
query: str,
|
||||
purpose: EvidencePurpose,
|
||||
context: EvidenceSearchContext,
|
||||
*,
|
||||
searcher,
|
||||
embedder: EvidenceQueryEmbedder,
|
||||
top_n: int = 10,
|
||||
) -> EvidenceSearchOutcome:
|
||||
"""Search the active generation once, with no stale-generation or purpose fallback."""
|
||||
try:
|
||||
rendered = render_evidence_query(query, context)
|
||||
generation = _active_vector_generation(searcher)
|
||||
if generation is None:
|
||||
return EvidenceSearchOutcome.unavailable("active_corpus_unavailable", "Active Evidence corpus is unavailable")
|
||||
query_embedding = embedder.embed_query(rendered)
|
||||
hits = searcher.search(
|
||||
query_embedding,
|
||||
top_n=top_n,
|
||||
kinds=["evidence"],
|
||||
query_text=rendered,
|
||||
metadata_filter=_required_metadata_filter(purpose, context),
|
||||
)
|
||||
return EvidenceSearchOutcome("available", generation, _group_evidence_fragments(hits))
|
||||
except VectorReadUnavailable:
|
||||
return EvidenceSearchOutcome.unavailable("vector_unavailable", "Evidence vector search is unavailable")
|
||||
except (VectorStoreError, CorpusWorkspaceMismatchError):
|
||||
return EvidenceSearchOutcome.unavailable("evidence_search_unavailable", "Evidence search is unavailable")
|
||||
|
||||
|
||||
class ActiveEvidenceSearcher:
|
||||
"""Searcher facade that enforces ACTIVE generation predicates before LIMIT."""
|
||||
|
||||
@@ -78,15 +238,17 @@ class ActiveEvidenceSearcher:
|
||||
if by_generation and (not isinstance(query_text, str) or query_text.strip() == ""):
|
||||
raise VectorStoreError("Evidence hybrid query text is required")
|
||||
for generation, document_ids in sorted(by_generation.items()):
|
||||
filters = dict(metadata_filter or {})
|
||||
filters.update({
|
||||
"vector_generation": generation,
|
||||
"document_ids": sorted(document_ids),
|
||||
"workspace_id": workspace_id,
|
||||
})
|
||||
hits.extend(self.delegate.search(
|
||||
embedding, top_n=top_n, kinds=["evidence"],
|
||||
query_text=query_text,
|
||||
query_language=query_language or self.evidence_language,
|
||||
metadata_filter={
|
||||
"vector_generation": generation,
|
||||
"document_ids": sorted(document_ids),
|
||||
"workspace_id": workspace_id,
|
||||
},
|
||||
metadata_filter=filters,
|
||||
))
|
||||
return sorted(hits, key=lambda hit: (-hit.similarity, hit.id))[:top_n]
|
||||
|
||||
@@ -144,9 +306,12 @@ def build_retrieval_entries(results, *, excerpt_chars: int) -> list[dict]:
|
||||
"""Project ordered Evidence search hits into the retrieval-pack shape."""
|
||||
return [
|
||||
{
|
||||
"title": result.label,
|
||||
"status": result.status,
|
||||
"excerpt": result.content[:excerpt_chars],
|
||||
"title": getattr(result, "title", getattr(result, "label", "")),
|
||||
"status": getattr(result, "status", None),
|
||||
"excerpt": (
|
||||
result.excerpts[0] if isinstance(result, EvidenceResult) and result.excerpts
|
||||
else getattr(result, "content", "")
|
||||
)[:excerpt_chars],
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -155,8 +320,14 @@ def build_retrieval_entries(results, *, excerpt_chars: int) -> list[dict]:
|
||||
__all__ = [
|
||||
"ActiveEvidenceSearcher",
|
||||
"CorpusWorkspaceMismatchError",
|
||||
"EvidenceQueryEmbedder",
|
||||
"EvidenceResult",
|
||||
"EvidenceSearchContext",
|
||||
"EvidenceSearchOutcome",
|
||||
"active_searcher",
|
||||
"build_retrieval_entries",
|
||||
"render_evidence_query",
|
||||
"resolve_citation",
|
||||
"search_evidence",
|
||||
"validate_corpus_workspace",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Evidence-specific projection into persisted session artifacts."""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -56,4 +58,34 @@ def project_session(
|
||||
return list(entries.values())
|
||||
|
||||
|
||||
__all__ = ["project_session"]
|
||||
@dataclass(frozen=True)
|
||||
class EvidenceReceipt:
|
||||
stage: str
|
||||
purpose: str
|
||||
vector_generation: str
|
||||
evidence_ids: tuple[str, ...]
|
||||
|
||||
def payload(self) -> dict:
|
||||
return {
|
||||
"stage": self.stage,
|
||||
"purpose": self.purpose,
|
||||
"vector_generation": self.vector_generation,
|
||||
"evidence_ids": list(self.evidence_ids),
|
||||
}
|
||||
|
||||
|
||||
def replace_evidence_receipt(repository, session_id: str, receipt: EvidenceReceipt) -> None:
|
||||
"""Replace the one minimal receipt for a semantic stage; preserve other stages."""
|
||||
current = repository.read_artifact(session_id, "evidence_receipts")
|
||||
try:
|
||||
receipts = json.loads(current) if current else []
|
||||
except json.JSONDecodeError as error:
|
||||
raise ValueError("Evidence receipts artifact is malformed") from error
|
||||
if not isinstance(receipts, list):
|
||||
raise TypeError("Evidence receipts artifact is malformed")
|
||||
replaced = [item for item in receipts if isinstance(item, dict) and item.get("stage") != receipt.stage]
|
||||
replaced.append(receipt.payload())
|
||||
repository.write_artifact(session_id, "evidence_receipts", json.dumps(replaced, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
__all__ = ["EvidenceReceipt", "project_session", "replace_evidence_receipt"]
|
||||
|
||||
@@ -12,6 +12,7 @@ PROJECTION_PATH = SKILL_ROOT / "SKILL.md"
|
||||
|
||||
# This tuple is the composition contract. Never derive it from directory order.
|
||||
FRAGMENT_ORDER = (
|
||||
("{{EVIDENCE_RUNTIME_SEARCH}}", "evidence/runtime-search.md"),
|
||||
("{{DISAMBIGUATION_OPEN_AMBIGUITY}}", "disambiguation/open-ambiguity.md"),
|
||||
("{{DISAMBIGUATION_INSTRUCTIONS}}", "disambiguation/phase-1.md"),
|
||||
("{{MEMORY_INSTRUCTIONS}}", "memory/phase-2.md"),
|
||||
|
||||
@@ -23,6 +23,7 @@ _ARTIFACT_FILES = {
|
||||
"question": "question.md",
|
||||
"schema_linking": "schema_linking.json",
|
||||
"evidence": "evidence.json",
|
||||
"evidence_receipts": "evidence_receipts.json",
|
||||
"sql_final": "sql_final.sql",
|
||||
"validation_report": "validation_report.md",
|
||||
"retrieval_pack": "retrieval_pack.md",
|
||||
|
||||
@@ -29,6 +29,7 @@ _ARTIFACT_KEYS = {
|
||||
"question",
|
||||
"schema_linking",
|
||||
"evidence",
|
||||
"evidence_receipts",
|
||||
"sql_final",
|
||||
"validation_report",
|
||||
"retrieval_pack",
|
||||
|
||||
Reference in New Issue
Block a user