feat: TECHNICAL_PLACEHOLDER (NCM 00000000) is replaceable by the product enrichment; real values preserved; Agent 1.3.4

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-26 10:50:12 -03:00
co-authored by Claude Sonnet 5
parent 5281c1d79b
commit c68c6c67ed
26 changed files with 290 additions and 31 deletions
+23 -5
View File
@@ -21,7 +21,10 @@ from datetime import datetime, timezone
logger = logging.getLogger("gti_fiscal_agent.enrichment")
CAPABILITY = "PRODUCT_ENRICHMENT_WRITE_V1"
ALLOWED_FIELDS = ("CLASSIFICACAO_FISCAL", "CEST", "IPI") # explicit allowlist; anything else raises before touching SQL
ALLOWED_FIELDS = ("CLASSIFICACAO_FISCAL", "CEST", "IPI")
# TECHNICAL_PLACEHOLDER: the SGV refuses a product without NCM, so operators type a filler. It means "not classified yet" (not a human decision) and may be replaced.
# Explicit per-field allowlist, identical to central/app/enrichment/placeholders.py (parity test). Nothing else is ever overwritten.
PLACEHOLDERS = {"CLASSIFICACAO_FISCAL": frozenset({"00000000"})} # explicit allowlist; anything else raises before touching SQL
READ_FIELDS = ("CODIGO", "PRODUTO", "GTIN", "CODBARRA") + ALLOWED_FIELDS # the SGV operator types the EAN in CODBARRA; GTIN (fiscal) is mostly empty
POLL_SECONDS = 2.0
SAFETY_DIFF_SECONDS = 60.0
@@ -108,6 +111,16 @@ def is_human_overridden(store_path, code, field) -> bool:
# ---- Firebird access (injected: `read_conn()` returns a READ-ONLY connection, `write_conn()` a write-capable one, only ever opened by ENABLED mode) ----------------------------------------
def is_placeholder(field, value) -> bool:
text = _text(value)
text = "".join(ch for ch in text if ch.isdigit()) if field == "CLASSIFICACAO_FISCAL" else text
return text in PLACEHOLDERS.get(field, frozenset())
def writable_now(field, value) -> bool:
return not _text(value) or is_placeholder(field, value)
def _digits(value) -> str:
return "".join(ch for ch in _text(value) if ch.isdigit())
@@ -174,10 +187,14 @@ def write_field(conn, code: str, field: str, value) -> dict:
if len(rows) != 1:
conn.rollback()
return {"outcome": "ABORTED", "reason": "PRODUCT_ROW_NOT_UNIQUE" if rows else "PRODUCT_NOT_FOUND"}
if _text(rows[0][0]):
raw = rows[0][0]
if not writable_now(field, raw):
conn.rollback()
return {"outcome": "ABORTED", "reason": "VALUE_ALREADY_SET_NEVER_OVERWRITTEN"}
cur.execute(f"UPDATE C000025 SET {field} = ? WHERE CODIGO = ? AND ({field} IS NULL OR TRIM({field}) = '')", (value, code))
if _text(raw): # technical placeholder: compare-and-swap on the exact value just read
cur.execute(f"UPDATE C000025 SET {field} = ? WHERE CODIGO = ? AND TRIM({field}) = ?", (value, code, _text(raw)))
else:
cur.execute(f"UPDATE C000025 SET {field} = ? WHERE CODIGO = ? AND ({field} IS NULL OR TRIM({field}) = '')", (value, code))
if cur.rowcount != 1:
conn.rollback()
return {"outcome": "ABORTED", "reason": "UPDATE_ROWCOUNT_NOT_ONE"}
@@ -189,7 +206,7 @@ def write_field(conn, code: str, field: str, value) -> dict:
same = (float(after) == value) if field == "IPI" and after is not None else _text(after) == value
if not same:
return {"outcome": "FAILED", "reason": "POST_WRITE_VERIFICATION_FAILED"}
return {"outcome": "APPLIED", "value": value, "rowcount": 1}
return {"outcome": "APPLIED", "value": value, "rowcount": 1, "replaced_placeholder": _text(raw) if _text(raw) else None}
except Exception as error: # noqa: BLE001
try:
conn.rollback()
@@ -345,7 +362,8 @@ class EnrichmentWorker:
for field in ALLOWED_FIELDS:
decision = fields.get(field) or {}
status, value = decision.get("status"), decision.get("value")
base = {"field": field, "rule": decision.get("rule"), "kb_status": decision.get("kb_status"), "evidence": decision.get("evidence") or [], "old": None, "new": value}
placeholder = _text(current.get(field)) if is_placeholder(field, current.get(field)) else None
base = {"field": field, "rule": decision.get("rule"), "kb_status": decision.get("kb_status"), "evidence": decision.get("evidence") or [], "old": placeholder, "new": value}
if status == "HUMAN_OVERRIDDEN" or overridden.get(field):
reports.append({**base, "outcome": "HUMAN_OVERRIDDEN", "reason": "HUMAN_OVERRIDDEN", "new": None})
continue
+1 -1
View File
@@ -9,7 +9,7 @@ import os
import sqlite3
from pathlib import Path
AGENT_VERSION_DEFAULT = "1.3.3"
AGENT_VERSION_DEFAULT = "1.3.4"
REQUIRED_ENV = ("GTI_HUB_URL", "AGENT_TOKEN", "FIREBIRD_DATABASE_PATH", "FB_USER", "FB_PASSWORD")
REQUIRED_CAPABILITIES = ("supports_ncm", "supports_ipi", "supports_cest")
FISCAL_MODES = ("DRY_RUN", "AUTO_APPLY")