feat: product enrichment foundation (GTIN, KB evidence, providers, quota gateway, resolver, migration 0029)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-25 18:35:42 -03:00
co-authored by Claude Sonnet 5
parent 4a12640031
commit 27f992d9e0
10 changed files with 1360 additions and 0 deletions
@@ -0,0 +1,140 @@
"""Per-installation functions (corrector / enrichment mode), KB field status, enrichment state + audit, provider state/usage/cache, official source refresh log.
Revision ID: 0029_product_enrichment
Revises: 0028_rtc_write_modes
"""
from alembic import op
import sqlalchemy as sa
revision = "0029_product_enrichment"
down_revision = "0028_rtc_write_modes"
branch_labels = None
depends_on = None
def _idx(table, *cols):
for col in cols:
op.create_index(f"ix_{table}_{col}", table, [col])
def upgrade():
# existing installations keep behaving exactly as before: corrector on, functions already configured
op.add_column("installations", sa.Column("corrector_enabled", sa.Boolean(), nullable=False, server_default=sa.text("true")))
op.add_column("installations", sa.Column("enrichment_mode", sa.String(16), nullable=False, server_default="DISABLED"))
op.add_column("installations", sa.Column("features_configured", sa.Boolean(), nullable=False, server_default=sa.text("true")))
op.add_column("agent_enrollments", sa.Column("features_selected", sa.Boolean(), nullable=False, server_default=sa.text("false")))
op.add_column("agent_enrollments", sa.Column("feature_corrector", sa.Boolean(), nullable=False, server_default=sa.text("false")))
op.add_column("agent_enrollments", sa.Column("feature_enrichment_mode", sa.String(16), nullable=False, server_default="DISABLED"))
op.add_column("kb_knowledge", sa.Column("status", sa.String(16), nullable=False, server_default="EVIDENCE_ONLY"))
op.create_index("ix_kb_knowledge_status", "kb_knowledge", ["status"])
op.create_table(
"product_enrichments",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("tenant_id", sa.Integer(), sa.ForeignKey("tenants.id"), nullable=False),
sa.Column("installation_id", sa.Integer(), sa.ForeignKey("installations.id"), nullable=False),
sa.Column("product_code", sa.String(64), nullable=False),
sa.Column("gtin", sa.String(16), nullable=True),
sa.Column("description", sa.String(255), nullable=True),
sa.Column("status", sa.String(32), nullable=False, server_default="PENDING"),
sa.Column("decisions", sa.JSON(), nullable=False),
sa.Column("fields_applied", sa.JSON(), nullable=False),
sa.Column("human_overridden", sa.JSON(), nullable=False),
sa.Column("attempts", sa.Integer(), nullable=False, server_default="0"),
sa.Column("next_attempt_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("latency_ms", sa.Integer(), nullable=True),
sa.Column("detected_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.UniqueConstraint("installation_id", "product_code", name="uq_product_enrichment"),
)
_idx("product_enrichments", "tenant_id", "installation_id", "gtin", "status", "next_attempt_at")
op.create_table(
"product_field_audits",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("tenant_id", sa.Integer(), sa.ForeignKey("tenants.id"), nullable=False),
sa.Column("installation_id", sa.Integer(), sa.ForeignKey("installations.id"), nullable=False),
sa.Column("agent_id", sa.Integer(), sa.ForeignKey("agents.id"), nullable=False),
sa.Column("product_code", sa.String(64), nullable=False),
sa.Column("gtin", sa.String(16), nullable=True),
sa.Column("description_snapshot", sa.String(255), nullable=True),
sa.Column("field", sa.String(48), nullable=False),
sa.Column("old_value", sa.String(64), nullable=True),
sa.Column("new_value", sa.String(64), nullable=True),
sa.Column("rule", sa.String(64), nullable=True),
sa.Column("kb_status", sa.String(16), nullable=True),
sa.Column("evidence", sa.JSON(), nullable=False),
sa.Column("dataset_versions", sa.JSON(), nullable=False),
sa.Column("authorization_origin", sa.String(48), nullable=True),
sa.Column("job_id", sa.Integer(), nullable=True),
sa.Column("outcome", sa.String(24), nullable=False, server_default="APPLIED"),
sa.Column("reason", sa.String(96), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
_idx("product_field_audits", "tenant_id", "installation_id", "agent_id", "product_code")
op.create_table(
"provider_states",
sa.Column("provider", sa.String(32), primary_key=True),
sa.Column("configured", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("status", sa.String(16), nullable=False, server_default="NOT_CONFIGURED"),
sa.Column("quota_kind", sa.String(16), nullable=False, server_default="DOCUMENTED_RATE"),
sa.Column("daily_limit", sa.Integer(), nullable=True),
sa.Column("remaining", sa.Integer(), nullable=True),
sa.Column("reset_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("rate_per_minute", sa.Integer(), nullable=True),
sa.Column("circuit_open_until", sa.DateTime(timezone=True), nullable=True),
sa.Column("consecutive_failures", sa.Integer(), nullable=False, server_default="0"),
sa.Column("last_request_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_success_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_error", sa.String(160), nullable=True),
sa.Column("last_error_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_latency_ms", sa.Integer(), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_table(
"provider_usage_daily",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("provider", sa.String(32), nullable=False),
sa.Column("day", sa.String(10), nullable=False),
*[sa.Column(name, sa.Integer(), nullable=False, server_default="0") for name in ("requests", "success", "not_found", "errors", "rate_limited", "timeouts", "cache_hits", "cache_misses")],
sa.UniqueConstraint("provider", "day", name="uq_provider_usage_day"),
)
_idx("provider_usage_daily", "provider", "day")
op.create_table(
"gtin_lookup_cache",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("gtin", sa.String(16), nullable=False),
sa.Column("provider", sa.String(32), nullable=False),
sa.Column("found", sa.Boolean(), nullable=False),
sa.Column("data", sa.JSON(), nullable=False),
sa.Column("fetched_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("gtin", "provider", name="uq_gtin_lookup_cache"),
)
_idx("gtin_lookup_cache", "gtin", "expires_at")
op.create_table(
"source_refresh_runs",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("source", sa.String(32), nullable=False),
sa.Column("source_url", sa.Text(), nullable=False),
sa.Column("started_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("status", sa.String(16), nullable=False, server_default="RUNNING"),
sa.Column("sha256", sa.String(64), nullable=True),
sa.Column("version_label", sa.String(64), nullable=True),
sa.Column("record_count", sa.Integer(), nullable=True),
sa.Column("error", sa.String(300), nullable=True),
)
_idx("source_refresh_runs", "source")
def downgrade():
for table in ("source_refresh_runs", "gtin_lookup_cache", "provider_usage_daily", "provider_states", "product_field_audits", "product_enrichments"):
op.drop_table(table)
op.drop_index("ix_kb_knowledge_status", table_name="kb_knowledge")
op.drop_column("kb_knowledge", "status")
for name in ("feature_enrichment_mode", "feature_corrector", "features_selected"):
op.drop_column("agent_enrollments", name)
for name in ("features_configured", "enrichment_mode", "corrector_enabled"):
op.drop_column("installations", name)