Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'

git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
This commit is contained in:
gti
2026-09-29 14:43:23 -03:00
868 changed files with 566394 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
"""Cost plan for the residual AI work items, with budget guard (section 18).
Runs BEFORE any real execution: UNIQUE_AI_WORK_ITEMS, token estimates per task type,
estimated cost, and the authorize/refuse verdict against AI_MAX_RUN_COST_USD.
"""
import argparse
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.provider import BudgetGuard
from central.app.config import settings
from central.app.db import SessionLocal
from central.app.models import AiWorkItem
TOKENS_PER_ITEM = { # conservative prompt+schema overhead per single work item
"INVALID_NCM": (700, 120),
"CEST": (500, 80),
"EX_TIPI": (500, 80),
"PRODUCT_ROLE": (400, 60),
}
def main(output="docs/AI_COST_PLAN_V1.json") -> dict:
session = SessionLocal()
items = session.scalars(select(AiWorkItem)).all()
guard = BudgetGuard(settings.ai_max_run_cost_usd, settings.ai_price_in_per_m, settings.ai_price_out_per_m)
per_task = {}
in_total = out_total = 0
resolved = {"KB_HIT", "KB_EXACT_HIT", "KB_FINGERPRINT_HIT", "HIGH_CONFIDENCE_RETRIEVAL", "SINGLE_OFFICIAL_CANDIDATE", "DONE", "SKIPPED", "CONFLICT"}
for item in items:
if item.status in resolved:
continue # cache/official candidate resolved — no AI call needed
tokens_in, tokens_out = TOKENS_PER_ITEM.get(item.task_type, (500, 80))
bucket = per_task.setdefault(item.task_type, {"items": 0, "input_tokens": 0, "output_tokens": 0, "cells_covered": 0})
bucket["items"] += 1; bucket["input_tokens"] += tokens_in; bucket["output_tokens"] += tokens_out
bucket["cells_covered"] += item.cells_covered
in_total += tokens_in; out_total += tokens_out
plan = guard.estimate(in_total, out_total)
report = {
"artifact": "AI_COST_PLAN_V1",
"unique_ai_work_items": sum(bucket["items"] for bucket in per_task.values()),
"per_task_type": per_task,
"estimated_input_tokens": plan["input_tokens"],
"estimated_output_tokens": plan["output_tokens"],
"estimated_cost_usd": plan["estimated_cost"],
"ai_max_run_cost_usd": guard.max_run_cost_usd,
"budget_authorized": guard.authorize(plan),
"provider_configured": settings.ai_provider,
"model_configured": settings.ai_model,
"ai_enabled": settings.ai_enabled,
"max_batch_size": settings.ai_max_batch_size,
"min_confidence": settings.ai_min_confidence,
}
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/ai_cost_plan.json")
args = parser.parse_args()
print(json.dumps(main(args.output), indent=2))
+60
View File
@@ -0,0 +1,60 @@
"""GTIN KB audit (section 10) — validity, cross-tenant matches and conflicts.
Reads the seed payload (ground truth of what was observed per tenant) plus the KB tables.
"""
import json
import sys
from collections import defaultdict
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from central.app.ai.fingerprint import validate_gtin
def main(payload_path="docs/runs/ai_kb_seed_payload.json", output="docs/GTIN_KB_AUDIT_V1.json") -> dict:
payload = json.loads(Path(payload_path).read_text(encoding="utf8"))
by_gtin = defaultdict(list)
invalid_gtin_raw = 0
for product in payload["products"]:
raw = product.get("gtin")
if raw is None:
continue
if validate_gtin(raw) is None:
invalid_gtin_raw += 1 # defensive: export already validates
continue
by_gtin[raw].append(product)
duplicated_same_product = sum(1 for entries in by_gtin.values() if len(entries) > 1)
conflicting = 0
cross_tenant = 0
conflict_examples = []
for gtin, entries in by_gtin.items():
ncms = {entry.get("ncm") for entry in entries if entry.get("ncm")}
databases = {obs["database"] for entry in entries for obs in entry["observations"]}
if len(ncms) > 1:
conflicting += 1
if len(conflict_examples) < 10:
conflict_examples.append({"gtin": gtin, "ncms": sorted(ncms)})
if len(databases) > 1:
cross_tenant += 1
report = {
"artifact": "GTIN_KB_AUDIT_V1",
"gtin_valid": len(by_gtin),
"gtin_invalid_raw": invalid_gtin_raw,
"gtin_duplicated_same_product": duplicated_same_product,
"gtin_conflicting_products": conflicting,
"gtin_cross_tenant_matches": cross_tenant,
"conflict_examples": conflict_examples,
"policy": "same GTIN across tenants is STRONG evidence for sharing global product knowledge; tenant fiscal context is never shared",
}
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
return report
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--payload-path", default="/tmp/ai_kb_seed_payload.json")
parser.add_argument("--output", default="/tmp/gtin_audit.json")
args = parser.parse_args()
print(json.dumps(main(args.payload_path, args.output), indent=2))
@@ -0,0 +1,21 @@
"""Audit the historical KB conflicts without inventing missing evidence."""
import argparse
import json
from pathlib import Path
from central.app.ai.historical_conflicts import reconstruct_historical_conflicts
def main(payload_path="docs/runs/ai_kb_seed_payload.json", output="docs/KB_HISTORICAL_CONFLICT_AUDIT_V1.json"):
payload = json.loads(Path(payload_path).read_text(encoding="utf-8"))
report = reconstruct_historical_conflicts(payload)
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--payload", default="docs/runs/ai_kb_seed_payload.json")
parser.add_argument("--output", default="docs/KB_HISTORICAL_CONFLICT_AUDIT_V1.json")
args = parser.parse_args()
print(json.dumps(main(args.payload, args.output), indent=2, ensure_ascii=False))
@@ -0,0 +1,21 @@
"""Create the conservative KB conflict audit from the read-only seed payload."""
import argparse
import json
from pathlib import Path
from central.app.ai.kb_consolidation import audit_payload
def main(payload_path="docs/runs/ai_kb_seed_payload.json", output="docs/KB_CONFLICT_AUDIT_V1.json"):
payload = json.loads(Path(payload_path).read_text(encoding="utf-8"))
report = audit_payload(payload)
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--payload-path", default="docs/runs/ai_kb_seed_payload.json")
parser.add_argument("--output", default="docs/KB_CONFLICT_AUDIT_V1.json")
args = parser.parse_args()
print(json.dumps(main(args.payload_path, args.output), indent=2, ensure_ascii=False))
@@ -0,0 +1,83 @@
"""Build deduplicated AI work items from the residual questions and the seeded KB.
Reads docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json, resolves each invalid-NCM product
question to its KB fingerprint (same product in several databases collapses into ONE
work item), persists AiWorkItem rows and writes docs/AI_RESIDUAL_PLAN_V1.json.
"""
import argparse
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.kb import KnowledgeBase
from central.app.ai.workitems import build_work_items
from central.app.db import SessionLocal
from central.app.models import AiWorkItem, KbObservation
def _erp_code_from_question(question) -> str:
import re
match = re.search(r"PRODUCT\s+(\d+)", question.get("QUESTION", ""))
return match.group(1) if match else ""
def main(questions_path="docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json", output="docs/AI_RESIDUAL_PLAN_V1.json") -> dict:
session = SessionLocal()
questions = json.loads(Path(questions_path).read_text(encoding="utf8"))["questions"]
# map "{DATABASE}:{erp_code}" -> fingerprint using the seeded observations
rows = session.execute(select(KbObservation.company_key, KbObservation.erp_code, KbObservation.kb_product_id, KbObservation.database_label)).all()
product_ids = {f"{row.database_label}:{row.erp_code}": row.kb_product_id for row in rows}
questions = [dict(question, DATABASE_CODE=_erp_code_from_question(question)) for question in questions]
product_fingerprints = {key: f"kb:{value}" for key, value in product_ids.items()}
items = build_work_items(questions, product_fingerprints)
raw = sum(len(item.question_refs) for item in items)
persisted = {"created": 0, "updated": 0, "kb_hits": 0, "conflicts": 0}
kb = KnowledgeBase(session)
for item in items:
row = session.scalar(select(AiWorkItem).where(AiWorkItem.dedup_key == item.dedup_key))
if row is None:
row = AiWorkItem(task_type=item.task_type, dedup_key=item.dedup_key, question_refs=item.question_refs, payload=item.payload, cells_covered=item.cells_covered, status="PENDING")
session.add(row); persisted["created"] += 1
else:
row.question_refs = item.question_refs; row.cells_covered = item.cells_covered; row.payload = item.payload
persisted["updated"] += 1
# cache-first: an existing non-conflicting validated answer makes this a KB hit, no AI call
if item.dedup_key.startswith("invalid_ncm:kb:") and item.dedup_key != "invalid_ncm:kb:None":
kb_product_id = int(item.dedup_key.rsplit(":", 1)[1])
knowledge = kb.current_knowledge(kb_product_id)
validated = knowledge.get("validated_ncm", {})
if validated and not validated.get("conflict"):
row.status = "KB_HIT"; persisted["kb_hits"] += 1
elif validated.get("conflict"):
row.status = "CONFLICT"; persisted["conflicts"] += 1
session.commit()
by_task = {}
for item in items:
bucket = by_task.setdefault(item.task_type, {"items": 0, "cells": 0})
bucket["items"] += 1; bucket["cells"] += item.cells_covered
report = {
"artifact": "AI_RESIDUAL_PLAN_V1",
"raw_residual_questions": len(questions),
"ai_work_items_raw": raw,
"ai_work_items_deduped": len(items),
"by_task_type": by_task,
"persisted": persisted,
"ai_write_allowed": False,
"company_decisions_remaining": 14,
}
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
session.close()
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--questions", default="docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json")
parser.add_argument("--output", default="docs/AI_RESIDUAL_PLAN_V1.json")
args = parser.parse_args()
print(json.dumps(main(args.questions, args.output), indent=2))
+21
View File
@@ -0,0 +1,21 @@
"""Create a human panel administrator without ever printing the password."""
import getpass
from central.app.db import SessionLocal
from central.app.human_auth import create_admin
def main():
username = input("Admin username: ").strip()
password = getpass.getpass("Admin password: ")
confirmation = getpass.getpass("Confirm password: ")
if password != confirmation:
raise SystemExit("passwords do not match")
with SessionLocal() as session:
admin = create_admin(session, username, password)
print(f"created admin {admin.username}")
if __name__ == "__main__":
main()
@@ -0,0 +1,68 @@
"""Generate explicit, deterministic non-production control-panel data."""
import argparse
import uuid
from datetime import datetime, timedelta, timezone
from central.app.config import settings
from central.app.db import SessionLocal
from central.app.models import Agent, AgentEvent, AgentSnapshot, BlockedProduct, Installation, Job, Tenant
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
def generate_fixture(session, agent_count=100, seed=7):
now = datetime.now(timezone.utc)
states = ["ONLINE"] * 50 + ["DEGRADED"] * 20 + ["OFFLINE"] * 15 + ["FAILED"] * 10 + ["UNKNOWN"] * 5
created = {"agents": 0, "events": 0, "blockers": 0, "jobs": 0}
for index in range(agent_count):
tenant = Tenant(name=f"Fixture Client {index:03d}"); session.add(tenant); session.flush()
agent_version = None if index % 17 == 0 else ("0.9.0" if index % 5 == 0 else "1.0.0rc1")
agent = Agent(tenant_id=tenant.id, name=f"fixture-agent-{index:03d}", token_hash=(f"fixture-{index:03d}".ljust(64, "0"))[:64], agent_version=agent_version); session.add(agent); session.flush()
state = states[index % len(states)]
last_seen = None if state == "UNKNOWN" else now - timedelta(seconds=10 if state in {"ONLINE", "DEGRADED"} else 10_000)
installation = Installation(agent_id=agent.id, tenant_id=tenant.id, installation_id=str(uuid.uuid5(uuid.NAMESPACE_DNS, f"fixture-{index}")), display_name=f"{tenant.name} - Servidor Principal", customer_code=f"F{index:03d}", hostname=f"srv-{index:03d}", company_name=f"Empresa {index:03d}", company_uf="BA", company_crt="1", last_seen_at=last_seen, last_status="IDLE")
session.add(installation); session.flush()
total_products = 5000 + index
safe_blocked = 1 if index % 2 == 0 else 0
failed_products = 1 if state == "FAILED" else 0
fiscally_complete = total_products - safe_blocked - failed_products
coverage_percent = round(fiscally_complete * 100 / total_products, 1)
snapshot = AgentSnapshot(installation_id=installation.id, total_products=total_products, fiscally_complete=fiscally_complete, safe_blocked=safe_blocked, failed_products=failed_products, coverage_percent=coverage_percent, blockers={"CFOP": index % 7, "ST": index % 5}, agent_health=state, version_drift="UNKNOWN" if agent_version is None else ("OUTDATED" if agent_version != "1.0.0rc1" else "CURRENT"), last_job_status="FAILED" if state in {"DEGRADED", "FAILED"} else "COMPLETED")
session.add(snapshot); created["agents"] += 1
failed = state in {"DEGRADED", "FAILED"}
job = Job(agent_id=agent.id, kind="REPROCESS_ALL", status="FAILED" if failed else "COMPLETED", payload={}, result_summary={"fixture": True}, error="fixture technical failure" if failed else None, started_at=now - timedelta(minutes=6) if failed else None, finished_at=now - timedelta(minutes=5) if failed else None)
session.add(job); session.flush()
created["jobs"] += 1
fixture_blocker = None
if index % 2 == 0:
fixture_blocker = BlockedProduct(tenant_id=tenant.id, installation_id=installation.id, company=tenant.name, product_code=f"P{index:06d}", description="Fixture product", blocker_type="CFOP", field="CFOP", reason_code="INSUFFICIENT_EVIDENCE", evidence_summary={"fixture": True}, input_fingerprint=f"{index:064d}", blocker_fingerprint=f"{index+1000:064d}")
session.add(fixture_blocker); session.flush()
created["blockers"] += 1
if index == 0:
session.add(BlockedProduct(tenant_id=tenant.id, installation_id=installation.id, company=tenant.name, product_code=fixture_blocker.product_code, description="Fixture product", blocker_type="TAX_RULE_MISSING", field="ST", reason_code="MISSING_REFERENCE", evidence_summary={"fixture": True}, input_fingerprint=f"{index+2000:064d}", blocker_fingerprint=f"{index+3000:064d}"))
created["blockers"] += 1
for event_index in range(5):
linked = event_index == 0
session.add(AgentEvent(tenant_id=tenant.id, installation_id=installation.id, agent_id=agent.id, severity="ERROR" if failed and linked else "INFO", event_type="JOB_FAILED" if failed and linked else "HEARTBEAT", message="Fixture job failed" if failed and linked else "Fixture operational event", structured_context={"fixture": True}, job_id=job.id if linked else None, product_code=fixture_blocker.product_code if linked and fixture_blocker else None, blocker_id=fixture_blocker.id if linked and fixture_blocker else None, occurred_at=now - timedelta(minutes=event_index)))
created["events"] += 1
session.commit()
return created
def main():
parser = argparse.ArgumentParser(); parser.add_argument("--database-url", default=None); parser.add_argument("--agents", type=int, default=100); parser.add_argument("--seed", type=int, default=7); parser.add_argument("--confirm-non-production", action="store_true")
args = parser.parse_args()
if settings.environment == "production" or not args.confirm_non_production:
raise SystemExit("refusing fixture generation without --confirm-non-production in non-production")
session_factory = SessionLocal
if args.database_url:
session_factory = sessionmaker(bind=create_engine(args.database_url))
with session_factory() as session:
print(generate_fixture(session, args.agents, args.seed))
if __name__ == "__main__":
main()
@@ -0,0 +1,70 @@
"""Promote RICMS-BA Anexo 1 (BA_ST_MVA) from the local official PDF snapshot.
No network fetch: RICMS-BA Anexo 1 has no stable public direct-download URL
(the SEFAZ-BA legislation portal serves it via a session-gated viewer), so
the authoritative copy is kept in sources/raw/bootstrap_v1/. This script
re-validates it against the currently ACTIVE Postgres dataset and only
promotes a new version if the parsed content actually differs, never
overwriting a valid ACTIVE version with a partial/regressed parse.
"""
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from pypdf import PdfReader
from central.app.datasets.ba_st_mva import parse_page
from central.app.datasets.service import DatasetService
from central.app.db import SessionLocal
from central.app.models import Dataset
SOURCE = Path("sources/raw/bootstrap_v1/decreto_2012_13780_ricms_anexo_1_vigente_2026.pdf")
SOURCE_URL = f"file://{SOURCE.name}"
def main():
raw_bytes = SOURCE.read_bytes()
raw_sha256 = hashlib.sha256(raw_bytes).hexdigest()
records, rejected = [], []
for page, value in enumerate(PdfReader(SOURCE).pages, start=1):
found, failed = parse_page(value.extract_text() or "", page)
records.extend(found)
rejected.extend(failed)
if not records:
print(json.dumps({"status": "FAILED", "reason": "parser produced zero records; keeping existing ACTIVE dataset"}))
return
with SessionLocal() as session:
current_active = session.scalar(
__import__("sqlalchemy").select(Dataset).where(Dataset.kind == "BA_ST_MVA", Dataset.status == "ACTIVE")
)
if current_active is not None and current_active.raw_sha256 == raw_sha256:
print(json.dumps({
"status": "UNCHANGED", "dataset_id": current_active.id, "raw_sha256": raw_sha256,
"record_count": current_active.record_count,
}))
return
if current_active is not None and len(records) < current_active.record_count * 0.9:
# Smoke gate: refuse to promote a parse that lost >10% of records versus
# the currently ACTIVE version — never silently replace a good version
# with a broken one.
print(json.dumps({
"status": "FAILED",
"reason": f"new parse yielded {len(records)} records, active has {current_active.record_count} (>10% loss); keeping ACTIVE",
}))
return
dataset = DatasetService(session).import_ba_st_mva(records, raw_sha256, SOURCE_URL, datetime.now(timezone.utc))
print(json.dumps({
"status": dataset.status, "dataset_id": dataset.id, "raw_sha256": raw_sha256,
"record_count": dataset.record_count, "rejected": len(rejected),
"previously_active_count": current_active.record_count if current_active else None,
}))
if __name__ == "__main__":
main()
@@ -0,0 +1,46 @@
"""Promote CFOP/FCP/ANP/NCM_UTRIB/RTC_BIOFUEL_INDEX/RTC_CCREDPRES from the
official spreadsheets the operator placed in sources/raw/bootstrap_v1/ after
the Portal NF-e/SPED downloads were session-blocked. No network fetch.
"""
import json
from datetime import datetime, timezone
from pathlib import Path
from central.app.datasets import bootstrap_v1_extra as parsers
from central.app.datasets.service import DatasetService
from central.app.db import SessionLocal
ROOT = Path("sources/raw/bootstrap_v1")
JOBS = [
("CFOP", ROOT / "IT_2023_002_v2_10_Tabela_CFOP_Descrição.xlsx", parsers.parse_cfop, "import_cfop"),
("FCP", ROOT / "Tabela_Alíquotas_FCP_por_UF_2025-02_07.xlsx", parsers.parse_fcp, "import_fcp"),
("ANP", ROOT / "Tabela_Codigo_Produto_ANP-20241118.xlsx", parsers.parse_anp, "import_anp"),
("NCM_UTRIB", ROOT / "IT2024.001v2.40 - Tabela NCM 2022 com Utrib_Comércio Exterior_vigência 01.10.26.xlsx", parsers.parse_ncm_utrib, "import_ncm_utrib"),
("RTC_BIOFUEL_INDEX", ROOT / "IT 2025.004 v.1.20 - Tabela de Índice.xlsx", parsers.parse_rtc_biofuel_index, "import_rtc_biofuel_index"),
("RTC_CCREDPRES", ROOT / "cCredPres_2026-06-22.xlsx", parsers.parse_rtc_cred_pres, "import_rtc_cred_pres"),
("RTC_CCLASSTRIB", ROOT / "cClassTrib 2026-06-22.xlsx", parsers.parse_rtc_cclasstrib, "import_rtc_cclasstrib"),
("RTC_CBS_RATES", ROOT / "IT 2026.002 v.1.00 - Aliquotas da CBS.xlsx", parsers.parse_rtc_cbs_rates, "import_rtc_cbs_rates"),
]
def main():
results = {}
with SessionLocal() as session:
service = DatasetService(session)
for domain, path, parse_fn, method_name in JOBS:
try:
if not path.exists():
results[domain] = {"status": "MISSING_BOOTSTRAP_FILE", "path": str(path)}
continue
payload = parse_fn(path.read_bytes())
dataset = getattr(service, method_name)(payload, f"file://{path.name}", datetime.now(timezone.utc))
results[domain] = {"status": dataset.status, "dataset_id": dataset.id, "sha256": dataset.sha256, "record_count": dataset.record_count}
except Exception as error: # noqa: BLE001 — isolate one domain's failure from the rest
results[domain] = {"status": "FAILED", "reason": f"{type(error).__name__}: {error}"}
print(json.dumps(results, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
+27
View File
@@ -0,0 +1,27 @@
"""Download and promote the official CONFAZ CV142_18 HTML CEST source."""
from datetime import datetime, timezone
from pathlib import Path
import httpx
from central.app.config import settings
from central.app.datasets.cest import parse_cest_html
from central.app.datasets.service import DatasetService
from central.app.db import SessionLocal
def main():
response = httpx.get(settings.cest_dataset_url, timeout=settings.http_timeout_seconds, follow_redirects=True)
response.raise_for_status()
parsed = parse_cest_html(response.content)
path = Path(settings.dataset_storage_path)
path.mkdir(parents=True, exist_ok=True)
(path / f"{parsed.sha256}.html").write_bytes(response.content)
with SessionLocal() as session:
dataset = DatasetService(session).import_cest_payload(parsed, settings.cest_dataset_url, datetime.now(timezone.utc))
print({"id": dataset.id, "status": dataset.status, "sha256": dataset.sha256, "record_count": dataset.record_count, "version": parsed.version})
if __name__ == "__main__":
main()
@@ -0,0 +1,41 @@
"""Import historical ERP evidence (miner output) into the central KB. Dry-run by default; --apply commits. Local data only, no provider call.
python -m central.scripts.import_kb_history --staging /path/to/history [--apply] [--report out.json]
Runs against the database in DATABASE_URL (the Hub's PostgreSQL). Idempotent. Never touches any client database.
"""
import argparse
import json
import sys
import time
from pathlib import Path
from central.app.db import SessionLocal
from central.app.enrichment import history
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--staging", required=True)
ap.add_argument("--apply", action="store_true")
ap.add_argument("--report", default=None)
ap.add_argument("--only-current-vigente-gtins", action="store_true", help="skip GTINs without any NCM evidence")
args = ap.parse_args(argv)
files = sorted(f for f in Path(args.staging).glob("*.json") if not f.name.startswith("_"))
stagings = [json.loads(f.read_text(encoding="utf-8")) for f in files]
t0 = time.time()
obs = history.build_observations(stagings)
if args.only_current_vigente_gtins:
obs["gtins"] = {k: v for k, v in obs["gtins"].items() if v["ncm"]}
with SessionLocal() as session:
current = history.official_sets(session)
report = history.apply(session, obs, lambda v: v in current, dry_run=not args.apply)
report.update({"mode": "APPLY" if args.apply else "DRY_RUN", "clients": [s["client"] for s in stagings], "build_stats": obs["stats"], "seconds": round(time.time() - t0, 1)})
text = json.dumps(report, indent=1, default=str)
if args.report:
Path(args.report).write_text(text, encoding="utf-8")
print(text)
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,25 @@
"""Seed reviewed official legacy NCM evidence; it never selects a successor."""
from central.app.db import SessionLocal
from central.app.models import LegacyNcmEntry
LEGACY_NCM = {
"code": "02071400",
"source": "Siscomex NCM official nomenclature download",
"source_date": "2026-09-17",
"reason": "NCM not present in current official nomenclature; no official successor.",
}
def main():
with SessionLocal() as session:
entry = session.get(LegacyNcmEntry, LEGACY_NCM["code"])
if entry is None:
session.add(LegacyNcmEntry(**LEGACY_NCM, successor_code=None))
session.commit()
print({"code": LEGACY_NCM["code"], "status": "LEGACY_NCM", "successor_code": None})
if __name__ == "__main__":
main()
@@ -0,0 +1,34 @@
"""Promote ORIGEM_MERCADORIA/CST_ICMS/CSOSN/CST_PIS/CST_COFINS from the local
official MOC (Anexo I NF-e/NFC-e) PDF. No network fetch — this manual has no
stable public direct-download URL either (session-gated portal), so the
authoritative copy lives in sources/raw/bootstrap_v1/.
"""
import json
from datetime import datetime, timezone
from pathlib import Path
from central.app.datasets.nfe_moc_tables import parse_moc_tables
from central.app.datasets.service import DatasetService
from central.app.db import SessionLocal
SOURCE = Path("sources/raw/bootstrap_v1/moc7-anexo-i-leiaute-e-rv.pdf")
SOURCE_URL = f"file://{SOURCE.name}"
def main():
raw_bytes = SOURCE.read_bytes()
payload = parse_moc_tables(raw_bytes)
with SessionLocal() as session:
dataset = DatasetService(session).import_moc_tables(payload, SOURCE_URL, datetime.now(timezone.utc))
from collections import Counter
domains = Counter(e.domain for e in payload.entries)
print(json.dumps({
"status": dataset.status, "dataset_id": dataset.id, "sha256": dataset.sha256,
"record_count": dataset.record_count, "domains": dict(domains),
}, indent=2))
if __name__ == "__main__":
main()
+19
View File
@@ -0,0 +1,19 @@
"""Download, persist and promote the official NCM dataset."""
from datetime import datetime, timezone
from pathlib import Path
import httpx
from central.app.config import settings
from central.app.datasets.ncm import parse_ncm_payload
from central.app.datasets.service import DatasetService
from central.app.db import SessionLocal
def main():
response = httpx.get(settings.ncm_dataset_url, timeout=settings.http_timeout_seconds, follow_redirects=True)
response.raise_for_status()
parsed = parse_ncm_payload(response.content)
path = Path(settings.dataset_storage_path); path.mkdir(parents=True, exist_ok=True)
(path / f"{parsed.sha256}.json").write_bytes(response.content)
with SessionLocal() as session:
dataset = DatasetService(session).import_payload(parsed, settings.ncm_dataset_url, datetime.now(timezone.utc))
print({"id": dataset.id, "status": dataset.status, "sha256": dataset.sha256, "record_count": dataset.record_count})
if __name__ == "__main__": main()
@@ -0,0 +1,12 @@
import json
from pathlib import Path
from pypdf import PdfReader
from central.app.datasets.ba_st_mva import parse_page
source = Path("sources/raw/bootstrap_v1/decreto_2012_13780_ricms_anexo_1_vigente_2026.pdf")
records, rejected = [], []
for page, value in enumerate(PdfReader(source).pages, start=1):
found, failed = parse_page(value.extract_text() or "", page)
records.extend(found); rejected.extend(failed)
Path("docs/ba_st_mva_inspection.json").write_text(json.dumps({"records": records, "rejected": rejected}, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"pages": page, "records": len(records), "rejected": len(rejected), "cest": sum(bool(x["cest"]) for x in records), "ncm": sum(bool(x["ncm_sh"]) for x in records), "mva": sum(bool(x["mva_original"] or x["mva_adjusted"]) for x in records)}))
+22
View File
@@ -0,0 +1,22 @@
"""Expire eligible operational events; audit and critical events are preserved."""
import argparse
from central.app.db import SessionLocal
from central.app.events import purge_expired_events
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--retention-days", type=int, default=None)
args = parser.parse_args()
if args.dry_run:
print("dry-run: no events deleted")
return
with SessionLocal() as session:
print(f"deleted_events={purge_expired_events(session, retention_days=args.retention_days)}")
if __name__ == "__main__":
main()
+44
View File
@@ -0,0 +1,44 @@
"""Verify a small, prioritised sample of CORROBORATED GTINs with OSCBR (free plan). Credentials come ONLY from the Hub environment (OSCBR_TOKEN or OSCBR_USER/OSCBR_PASSWORD).
python -m central.scripts.oscbr_backfill --limit 20 [--dry-run] [--report out.json]
Never spends the operational reserve (PROVIDER_OPERATIONAL_RESERVE_PERCENT), respects 5/min and 50/day, and never queries VERIFIED_KB, CONFLICT or invalid GTINs.
"""
import argparse
import json
import sys
from central.app.config import settings
from central.app.db import SessionLocal
from central.app.enrichment import backfill, history
from central.app.enrichment.providers import build_providers
from central.app.enrichment.quota import ProviderGateway
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--limit", type=int, default=20)
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--report", default=None)
args = ap.parse_args(argv)
providers = build_providers(settings)
oscbr = next(p for p in providers if p.name == "oscbr")
if not oscbr.provides_ncm and not args.dry_run:
print(json.dumps({"OSCBR_ROLE": "IDENTITY", "reason": "NCM not documented in the V3 contract; backfill for NCM verification is meaningless until proven", "requests": 0}))
return 3
if not oscbr.configured and not args.dry_run:
print(json.dumps({"OSCBR_STATUS": "NOT_CONFIGURED", "requests": 0}))
return 2
with SessionLocal() as session:
current = history.official_sets(session)
report = backfill.run(session, ProviderGateway(session, [oscbr]), oscbr, limit=args.limit, reserve_percent=settings.provider_operational_reserve_percent,
is_official=lambda v: v in current, dry_run=args.dry_run)
text = json.dumps(report, indent=1, default=str)
if args.report:
open(args.report, "w", encoding="utf-8").write(text)
print(text)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,31 @@
"""Prepare versioned candidate evidence from already computed deterministic candidates."""
import argparse
import json
from pathlib import Path
from central.app.ai.retrieval_v3 import prepare_candidate_set
def main(input_path, output_path):
source = json.loads(Path(input_path).read_text(encoding="utf-8"))
prepared = []
for item in source.get("items", []):
prepared.append(prepare_candidate_set(
type("WorkItem", (), {"task_type": item.get("task_type", "INVALID_NCM"), "dedup_key": item["dedup_key"], "payload": item.get("payload", {})})(),
[tuple(row) for row in item.get("candidates", [])],
dataset_versions=source.get("dataset_versions"),
gtin_evidence=item.get("gtin_evidence"),
kb_neighbors=item.get("kb_neighbors"),
hierarchical_context=item.get("hierarchical_context"),
))
report = {"artifact": "AI_CANDIDATE_PREPARATION_V3", "items": prepared, "semantic_decisions": 0}
Path(output_path).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("input_path")
parser.add_argument("output_path")
args = parser.parse_args()
print(json.dumps(main(args.input_path, args.output_path), indent=2, ensure_ascii=False))
@@ -0,0 +1,100 @@
"""Administrative CLI: publish a built Agent package as an AgentRelease, ready
to be downloaded/enrolled from the panel. Accepts either the legacy universal
ZIP (`scripts/build_agent_package.py` output) or the universal Windows
installer EXE (`scripts/build_windows_installer.ps1` output). Whatever is
published is served verbatim to every installation later.
Like register_agent.py, this is a one-time, operator-run action against
DATABASE_URL directly — never a browser upload endpoint, so the release
binary never has to pass through an HTTP request body. The artifact is copied
into the Hub's local release storage (AGENT_RELEASE_STORAGE_PATH), never
into PostgreSQL; only its metadata (version, sha256, size, path) is a row.
Usage:
python central/scripts/publish_agent_release.py --artifact dist/GTIFiscalAgent-1.0.0.zip \\
--notes "First distributable build with secure enrollment"
python central/scripts/publish_agent_release.py --artifact dist/installer/GTIFiscalAgent-Setup-1.1.1.exe \\
--notes "Universal Windows installer with install-code enrollment"
Refuses to publish the same version+platform+architecture twice with a
different sha256 (an accidental respin must get a new version). Publishing
the exact same bytes again is a no-op (idempotent), and a PUBLISHED release
is never replaced with different content.
"""
import argparse
import hashlib
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "central"))
from sqlalchemy.orm import Session # noqa: E402
from app import release_storage # noqa: E402
from app.db import engine # noqa: E402
from app.models import AgentRelease # noqa: E402
def publish(session, artifact_path: Path, *, platform="windows", architecture="x64", notes=None, created_by=None, version=None, filename=None):
data = artifact_path.read_bytes()
digest = hashlib.sha256(data).hexdigest()
version = version or _version_from_artifact_name(artifact_path)
filename = filename or _release_filename(artifact_path, version, platform, architecture)
existing = session.query(AgentRelease).filter_by(version=version, platform=platform, architecture=architecture).one_or_none()
if existing:
if existing.sha256 == digest and existing.filename == filename:
return existing, False # identical content already published: idempotent no-op
raise SystemExit(f"refused: {version}/{platform}/{architecture} is already published "
f"(sha256 {existing.sha256[:12]}..., filename {existing.filename!r}) — the release is immutable; "
f"bump the version instead of overwriting it")
dest = release_storage.storage_root() / filename
dest.write_bytes(data)
release = AgentRelease(version=version, platform=platform, architecture=architecture, filename=filename,
size_bytes=len(data), sha256=digest, storage_path=filename, status="PUBLISHED",
published_at=datetime.now(timezone.utc),
release_notes=notes, created_by=created_by)
session.add(release)
session.commit()
return release, True
def _release_filename(artifact_path: Path, version: str, platform: str, architecture: str) -> str:
"""Filename used for the stored artifact. Installer EXEs keep their own
Setup-versioned name; ZIPs keep the legacy GTIFiscalAgent-<v>-<p>-<a>.zip."""
if artifact_path.name.lower().endswith(".exe"):
return artifact_path.name if artifact_path.name.lower() == f"gtifiscalagent-setup-{version}.exe" else release_storage.setup_exe_filename(version)
return release_storage.release_filename(version, platform, architecture)
def _version_from_artifact_name(artifact_path: Path) -> str:
# GTIFiscalAgent-<version>.zip / GTIFiscalAgent-Setup-<version>.exe
stem = artifact_path.stem
for prefix in ("GTIFiscalAgent-Setup-", "GTIFiscalAgent-"):
if stem.startswith(prefix):
return stem[len(prefix):]
raise SystemExit(f"cannot infer version from filename {artifact_path.name!r} — pass --version explicitly")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--artifact", required=True, type=Path, help="Path to the built package zip or installer exe")
parser.add_argument("--version", default=None, help="Override the version instead of inferring it from the filename")
parser.add_argument("--platform", default="windows")
parser.add_argument("--architecture", default="x64")
parser.add_argument("--notes", default=None, help="Release notes shown in the panel")
args = parser.parse_args()
if not args.artifact.exists():
raise SystemExit(f"not found: {args.artifact}")
with Session(engine) as session:
release, created = publish(session, args.artifact, platform=args.platform, architecture=args.architecture, notes=args.notes, version=args.version)
print(f"{'published' if created else 'already published (idempotent)'}: version={release.version} "
f"platform={release.platform} architecture={release.architecture} filename={release.filename} "
f"sha256={release.sha256} size_bytes={release.size_bytes}")
if __name__ == "__main__":
main()
+61
View File
@@ -0,0 +1,61 @@
"""Administrative CLI: register a new Agent directly against Postgres.
No new HTTP endpoint is needed for this — it's a one-time, operator-run
action per installation, run from wherever DATABASE_URL is reachable (the
Hub host, or a machine with network access to Postgres). Only the token's
SHA-256 hash is ever stored; the plaintext token is printed once and must
be copied into that installation's local Agent config immediately.
Usage:
python central/scripts/register_agent.py --company "Restaurante X" --name "loja-01"
"""
import argparse
import secrets
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "central"))
from sqlalchemy.orm import Session # noqa: E402
from app.auth import token_hash # noqa: E402
from app.db import engine # noqa: E402
from app.models import Agent, Tenant # noqa: E402
def register_agent(session, company_name, agent_name, token=None):
token = token or secrets.token_urlsafe(32)
tenant = session.query(Tenant).filter_by(name=company_name).one_or_none()
if tenant is None:
tenant = Tenant(name=company_name)
session.add(tenant)
session.flush()
agent = Agent(tenant_id=tenant.id, name=agent_name, token_hash=token_hash(token), enabled=True)
session.add(agent)
session.commit()
# Read everything the caller needs now, while the session is still open —
# session.commit() expires ORM attributes by default, and callers may use
# these values after the session (and its `with` block) has closed.
return {"id": agent.id, "tenant_id": tenant.id, "enabled": agent.enabled}, token
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--company", required=True, help="Company/tenant name (created if it doesn't exist)")
parser.add_argument("--name", required=True, help="Human-readable name for this Agent installation, e.g. 'loja-01'")
args = parser.parse_args()
with Session(engine) as session:
info, token = register_agent(session, args.company, args.name)
print("Agent registered. Copy these into that installation's local .env NOW — the token is shown only once:")
print(f" AGENT_ID={info['id']}")
print(f" AGENT_TOKEN={token}")
print(f" COMPANY_ID={info['tenant_id']}")
print(" FIREBIRD_WRITES=0")
print(f"enabled={info['enabled']}")
if __name__ == "__main__":
main()
@@ -0,0 +1,112 @@
"""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))
@@ -0,0 +1,60 @@
"""Offline reprocessing of residual questions against the final canonical identity map."""
import argparse
import json
from collections import defaultdict
from pathlib import Path
from central.app.ai.kb_consolidation import canonical_product_id
from central.app.ai.workitems import build_work_items
def _erp_code(question):
import re
match = re.search(r"PRODUCT\s+(\d+)", question.get("QUESTION", ""))
return match.group(1) if match else str(question.get("DATABASE_CODE") or "")
def main(questions_path, payload_path, output_path):
questions_doc = json.loads(Path(questions_path).read_text(encoding="utf-8"))
payload = json.loads(Path(payload_path).read_text(encoding="utf-8"))
canonical = {}
fingerprints = {}
by_observation = defaultdict(list)
for product in payload.get("products", []):
cid = canonical_product_id(product)
for observation in product.get("observations", []):
key = f"{observation.get('database')}:{observation.get('erp_code')}"
canonical[key] = cid
fingerprints[key] = product.get("fingerprint") or cid
questions = [dict(question, DATABASE_CODE=_erp_code(question)) for question in questions_doc.get("questions", [])]
items = build_work_items(questions, fingerprints, canonical)
by_task = {}
for item in items:
bucket = by_task.setdefault(item.task_type, {"items": 0, "cells": 0})
bucket["items"] += 1
bucket["cells"] += item.cells_covered
report = {
"artifact": "AI_RESIDUAL_PLAN_V3",
"work_items_before": 750,
"work_items_after": len(items),
"llm_items_before": 511,
# Candidate preparation is evidence-only; without an official validated
# answer it must not reclassify a selection item as resolved.
"llm_items_after": 511,
"llm_reclassification": "UNCHANGED_PENDING_FINAL_PROVIDER_SELECTION",
"by_task_type": by_task,
"canonical_observation_keys": len(canonical),
"ai_write_allowed": False,
"semantic_decisions": 0,
}
Path(output_path).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--questions", default="docs/LOCAL_FISCAL_CONTEXT_QUESTIONS_V2.json")
parser.add_argument("--payload", default="docs/runs/ai_kb_seed_payload.json")
parser.add_argument("--output", default="docs/AI_RESIDUAL_PLAN_V3.json")
args = parser.parse_args()
print(json.dumps(main(args.questions, args.payload, args.output), indent=2, ensure_ascii=False))
+100
View File
@@ -0,0 +1,100 @@
"""NCM evaluation benchmark (section 19) — same pipeline as production, known answer hidden.
For a stratified sample of products whose NCM is valid in the ACTIVE dataset:
candidate generation (ncm_top_k) -> provider selection -> validator.
Metrics: EXACT_MATCH / WRONG_SELECTION / NONE_WHEN_KNOWN / INVALID_OUTPUT (+ recall@K),
per segment (database). With no AI credential the BaselineProvider runs: it measures the
deterministic floor of the same pipeline and costs nothing.
"""
import argparse
import json
import random
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 ncm_top_k
from central.app.ai.provider import BaselineProvider, PROMPT_VERSION, SemanticOutputError, result_from_dict
from central.app.ai.fingerprint import normalize_description
from central.app.db import SessionLocal
from central.app.models import Dataset, KbObservation, KbProduct, NcmEntry
SEGMENTS = {"CONSTRUCAO": "construcao", "MMMODAS": "moda", "RINCAO": "restaurante", "UNIAO": "mercado", "TESTESOFICIAIS": "base_testes"}
def _samples(session, per_segment=40, seed=42):
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())
rows = session.execute(select(KbProduct.normalized_description, KbObservation.database_label, KbObservation.observed_fields).join(KbObservation, KbObservation.kb_product_id == KbProduct.id)).all()
by_segment = {}
for description, database, fields in rows:
ncm = (fields or {}).get("ncm")
if ncm and ncm in valid_ncm and description:
by_segment.setdefault(SEGMENTS.get(database, database), set()).add((description, ncm))
rng = random.Random(seed)
samples = []
for segment, values in sorted(by_segment.items()):
picked = sorted(values)[: per_segment] if len(values) <= per_segment else rng.sample(sorted(values), per_segment)
samples.extend({"segment": segment, "description": description, "true_ncm": ncm} for description, ncm in picked)
return samples, dataset.sha256
def main(per_segment=40, k=10, output="docs/AI_EVALUATION_V1.json") -> dict:
session = SessionLocal()
samples, ncm_sha = _samples(session, per_segment)
provider = BaselineProvider()
metrics = {"EXACT_MATCH": 0, "WRONG_SELECTION": 0, "NONE_WHEN_KNOWN": 0, "INVALID_OUTPUT": 0, "RECALL_AT_K": 0}
per_segment = {}
for sample in samples:
candidates = ncm_top_k(session, sample["description"], k=k)
codes = [code for code, _, _ in candidates]
bucket = per_segment.setdefault(sample["segment"], {key: 0 for key in metrics} | {"TOTAL": 0})
bucket["TOTAL"] += 1
if sample["true_ncm"] in codes:
metrics["RECALL_AT_K"] += 1; bucket["RECALL_AT_K"] += 1
item = type("W", (), {"task_type": "INVALID_NCM", "dedup_key": f"eval:{sample['segment']}:{normalize_description(sample['description'])[:60]}", "payload": {"description": sample["description"]}})()
try:
result = result_from_dict(provider.select(item, candidates, dataset_versions={"ncm": ncm_sha}).to_dict())
except SemanticOutputError:
metrics["INVALID_OUTPUT"] += 1; bucket["INVALID_OUTPUT"] += 1
continue
if result.selected_candidate == sample["true_ncm"]:
metrics["EXACT_MATCH"] += 1; bucket["EXACT_MATCH"] += 1
elif result.selected_candidate == "NONE":
metrics["NONE_WHEN_KNOWN"] += 1; bucket["NONE_WHEN_KNOWN"] += 1
else:
metrics["WRONG_SELECTION"] += 1; bucket["WRONG_SELECTION"] += 1
total = len(samples)
report = {
"artifact": "AI_EVALUATION_V1",
"task": "INVALID_NCM candidate selection",
"pipeline": "candidate_generation -> provider_selection -> validator",
"provider": provider.name,
"model": provider.model,
"prompt_version": PROMPT_VERSION,
"dataset_versions": {"ncm": ncm_sha},
"k": k,
"eval_total": total,
"metrics": metrics,
"accuracy_exact_match": round(metrics["EXACT_MATCH"] / total, 4) if total else None,
"recall_at_k": round(metrics["RECALL_AT_K"] / total, 4) if total else None,
"by_segment": per_segment,
"cost_usd": 0.0,
"note": "baseline provider (deterministic, zero-cost): measures the pipeline floor; rerun with a real provider once credentials exist",
}
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
session.close()
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--per-segment", type=int, default=40)
parser.add_argument("--k", type=int, default=10)
parser.add_argument("--output", default="docs/AI_EVALUATION_V1.json")
args = parser.parse_args()
print(json.dumps(main(args.per_segment, args.k, args.output), indent=2))
@@ -0,0 +1,115 @@
"""NCM retrieval evaluation V2 (sections 1, 13).
Same sampling as V1 (seed 42, per-segment) with each sample now quality-classified:
GOLD - GTIN product or cross-tenant agreement on the same fingerprint/NCM
SILVER - single-tenant valid NCM, no independent confirmation
NOISY - commercial description with < 2 informative tokens (not pursued)
Retrieval measured separately: OFFICIAL_ONLY (hierarchical enriched corpus),
KB_ONLY (neighbor bridge), HYBRID; recall@1/5/10/20/50 + MRR, per class and segment.
"""
import argparse
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.retrieval import (EnrichedNcmCorpus, KbNeighborIndex, commercial_tokens, derive_abbreviation_lexicon,
hierarchical_candidates, hybrid_candidates, kb_neighbor_candidates)
from central.app.db import SessionLocal
from central.app.models import Dataset, KbObservation, KbProduct, NcmEntry
SEGMENTS = {"CONSTRUCAO": "construcao", "MMMODAS": "moda", "RINCAO": "restaurante", "UNIAO": "mercado", "TESTESOFICIAIS": "base_testes"}
KS = (1, 5, 10, 20, 50)
def _samples(session, per_segment=40, seed=42):
import random
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())
rows = session.execute(
select(KbProduct, KbObservation).join(KbObservation, KbObservation.kb_product_id == KbProduct.id)
).all()
per_product_tenants = {}
for product, observation in rows:
per_product_tenants.setdefault(product.id, set()).add(observation.tenant_id)
by_segment = {}
for product, observation in rows:
ncm = (observation.observed_fields or {}).get("ncm")
if ncm and ncm in valid_ncm and product.normalized_description:
by_segment.setdefault(SEGMENTS.get(observation.database_label, observation.database_label), set()).add(
(product.id, product.normalized_description, ncm, bool(product.gtin), len(per_product_tenants[product.id]) >= 2)
)
rng = random.Random(seed)
samples = []
for segment, values in sorted(by_segment.items()):
picked = sorted(values) if len(values) <= per_segment else rng.sample(sorted(values), per_segment)
for product_id, description, ncm, has_gtin, cross_tenant in picked:
quality = "NOISY" if len(commercial_tokens(description)) < 2 else ("GOLD" if (has_gtin or cross_tenant) else "SILVER")
samples.append({"segment": segment, "product_id": product_id, "description": description, "true_ncm": ncm, "quality": quality})
return samples
def _recall_mrr(runs, samples):
"""runs: {mode: [ranked_codes_per_sample]}"""
out = {}
for mode, ranked_lists in runs.items():
metrics = {f"recall_at_{k}": 0 for k in KS} | {"mrr": 0.0}
total = 0
for sample, ranked in zip(samples, ranked_lists):
codes = [row[0] for row in ranked]
total += 1
for k in KS:
if sample["true_ncm"] in codes[:k]:
metrics[f"recall_at_{k}"] += 1
if sample["true_ncm"] in codes:
metrics["mrr"] += 1.0 / (codes.index(sample["true_ncm"]) + 1)
if total:
for key in metrics:
metrics[key] = round(metrics[key] / total, 4)
out[mode] = {"total": total, **metrics}
return out
def main(per_segment=40, output="docs/NCM_RETRIEVAL_EVALUATION_V2.json") -> dict:
session = SessionLocal()
samples = _samples(session, per_segment)
corpus = EnrichedNcmCorpus.build(session)
index = KbNeighborIndex.build(session)
lexicon = derive_abbreviation_lexicon(index.corpus(), min_support=3)
runs = {"OFFICIAL_ONLY": [], "KB_ONLY": [], "HYBRID": []}
for sample in samples:
# leave-one-out: the queried product never retrieves itself from the KB
runs["OFFICIAL_ONLY"].append(hierarchical_candidates(corpus, sample["description"], k=50))
runs["KB_ONLY"].append(kb_neighbor_candidates(index, sample["description"], k=50, exclude_product_id=sample["product_id"]))
runs["HYBRID"].append(hybrid_candidates(corpus, index, sample["description"], k=50, lexicon=lexicon, exclude_product_id=sample["product_id"]))
overall = _recall_mrr(runs, samples)
by_quality = {quality: _recall_mrr({mode: [row for row, sample in zip(list_rows, samples) if sample["quality"] == quality] for mode, list_rows in runs.items()},
[sample for sample in samples if sample["quality"] == quality]) for quality in ("GOLD", "SILVER", "NOISY")}
by_segment = {segment: _recall_mrr({mode: [row for row, sample in zip(list_rows, samples) if sample["segment"] == segment] for mode, list_rows in runs.items()},
[sample for sample in samples if sample["segment"] == segment]) for segment in sorted({sample["segment"] for sample in samples})}
report = {
"artifact": "NCM_RETRIEVAL_EVALUATION_V2",
"eval_total": len(samples),
"quality_counts": {quality: sum(1 for sample in samples if sample["quality"] == quality) for quality in ("GOLD", "SILVER", "NOISY")},
"lexicon_size": len(lexicon),
"lexicon_sample": dict(sorted(lexicon.items())[:40]),
"overall": overall,
"by_quality": by_quality,
"by_segment": by_segment,
"llm_liberation_gate": {"gold_recall_at_20_min": 0.80, "gold_recall_at_50_preferred": 0.90},
"cost_usd": 0.0,
}
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
session.close()
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--per-segment", type=int, default=40)
parser.add_argument("--output", default="docs/NCM_RETRIEVAL_EVALUATION_V2.json")
args = parser.parse_args()
print(json.dumps(main(args.per_segment, args.output), indent=2))
@@ -0,0 +1,95 @@
"""Run the real evaluation plumbing without network, credentials, or Firebird writes."""
import argparse
import json
from pathlib import Path
from central.app.ai.batch_runner import run_batches
from central.app.ai.fake_provider import DeterministicFakeSemanticProvider
from central.app.ai.llm_provider import LlmEvalItem
def _items(prefix, count):
return [
LlmEvalItem(
work_item_id=f"{prefix}-{index:03d}",
description=f"Produto de teste {index}",
normalized_description=f"PRODUTO TESTE {index}",
brand="TESTE",
unit="UN",
segment="offline",
candidates=[(f"{index:08d}", "CANDIDATO OFICIAL PRINCIPAL", 1.0, 1.0, 0.0, 0),
(f"{index + 1:08d}", "CANDIDATO OFICIAL ALTERNATIVO", 0.5, 0.5, 0.0, 0)],
)
for index in range(count)
]
def _run_phase(name, items, batch_sizes):
phase = {"items": len(items), "batch_sizes": {}, "provider_calls": 0, "retries": 0, "answered": 0, "errors": 0}
for batch_size in batch_sizes:
provider = DeterministicFakeSemanticProvider()
result = run_batches(items, provider, batch_size=batch_size)
phase["batch_sizes"][str(batch_size)] = {
"provider_calls": result.provider_calls,
"retries": result.retries,
"answered": len(result.results_by_work_item_id),
"errors": len(result.errors),
}
phase["provider_calls"] += result.provider_calls
phase["retries"] += result.retries
phase["answered"] = max(phase["answered"], len(result.results_by_work_item_id))
phase["errors"] = max(phase["errors"], len(result.errors))
phase["phase"] = name
return phase
def run_offline_ai_simulation(output_dir="docs/runs/offline_ai", scenario="CORRECT", batch_sizes=(5, 10, 20), final_work_items_path=None, final_candidates_path=None):
"""Exercise GOLD -> gate -> SILVER/NOISY -> residual plumbing only."""
if scenario != "CORRECT":
raise ValueError("E2E simulation uses CORRECT only; failure scenarios belong to focused tests")
gold = _items("gold", 65)
silver = _items("silver", 118)
noisy = _items("noisy", 17)
gold_phase = _run_phase("GOLD", gold, batch_sizes)
correct = gold_phase["answered"]
selection_accuracy = correct / len(gold) if gold else 0.0
gate = selection_accuracy >= 0.95 and gold_phase["errors"] == 0
phases = [gold_phase]
if gate:
phases.extend((_run_phase("SILVER", silver, batch_sizes), _run_phase("NOISY", noisy, batch_sizes)))
final_inputs = {}
for label, path in (("work_items", final_work_items_path), ("candidates", final_candidates_path)):
if path:
source = Path(path)
if source.exists():
final_inputs[label] = {"path": str(source), "bytes": source.stat().st_size, "loaded": True}
report = {
"status": "PASS_PLUMBING" if gate else "FAIL_PLUMBING",
"scenario": scenario,
"gold": {"count": len(gold), "selection_accuracy": selection_accuracy, "gate_opened": gate},
"phases": phases,
"residual_plan": {"executed": gate, "semantic_results_accepted": False, "llm_selection_required": 511},
"external_calls": 0,
"firebird_ai_writes": 0,
"ai_write_allowed": False,
"fake_results_are_authoritative": False,
"final_inputs": final_inputs,
}
path = Path(output_dir) / "OFFLINE_AI_SIMULATION_V1.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return report
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output-dir", default="docs/runs/offline_ai")
parser.add_argument("--final-work-items")
parser.add_argument("--final-candidates")
args = parser.parse_args()
print(json.dumps(run_offline_ai_simulation(args.output_dir, final_work_items_path=args.final_work_items, final_candidates_path=args.final_candidates), indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
@@ -0,0 +1,220 @@
"""Real LLM NCM evaluation over known-answer cases (mission REAL LLM EVALUATION V1).
Phases: gold first (65 cases, gold completely hidden from the prompt); silver and noisy
run ONLY after the gold quality gate passes (selection accuracy >= 0.95 among valid
answers AND high-confidence wrong rate <= 0.02). Batching verified progressively
(5/10/20) with 1:1 id matching; results cached permanently per
(input_fingerprint, candidate_set_hash, prompt_version, model, dataset_versions) so an
identical re-execution makes ZERO new calls. AI_WRITE_ALLOWED=false; nothing here can
produce a Firebird write.
If no AI credential exists the run refuses to call anything and exits with
BLOCKED_EXTERNAL_AI_CREDENTIAL (no key is ever invented).
"""
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.eval_metrics import attribution, confidence_buckets, metrics
from central.app.ai.llm_provider import (PROMPT_VERSION_LLM, LlmAnswerError, LlmEvalItem, OpenAICompatibleProvider,
candidate_set_hash, input_fingerprint, validate_answer)
from central.app.ai.retrieval import EnrichedNcmCorpus, KbNeighborIndex, derive_abbreviation_lexicon, hybrid_candidates
from central.app.config import settings
from central.app.db import SessionLocal
from central.app.models import AiEvalCache, Dataset, NcmEntry
GATE_SELECTION_ACCURACY = 0.95
GATE_HIGH_CONF_WRONG = 0.02
BATCH_PROBE_SIZES = (20, 10, 5)
def _load_samples(session, per_segment=40):
from central.scripts.run_ncm_retrieval_eval_v2 import _samples
return _samples(session, per_segment)
def _build_items(samples, corpus, index, lexicon, top_k=20):
items = []
meta = []
for sample in samples:
ranked = hybrid_candidates(corpus, index, sample["description"], k=top_k, lexicon=lexicon, exclude_product_id=sample["product_id"])
item = LlmEvalItem(
work_item_id=f"{sample['segment']}:{sample['product_id']}",
description=sample["description"],
normalized_description=sample["description"],
brand=None, unit=None, segment=sample["segment"],
candidates=ranked,
)
items.append(item)
meta.append({**sample, "gold_in_candidates": sample["true_ncm"] in {code for code, *_ in ranked}})
return items, meta
def _cached_answers(session, items, model, ncm_sha, phase):
hits = {}
for item in items:
row = session.scalar(select(AiEvalCache).where(
AiEvalCache.input_fingerprint == input_fingerprint(item),
AiEvalCache.candidate_set_hash == candidate_set_hash(item),
AiEvalCache.prompt_version == PROMPT_VERSION_LLM,
AiEvalCache.model == model,
AiEvalCache.dataset_version_ncm == ncm_sha,
AiEvalCache.phase == phase,
))
if row:
hits[item.work_item_id] = row.answer
return hits
def _diagnose_http(status_code: int) -> str:
return {401: "credential invalid (AI_API_KEY)", 403: "permission/plan does not allow this endpoint or model", 404: "endpoint or model not found (AI_BASE_URL/AI_MODEL)", 429: "quota or rate limit"}.get(status_code, f"http {status_code}")
def _select_model(available: list, requested: str) -> tuple:
"""Prefer the operator-requested model; else a current GLM suitable for classification.
Never invents a name and never switches vendor — returns (model, source)."""
if requested in available:
return requested, "requested"
glm = [model for model in available if model.lower().startswith("glm")]
for candidate in glm:
if "flash" in candidate.lower():
return candidate, "fallback_glm_flash"
if glm:
return sorted(glm)[-1], "fallback_glm_latest"
return None, "no_glm_available"
def _preflight(provider: OpenAICompatibleProvider, requested_model: str) -> dict:
"""Cheap connectivity check (GET /models — no completion tokens). Validates auth,
base URL and model availability; diagnoses 401/403/404/429 autonomously."""
import httpx
try:
available = provider.list_models()
except httpx.HTTPStatusError as error:
return {"zai_connection": "FAIL", "diagnosis": _diagnose_http(error.response.status_code), "model_available": False}
except httpx.HTTPError as error:
return {"zai_connection": "FAIL", "diagnosis": f"transport: {type(error).__name__}", "model_available": False}
model, source = _select_model(available, requested_model)
return {"zai_connection": "PASS", "model_available": model is not None, "model": model, "model_source": source,
"available_models": available if model is None else None, "base_url": provider.base_url}
def _probe_batch_size(provider, items):
"""Try the largest batch that keeps valid JSON with 1:1 ids on a small probe slice."""
for size in BATCH_PROBE_SIZES:
probe = items[:size]
try:
provider.select_batch(probe)
return size
except (LlmAnswerError, json.JSONDecodeError):
continue
return 1
def run_phase(provider_factory, phase, samples, corpus, index, lexicon, session, ncm_sha, valid_ncm):
items, meta = _build_items(samples, corpus, index, lexicon)
cached = _cached_answers(session, items, provider_factory.model if hasattr(provider_factory, "model") else settings.ai_model, ncm_sha, phase)
todo = [item for item in items if item.work_item_id not in cached]
answers = dict(cached)
provider = None
batch_size = 0
if todo:
provider = provider_factory()
batch_size = _probe_batch_size(provider, todo)
for start in range(0, len(todo), batch_size):
for answer in provider.select_batch(todo[start:start + batch_size]):
answers[answer["work_item_id"]] = answer
rows, errors = [], []
for item, sample in zip(items, meta):
answer = answers.get(item.work_item_id)
invalid = False
if answer is None:
invalid = True
answer = {"selected_ncm": None, "confidence": 0.0, "abstain": True, "reason_short": "missing"}
else:
try:
validate_answer(answer, item)
if answer["selected_ncm"] is not None and answer["selected_ncm"] not in valid_ncm:
invalid = True
except LlmAnswerError:
invalid = True
selected = None if (invalid or answer["abstain"] or answer["selected_ncm"] is None) else answer["selected_ncm"]
rows.append({"gold_in_candidates": sample["gold_in_candidates"], "selected": selected, "gold": sample["true_ncm"], "confidence": answer["confidence"], "invalid": invalid})
if selected is not None and selected != sample["true_ncm"]:
errors.append({"work_item_id": item.work_item_id, "gold": sample["true_ncm"], "selected": selected, "confidence": answer["confidence"], "gold_in_candidates": sample["gold_in_candidates"], "reason_short": answer.get("reason_short", "")})
if invalid or answer.get("abstain") or selected is not None:
if item.work_item_id not in cached:
session.add(AiEvalCache(input_fingerprint=input_fingerprint(item), candidate_set_hash=candidate_set_hash(item), prompt_version=PROMPT_VERSION_LLM,
model=provider.model if provider else settings.ai_model, dataset_version_ncm=ncm_sha, phase=phase,
answer=answer, gold_ncm=sample["true_ncm"]))
session.commit()
return rows, errors, provider, batch_size, len(cached)
def main(output_dir="/tmp", per_segment=40) -> dict:
if not settings.ai_api_key:
print(json.dumps({"status": "BLOCKED_EXTERNAL_AI_CREDENTIAL",
"operator_action": "set AI_API_KEY (and optionally AI_BASE_URL/AI_MODEL) in the Hub environment (.env); never commit or log it"}, indent=2))
sys.exit(2)
session = SessionLocal()
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())
corpus = EnrichedNcmCorpus.build(session)
index = KbNeighborIndex.build(session)
lexicon = derive_abbreviation_lexicon(index.corpus(), min_support=3)
samples = _load_samples(session, per_segment)
# ---- preflight: cheap /models call, never spends completion tokens ----
pre_provider = OpenAICompatibleProvider(api_key=settings.ai_api_key, model=settings.ai_model, base_url=settings.ai_base_url)
preflight = _preflight(pre_provider, settings.ai_model)
if preflight["zai_connection"] != "PASS" or not preflight["model_available"]:
print(json.dumps({"status": "ZAI_PREFLIGHT_FAIL", "preflight": preflight}, indent=2))
sys.exit(3)
model_name = preflight["model"]
factory = lambda: OpenAICompatibleProvider(api_key=settings.ai_api_key, model=model_name, base_url=settings.ai_base_url)
gold = [sample for sample in samples if sample["quality"] == "GOLD"]
provider = factory()
rows, errors, provider_used, batch_size, cache_hits = run_phase(lambda: provider, "gold", gold, corpus, index, lexicon, session, dataset.sha256, valid_ncm)
m = metrics(rows)
prices_configured = settings.ai_price_in_per_m is not None and settings.ai_price_out_per_m is not None
usage = provider.usage if provider is not None else {"input_tokens": 0, "output_tokens": 0, "cached_tokens": 0}
cost_total = round(usage["input_tokens"] / 1e6 * settings.ai_price_in_per_m + usage["output_tokens"] / 1e6 * settings.ai_price_out_per_m, 6) if prices_configured else "UNKNOWN"
report = {
"artifact": "REAL_LLM_NCM_EVALUATION_V1", "phase": "gold",
"provider": "z.ai", "base_url": settings.ai_base_url, "model": model_name, "prompt_version": PROMPT_VERSION_LLM,
"preflight": preflight,
"metrics": m, "attribution": attribution(rows), "confidence_buckets": confidence_buckets(rows),
"batch_size_selected": batch_size,
"gate": {"selection_accuracy_min": GATE_SELECTION_ACCURACY, "high_conf_wrong_max": GATE_HIGH_CONF_WRONG,
"passed": (m["SELECTION_ACCURACY_WHEN_GOLD_AVAILABLE"] or 0) >= GATE_SELECTION_ACCURACY and (m["HIGH_CONFIDENCE_WRONG_RATE"] or 0) <= GATE_HIGH_CONF_WRONG},
"cache_hits_first_run": cache_hits, "ai_write_allowed": False,
"usage": {**usage, "http_calls": provider.http_calls, "retries": provider.retries,
"average_items_per_call": round(len(rows) / max(provider.http_calls, 1), 2),
"tokens_per_item": round((usage["input_tokens"] + usage["output_tokens"]) / max(len(rows), 1), 1),
"cost_total": cost_total, "cost_per_item": (round(cost_total / max(len(rows), 1), 6) if prices_configured else "UNKNOWN"),
"prices_configured": prices_configured},
}
Path(f"{output_dir}/REAL_LLM_NCM_EVALUATION_V1.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
Path(f"{output_dir}/REAL_LLM_NCM_ERRORS_V1.json").write_text(json.dumps(errors, indent=2, ensure_ascii=False), encoding="utf-8")
# cache proof: identical re-execution must make zero new calls
provider2 = factory()
rows2, _, _, _, cache_hits2 = run_phase(lambda: provider2, "gold", gold, corpus, index, lexicon, session, dataset.sha256, valid_ncm)
second_calls = provider2.http_calls
report["cache_proof"] = {"second_run_calls": second_calls, "second_run_cache_hits": cache_hits2,
"cache_hit_reexecution": second_calls == 0, "identical_metrics": metrics(rows2)["CORRECT"] == m["CORRECT"]}
Path(f"{output_dir}/REAL_LLM_NCM_EVALUATION_V1.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
Path(f"{output_dir}/REAL_LLM_COST_V1.json").write_text(json.dumps({"artifact": "REAL_LLM_COST_V1", "usage": report["usage"], "previous_estimate_usd": 0.44, "cache_proof": report["cache_proof"]}, indent=2), encoding="utf-8")
session.close()
return report
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--output-dir", default="/tmp")
parser.add_argument("--per-segment", type=int, default=40)
args = parser.parse_args()
print(json.dumps(main(args.output_dir, args.per_segment), indent=2))
@@ -0,0 +1,37 @@
"""Compare V3 retrieval policy with the recorded V2 holdout without retuning on GOLD rows."""
import argparse
import json
from pathlib import Path
def main(v2_path="docs/NCM_RETRIEVAL_EVALUATION_V2.json", output="docs/NCM_RETRIEVAL_EVALUATION_V3.json"):
baseline = json.loads(Path(v2_path).read_text(encoding="utf-8"))
gold = baseline.get("by_quality", {}).get("GOLD", {}).get("HYBRID", {})
# V3 is deliberately not activated from a synthetic or incomplete replay.
# Its optional evidence layer is covered by unit tests and retains V2 ordering
# when no new evidence is supplied.
report = {
"artifact": "NCM_RETRIEVAL_EVALUATION_V3",
"baseline_artifact": "NCM_RETRIEVAL_EVALUATION_V2",
"activation": "NOT_ACTIVATED_PENDING_FULL_HOLDOUT_REPLAY",
"gold_recall_at_1_before": gold.get("recall_at_1"),
"gold_recall_at_20_before": gold.get("recall_at_20"),
"gold_mrr_before": gold.get("mrr"),
"gold_recall_at_1_after": gold.get("recall_at_1"),
"gold_recall_at_20_after": gold.get("recall_at_20"),
"gold_mrr_after": gold.get("mrr"),
"local_embeddings_used": False,
"local_embeddings_improvement": None,
"evidence_features": ["BM25-compatible lexical baseline", "word/character evidence hooks", "token rarity", "hierarchy", "KB neighbors", "GTIN", "product group"],
"policy": "retain V2 until a complete untouched holdout replay proves recall_at_20 >= 0.954 and no regression",
}
Path(output).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--v2", default="docs/NCM_RETRIEVAL_EVALUATION_V2.json")
parser.add_argument("--output", default="docs/NCM_RETRIEVAL_EVALUATION_V3.json")
args = parser.parse_args()
print(json.dumps(main(args.v2, args.output), indent=2, ensure_ascii=False))
+158
View File
@@ -0,0 +1,158 @@
"""Seed the Product Knowledge Base deterministically from the 5 Firebird databases.
Two phases (Firebird is only reachable from Windows; Postgres only inside the compose network):
export (Windows, READ-ONLY SELECT against Firebird):
python scripts/seed_product_kb.py export --output docs/runs/ai_kb_seed_payload.json
load (inside the fiscal-hub container, against Postgres):
python -m central.scripts.seed_product_kb load --input docs/runs/ai_kb_seed_payload.json
Export computes, per physical product row: the stable global fingerprint
(description|unit; GTIN validated by checksum), and emits observations. Load upserts the
GLOBAL product, records per-tenant observations, seeds knowledge already proven by the
gate (validated_ncm when the declared NCM is active-official; ERP-declared values stay
ERP_OBSERVATION evidence), and links TENANT_PRODUCT_CONTEXT rows. Tenant context is
never promoted to global knowledge.
"""
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
DATABASES = [
("TESTESOFICIAIS", None), # path from FIREBIRD_DATABASE_PATH
("CONSTRUCAO", "Banco/CONSTRUCAO.FDB"),
("MMMODAS", "Banco/MMMODAS.FDB"),
("RINCAO", "Banco/RINCAO.FDB"),
("UNIAO", "Banco/UNIAO.FDB"),
]
QUERY = "SELECT p.CODIGO, p.PRODUTO, p.UNIDADE, p.CODBARRA, p.GTIN, p.CLASSIFICACAO_FISCAL, p.CEST FROM C000025 p"
def _canonical_ncm(value) -> str:
digits = "".join(char for char in str(value or "") if char.isdigit())
return digits if len(digits) == 8 else ""
def export_payload(limit_per_db: int = 0, output: str = "docs/runs/ai_kb_seed_payload.json") -> dict:
import fdb
from dotenv import dotenv_values
from central.app.ai.fingerprint import normalize_description, product_fingerprint, validate_gtin
env = dotenv_values(".env")
stats = {"databases": [], "rows_read": 0, "firebird_writes": 0}
products = {}
for label, relative in DATABASES:
path = env["FIREBIRD_DATABASE_PATH"] if relative is None else str(Path(relative).resolve())
connection = fdb.connect(database=path, user=env["FB_USER"], password=env["FB_PASSWORD"], fb_library_name=env.get("FIREBIRD_CLIENT_LIBRARY"))
try:
cursor = connection.cursor(); cursor.execute(QUERY)
columns = [column[0].strip().upper() for column in cursor.description]
rows = cursor.fetchmany(limit_per_db) if limit_per_db else cursor.fetchall()
finally:
connection.close()
# collapse the physical duplicate CODIGO rows into observations of the same product
seen_codes = set()
for row in rows:
record = dict(zip(columns, row))
code = str(record.get("CODIGO") or "").strip()
dedupe = f"{label}:{code}"
if dedupe in seen_codes: # duplicate physical key (e.g. MMMODAS 017421) -> one observation
continue
seen_codes.add(dedupe)
unit = normalize_description(record.get("UNIDADE")) or None
fingerprint = product_fingerprint(record.get("PRODUTO"), None, unit)
gtin = validate_gtin(record.get("GTIN")) or validate_gtin(record.get("CODBARRA"))
entry = products.setdefault(fingerprint, {
"fingerprint": fingerprint,
"gtin": gtin,
"normalized_description": normalize_description(record.get("PRODUTO")),
"unit": unit,
"ncm": _canonical_ncm(record.get("CLASSIFICACAO_FISCAL")),
"cest": str(record.get("CEST") or "").strip() or None,
"observations": [],
})
if gtin and not entry["gtin"]:
entry["gtin"] = gtin
entry["observations"].append({"database": label, "company_key": label, "erp_code": code})
stats["databases"].append({"database": label, "rows": len(rows)})
stats["rows_read"] += len(rows)
payload = {"artifact": "AI_KB_SEED_PAYLOAD", "exported_at": datetime.now(timezone.utc).isoformat(), "stats": stats, "products": list(products.values())}
Path(output).parent.mkdir(parents=True, exist_ok=True)
Path(output).write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
stats["unique_products"] = len(products)
stats["payload_path"] = output
return stats
def load_payload(input_path: str) -> dict:
from sqlalchemy import select
from central.app.ai.kb import KnowledgeBase
from central.app.db import SessionLocal
from central.app.models import Dataset, NcmEntry, Tenant, TenantProductContext
payload = json.loads(Path(input_path).read_text(encoding="utf8"))
session = SessionLocal()
kb = KnowledgeBase(session)
dataset = session.scalar(select(Dataset).where(Dataset.kind == "NCM", Dataset.status == "ACTIVE"))
active_ncm = set(session.scalars(select(NcmEntry.code).where(NcmEntry.dataset_id == dataset.id)).all()) if dataset else set()
tenant_ids = {}
stats = {"products": 0, "observations": 0, "validated_ncm": 0, "erp_ncm_observations": 0, "erp_cest_observations": 0, "tenant_contexts": 0, "firebird_writes": 0}
for product in payload["products"]:
row = kb.upsert_product(fingerprint=product["fingerprint"], gtin=product.get("gtin"), normalized_description=product.get("normalized_description", ""), unit=product.get("unit"))
stats["products"] += 1
for observation in product["observations"]:
label = observation["database"]
if label not in tenant_ids:
tenant = session.scalar(select(Tenant).where(Tenant.name == label))
if tenant is None:
tenant = Tenant(name=label); session.add(tenant); session.flush()
tenant_ids[label] = tenant.id
kb.record_observation(row.id, tenant_id=tenant_ids[label], company_key=label, erp_code=observation["erp_code"], database_label=label, observed_fields={"ncm": product.get("ncm") or None, "cest": product.get("cest")})
stats["observations"] += 1
existing = session.scalar(select(TenantProductContext).where(TenantProductContext.tenant_id == tenant_ids[label], TenantProductContext.company_key == label, TenantProductContext.erp_code == observation["erp_code"]))
if existing is None:
session.add(TenantProductContext(tenant_id=tenant_ids[label], company_key=label, erp_code=observation["erp_code"], kb_product_id=row.id))
stats["tenant_contexts"] += 1
ncm = product.get("ncm") or ""
if ncm and ncm in active_ncm:
kb.add_knowledge(row.id, "validated_ncm", ncm, source="OFFICIAL_DATASET", confidence=1.0, dataset_versions={"ncm": dataset.sha256})
stats["validated_ncm"] += 1
elif ncm:
kb.add_knowledge(row.id, "candidate_ncm", ncm, source="ERP_OBSERVATION", confidence=0.3, evidence={"declared_in": [observation["database"] for observation in product["observations"]]})
stats["erp_ncm_observations"] += 1
if product.get("cest"):
kb.add_knowledge(row.id, "candidate_cest", product["cest"], source="ERP_OBSERVATION", confidence=0.3, evidence={"declared_in": [observation["database"] for observation in product["observations"]]})
stats["erp_cest_observations"] += 1
if stats["products"] % 2000 == 0:
session.commit()
session.commit()
stats.update(kb.stats())
session.close()
return stats
def main(argv=None):
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="phase", required=True)
export_parser = sub.add_parser("export")
export_parser.add_argument("--limit", type=int, default=0)
export_parser.add_argument("--output", default="docs/runs/ai_kb_seed_payload.json")
load_parser = sub.add_parser("load")
load_parser.add_argument("--input", default="docs/runs/ai_kb_seed_payload.json")
load_parser.add_argument("--output", default="docs/runs/ai_kb_seed.json")
args = parser.parse_args(argv)
if args.phase == "export":
result = export_payload(args.limit, args.output)
else:
result = load_payload(args.input)
result["loaded_at"] = datetime.now(timezone.utc).isoformat()
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
Path(args.output).write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
@@ -0,0 +1,39 @@
"""Administrative CLI: pull the client registry from GTI Gerenciador (the
panel) and mirror it into Tenant rows (source='GTI_GERENCIADOR'). GTI
Gerenciador is the single source of truth; the Fiscal Hub never gets MySQL
credentials — this only calls the panel's read-only internal endpoint.
Usage (from inside the Hub container, or anywhere DATABASE_URL +
GERENCIADOR_INTERNAL_URL + FISCAL_INTERNAL_API_TOKEN are reachable):
python central/scripts/sync_clients_from_gerenciador.py
Prints a reconciliation summary; never prints the bearer token.
"""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "central"))
from sqlalchemy.orm import Session # noqa: E402
from app import client_sync # noqa: E402
from app.db import engine # noqa: E402
def main():
clients = client_sync.fetch_from_gerenciador()
with Session(engine) as session:
result = client_sync.sync_from_gerenciador(session, clients)
session.commit()
print(f"CLIENT_SYNC=ok fetched={result.fetched} created={result.created} updated={result.updated} unchanged={result.unchanged}")
print(f"TOTAL_CLIENTS_GERENCIADOR={result.fetched} ACTIVE_CLIENTS_GERENCIADOR={result.active} INACTIVE_CLIENTS_GERENCIADOR={result.inactive}")
if __name__ == "__main__":
try:
main()
except client_sync.ClientSyncUnavailable as error:
print(f"CLIENT_SYNC=unavailable reason={error}")
raise SystemExit(1)
@@ -0,0 +1,47 @@
"""Post-deploy proof through the PUBLIC panel: the default release is served as the real installer EXE (not a zip)
with the published sha256. Creates a throwaway LOCAL admin + an E2E installation, revokes/deactivates both after
(never deleted: audit FKs). Run inside the Hub container: python central/scripts/verify_release_download.py <version>"""
import hashlib
import sys
import time
import httpx
from sqlalchemy.orm import Session
sys.path.insert(0, "central")
from app.db import engine # noqa: E402
from app.human_auth import hash_password # noqa: E402
from app.models import AdminUser, AgentRelease, Tenant # noqa: E402
BASE = "https://gtifiscal.gtigrupo.com.br"
PASSWORD = "Verify-tmp-2026!x"
want = sys.argv[1]
user = f"e2e-verify-{int(time.time())}"
with Session(engine) as db:
rel = db.query(AgentRelease).filter_by(version=want, platform="windows", architecture="x64").one()
default = db.query(AgentRelease).filter_by(status="PUBLISHED").order_by(AgentRelease.created_at.desc()).first()
assert rel.status == "PUBLISHED" and default.id == rel.id, f"default release is {default.version}, expected {want}"
tenant = db.query(Tenant).filter(Tenant.source == "GTI_GERENCIADOR", Tenant.active.is_(True), Tenant.external_client_id.isnot(None)).order_by(Tenant.id).first()
assert tenant, "no active Gerenciador-mirrored client to attach the throwaway installation to"
db.add(AdminUser(username=user, password_hash=hash_password(PASSWORD), is_active=True, auth_source="LOCAL")); db.flush()
client_id, release_id, sha, name = tenant.external_client_id, rel.id, rel.sha256, rel.filename
db.commit()
try:
c = httpx.Client(follow_redirects=False, timeout=120)
assert c.post(f"{BASE}/login", data={"username": user, "password": PASSWORD}).status_code == 303
csrf = c.cookies.get("gti_csrf")
r = c.post(f"{BASE}/agents/new", data={"csrf_token": csrf, "client_id": client_id, "display_name": "E2E-VERIFY Servidor", "release_id": release_id})
assert r.status_code == 303, r.status_code
uuid = r.headers["location"].rsplit("/", 1)[-1]
d = c.get(f"{BASE}/agents/pending/{uuid}/download")
disp = d.headers.get("content-disposition", "")
assert d.status_code == 200 and name in disp and name.endswith(".exe"), (d.status_code, disp)
assert d.content[:2] == b"MZ", "not a Windows executable"
got = hashlib.sha256(d.content).hexdigest()
assert got == sha, "sha256 mismatch"
print(f"DOWNLOAD_IS_EXE=true filename={name} SHA256_MATCH=true size={len(d.content)}")
c.post(f"{BASE}/agents/pending/{uuid}/revoke", data={"csrf_token": csrf})
finally:
with Session(engine) as db:
db.query(AdminUser).filter_by(username=user).update({"is_active": False}); db.commit()
print("VERIFY_DOWNLOAD=PASS")