"""Reprocess the 750 AI work items with V2 retrieval (section 16). Classification per item (INVALID_NCM): identity resolve (GTIN -> fingerprint) against the KB; validated non-conflicting knowledge resolves without AI. Otherwise hybrid retrieval: a HIGH_CONFIDENCE_RETRIEVAL requires hybrid score >= 0.75, an officially valid NCM and cross-tenant agreement >= 2 — it is NOT an automatic fiscal resolution (validator still required). Everything else with candidates -> LLM_SELECTION_REQUIRED; without candidates -> UNRESOLVED. CEST/EX_TIPI items never call retrieval semantics beyond the official candidate lists: exactly one official candidate resolves deterministically; none is UNRESOLVED (NO_OFFICIAL_MAPPING); more than one requires (future) LLM selection. PRODUCT_ROLE stays LLM/manual by contract. """ import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from sqlalchemy import select from central.app.ai.candidates import cest_candidates_for_ncm, ex_tipi_candidates_for_ncm from central.app.ai.kb import EXACT, FINGERPRINT, KnowledgeBase from central.app.ai.retrieval import (EnrichedNcmCorpus, KbNeighborIndex, derive_abbreviation_lexicon, hybrid_candidates) from central.app.db import SessionLocal from central.app.models import AiWorkItem, Dataset, KbObservation, KbProduct, NcmEntry HIGH_CONFIDENCE_THRESHOLD = 0.75 def _resolve_product(session, dedup_key: str): """invalid_ncm:kb:{id} -> KB product by id; invalid_ncm:{db}:{code} -> via observation.""" if dedup_key.startswith("invalid_ncm:kb:"): return session.get(KbProduct, int(dedup_key.rsplit(":", 1)[1])) parts = dedup_key.split(":") if len(parts) == 3: observation = session.scalar(select(KbObservation).where(KbObservation.database_label == parts[1], KbObservation.erp_code == parts[2])) return session.get(KbProduct, observation.kb_product_id) if observation else None return None def main(output="docs/AI_RESIDUAL_PLAN_V2.json") -> dict: session = SessionLocal() kb = KnowledgeBase(session) corpus = EnrichedNcmCorpus.build(session) index = KbNeighborIndex.build(session) lexicon = derive_abbreviation_lexicon(index.corpus(), min_support=3) dataset = session.scalar(select(Dataset).where(Dataset.kind == "NCM", Dataset.status == "ACTIVE")) valid_ncm = set(session.scalars(select(NcmEntry.code).where(NcmEntry.dataset_id == dataset.id)).all()) if dataset else set() items = session.scalars(select(AiWorkItem)).all() counts = {key: 0 for key in ("KB_EXACT_HIT", "KB_FINGERPRINT_HIT", "HIGH_CONFIDENCE_RETRIEVAL", "SINGLE_OFFICIAL_CANDIDATE", "LLM_SELECTION_REQUIRED", "UNRESOLVED")} llm_cells = 0 for item in items: if item.task_type == "INVALID_NCM": product = None product = _resolve_product(session, item.dedup_key) if product is not None and product.gtin: resolved, hit = kb.resolve(gtin=product.gtin) if resolved and hit == EXACT: knowledge = kb.current_knowledge(resolved.id).get("validated_ncm", {}) if knowledge and not knowledge.get("conflict"): item.status = "KB_EXACT_HIT"; counts["KB_EXACT_HIT"] += 1; continue if product is not None: resolved, hit = kb.resolve(fingerprint=product.fingerprint) if resolved and hit == FINGERPRINT: knowledge = kb.current_knowledge(resolved.id).get("validated_ncm", {}) if knowledge and not knowledge.get("conflict"): item.status = "KB_FINGERPRINT_HIT"; counts["KB_FINGERPRINT_HIT"] += 1; continue description = product.normalized_description if product is not None else (item.payload or {}).get("description", "") ranked = hybrid_candidates(corpus, index, description, k=10, lexicon=lexicon, exclude_product_id=(product.id if product else None)) if ranked and ranked[0][2] >= HIGH_CONFIDENCE_THRESHOLD and ranked[0][0] in valid_ncm and ranked[0][5] >= 2: item.status = "HIGH_CONFIDENCE_RETRIEVAL"; counts["HIGH_CONFIDENCE_RETRIEVAL"] += 1 item.payload = {**item.payload, "retrieval_top1": ranked[0][0], "retrieval_score": ranked[0][2], "tenant_agreement": ranked[0][5]} elif ranked: item.status = "LLM_SELECTION_REQUIRED"; counts["LLM_SELECTION_REQUIRED"] += 1; llm_cells += item.cells_covered else: item.status = "UNRESOLVED"; counts["UNRESOLVED"] += 1 elif item.task_type in ("CEST", "EX_TIPI"): ncm = (item.payload or {}).get("ncm", "") candidates = cest_candidates_for_ncm(session, ncm) if item.task_type == "CEST" else ex_tipi_candidates_for_ncm(session, ncm) if len(candidates) == 1: item.status = "SINGLE_OFFICIAL_CANDIDATE"; counts["SINGLE_OFFICIAL_CANDIDATE"] += 1 elif not candidates: item.status = "UNRESOLVED_NO_OFFICIAL_MAPPING"; counts["UNRESOLVED"] += 1 else: item.status = "LLM_SELECTION_REQUIRED"; counts["LLM_SELECTION_REQUIRED"] += 1; llm_cells += item.cells_covered else: # PRODUCT_ROLE item.status = "LLM_SELECTION_REQUIRED"; counts["LLM_SELECTION_REQUIRED"] += 1; llm_cells += item.cells_covered session.commit() llm_items = counts["LLM_SELECTION_REQUIRED"] report = { "artifact": "AI_RESIDUAL_PLAN_V2", "work_items": len(items), "classification": counts, "llm_selection_required_items": llm_items, "llm_selection_required_cells": llm_cells, "high_confidence_threshold": HIGH_CONFIDENCE_THRESHOLD, "note": "HIGH_CONFIDENCE_RETRIEVAL is a candidate, not a fiscal resolution: the official validator + operator gate still apply. AI_WRITE_ALLOWED=false.", "dataset_versions": {"ncm": dataset.sha256} if dataset else {}, } Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") session.close() return report if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--output", default="/tmp/plan_v2.json") args = parser.parse_args() print(json.dumps(main(args.output), indent=2))