chore: capture semantic pipeline baseline

This commit is contained in:
OpenCode
2026-09-21 15:52:17 -03:00
parent 9dcc263217
commit 6731eeca08
47 changed files with 3724 additions and 15 deletions
+171
View File
@@ -0,0 +1,171 @@
"""AI semantic layer V1: fingerprint, KB, dedup, provider contract, budget guard, candidates."""
import json
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from central.app.models import Base, Dataset, NcmEntry, CestEntry, CestNcmEntry, TipiEntry
def _engine():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return engine
def _ncm_dataset(session, entries):
dataset = Dataset(kind="NCM", provider="Siscomex", sha256="a" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=len(entries))
session.add(dataset); session.flush()
for code, description in entries:
session.add(NcmEntry(dataset_id=dataset.id, code=code, description=description, valid_from="", valid_to=""))
session.flush()
return dataset
# ---------------- fingerprint / identity ----------------
def test_fingerprint_is_stable_and_ignores_unstable_context():
from central.app.ai.fingerprint import normalize_description, product_fingerprint, validate_gtin
a = product_fingerprint("Coca-Cola 2LT", "COCA-COLA", "UN", "2")
b = product_fingerprint("coca cola 2lt ", "coca-cola", "un", "2.0")
assert a == b
# fiscal/company context never participates in global identity
c = product_fingerprint("Coca-Cola 2LT", "COCA-COLA", "UN", "2")
assert a == c
assert normalize_description("Água Mineral 500ml") == "AGUA MINERAL 500ML"
assert validate_gtin("7891234567895") == "7891234567895" # valid GTIN-13
assert validate_gtin("7891234567890") is None # bad checksum
assert validate_gtin("123") is None
# ---------------- knowledge base ----------------
def test_kb_exact_fingerprint_hit_and_conflict_without_majority():
from central.app.ai.kb import KnowledgeBase, EXACT, FINGERPRINT
engine = _engine()
with Session(engine) as session:
kb = KnowledgeBase(session)
p1 = kb.upsert_product(fingerprint="f1", gtin="7891234567895", normalized_description="AGUA MINERAL 500ML")
# same GTIN arrives with a different fingerprint -> same product, exact hit
p2, hit = kb.resolve(gtin="7891234567895", fingerprint="other")
assert p2.id == p1.id and hit == EXACT
# same fingerprint, no GTIN -> fingerprint hit
p3, hit = kb.resolve(gtin=None, fingerprint="f2")
assert p3 is None and hit is None
p4 = kb.upsert_product(fingerprint="f2", normalized_description="OUTRO")
_, hit = kb.resolve(gtin=None, fingerprint="f2")
assert hit == FINGERPRINT
kb.add_knowledge(p1.id, "candidate_ncm", "22011000", source="OFFICIAL_DATASET", confidence=1.0)
kb.add_knowledge(p1.id, "candidate_ncm", "22019000", source="OFFICIAL_DATASET", confidence=1.0)
current = kb.current_knowledge(p1.id)
assert current["candidate_ncm"]["conflict"] is True # no majority auto-pick
assert current["candidate_ncm"]["values"] == {"22011000", "22019000"}
stats = kb.stats()
assert stats["products"] == 2 and stats["gtin_products"] == 1 and stats["conflicts"] == 1
def test_kb_aggregates_observations_across_tenants():
from central.app.ai.kb import KnowledgeBase
engine = _engine()
with Session(engine) as session:
kb = KnowledgeBase(session)
p = kb.upsert_product(fingerprint="f1", normalized_description="REFRIGERANTE 2L")
kb.record_observation(p.id, tenant_id=1, company_key="MMMODAS", erp_code="000001", database_label="MMMODAS", observed_fields={"ncm": "22021000"})
kb.record_observation(p.id, tenant_id=2, company_key="UNIAO", erp_code="000009", database_label="UNIAO", observed_fields={"ncm": "22021000"})
agg = kb.observation_summary(p.id)
assert agg["observation_count"] == 2 and agg["tenant_count"] == 2
assert agg["agreement"]["ncm"] == {"22021000": 2} and agg["conflicts"] == {}
# ---------------- work item dedup ----------------
def test_work_items_dedup_by_knowledge_needed():
from central.app.ai.workitems import build_work_items
questions = [
{"QUESTION_ID": "Q1", "DATABASE": "MMMODAS", "COMPANY": "MMMODAS", "SCOPE": "NCM", "GROUP": "", "NCM": "33059000", "KEY": "ex_tipi", "CELLS": 10},
{"QUESTION_ID": "Q2", "DATABASE": "UNIAO", "COMPANY": "UNIAO", "SCOPE": "NCM", "GROUP": "", "NCM": "33059000", "KEY": "ex_tipi", "CELLS": 5},
{"QUESTION_ID": "Q3", "DATABASE": "RINCAO", "COMPANY": "RINCAO", "SCOPE": "GROUP", "GROUP": "000015", "KEY": "product_role", "CELLS": 40},
{"QUESTION_ID": "Q4", "DATABASE": "CONSTRUCAO", "COMPANY": "CONSTRUCAO", "SCOPE": "PRODUCT", "GROUP": "", "NCM": "", "KEY": "ncm", "CELLS": 8, "QUESTION": "produto PRODUCT 000054", "DATABASE_CODE": "000054"},
]
items = build_work_items(questions, product_fingerprints={"CONSTRUCAO:000054": "fp-a"})
# 4 raw questions -> 3 deduped work items (two ex_tipi questions for NCM 33059000 collapse)
by_key = {i.dedup_key: i for i in items}
assert len(items) == 3
assert by_key["ex_tipi:33059000"].cells_covered == 15
assert by_key["ex_tipi:33059000"].question_refs == ["Q1", "Q2"]
assert by_key["product_role:RINCAO:000015"].cells_covered == 40
assert by_key["invalid_ncm:fp-a"].cells_covered == 8
# AI-ineligible keys never become work items
assert all(i.task_type in ("INVALID_NCM", "CEST", "EX_TIPI", "PRODUCT_ROLE") for i in items)
# ---------------- provider contract / structured output / budget ----------------
def test_structured_output_schema_and_baseline_provider():
from central.app.ai.provider import BaselineProvider, result_from_dict, SemanticOutputError
provider = BaselineProvider()
item = type("W", (), {"task_type": "INVALID_NCM", "dedup_key": "invalid_ncm:fp-a", "payload": {"description": "AGUA MINERAL NATURAL 500ML"}})()
candidates = [("22011000", "AGUAS MINERAIS NATURAIS", 0.75), ("22019000", "AGUAS ... OUTRAS", 0.25)]
result = provider.select(item, candidates, dataset_versions={"ncm": "sha-ncm"})
assert result.task_type == "INVALID_NCM" and result.selected_candidate == "22011000"
assert result.candidate_ids_seen == ["22011000", "22019000"]
assert result.model == "baseline-deterministic-v1" and result.prompt_version == "ai-semantic-v1"
assert result.dataset_versions and result.engine_version
as_dict = result.to_dict()
assert result_from_dict(as_dict).selected_candidate == "22011000"
# schema fail-closed: a provider answer outside the candidate list is invalid
try:
result_from_dict({**as_dict, "selected_candidate": "99999999"})
raise AssertionError("must reject")
except SemanticOutputError:
pass
# no candidates at all -> NONE, never invented
none_result = provider.select(item, [])
assert none_result.selected_candidate == "NONE"
def test_budget_guard_blocks_run_over_limit():
from central.app.ai.provider import BudgetGuard
guard = BudgetGuard(max_run_cost_usd=1.0, price_in_per_m=1.0, price_out_per_m=2.0)
plan = guard.estimate(items_in_tokens=600_000, items_out_tokens=300_000)
assert plan["estimated_cost"] > 0
assert guard.authorize(plan) is False # (600k*1 + 300k*2)/1M = 1.2 > 1.0
assert guard.authorize(guard.estimate(100_000, 50_000)) is True
# ---------------- deterministic candidates ----------------
def test_ncm_top_k_and_ex_tipi_candidates():
from central.app.ai.candidates import ncm_top_k, ex_tipi_candidates_for_ncm, cest_candidates_for_ncm
engine = _engine()
with Session(engine) as session:
_ncm_dataset(session, [("22011000", "AGUAS MINERAIS NATURAIS"), ("22019000", "AGUAS MINERAIS GASEIFICADAS OUTRAS"), ("08043000", "ABACAXIS FRESCOS")])
top = ncm_top_k(session, "AGUA MINERAL NATURAL", k=2)
assert [c[0] for c in top] == ["22011000", "22019000"]
dataset = Dataset(kind="TIPI_IPI", provider="RFB", sha256="b" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=2)
session.add(dataset); session.flush()
session.add(TipiEntry(dataset_id=dataset.id, ncm="33059000", ex=None, description="outros", ipi_rate=None, ipi_rate_text="NT", source_row=1))
session.add(TipiEntry(dataset_id=dataset.id, ncm="33059000", ex="01", description="preparacoes para alisar", ipi_rate=None, ipi_rate_text="NT", source_row=2))
cest_dataset = Dataset(kind="CEST", provider="CONFAZ", sha256="c" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=1)
session.add(cest_dataset); session.flush()
cest = CestEntry(dataset_id=cest_dataset.id, code="03.999.99", description="x", annex="II")
session.add(cest); session.flush()
session.add(CestNcmEntry(cest_entry_id=cest.id, ncm_code="22011000"))
session.commit()
assert [e.ex for e in ex_tipi_candidates_for_ncm(session, "33059000")] == ["01"]
assert [c.code for c in cest_candidates_for_ncm(session, "22011000")] == ["03.999.99"]
def test_kb_persist_roundtrip_models():
from central.app.models import KbProduct, KbKnowledge, KbObservation, TenantProductContext, AiWorkItem, AiRun
engine = _engine()
with Session(engine) as session:
p = KbProduct(fingerprint="f", normalized_description="D")
session.add(p); session.flush()
session.add(KbKnowledge(kb_product_id=p.id, knowledge_key="candidate_ncm", value="22011000", source="OFFICIAL_DATASET", confidence=1.0))
session.add(KbObservation(kb_product_id=p.id, tenant_id=1, company_key="C", erp_code="1", database_label="C", observed_fields={}))
session.add(TenantProductContext(tenant_id=1, company_key="C", erp_code="1", kb_product_id=p.id))
session.add(AiWorkItem(task_type="CEST", dedup_key="cest:1", payload={}, question_refs=[], cells_covered=1))
session.add(AiRun(provider="baseline", model="m", prompt_version="v1", input_tokens=1, output_tokens=1, cost_usd=0.0, status="DONE", results=[]))
session.commit()
assert session.query(KbProduct).count() == 1 and session.query(AiWorkItem).count() == 1
+12
View File
@@ -121,6 +121,18 @@ def test_cfop_entrada_needs_company_entry_scope_then_derives():
assert run(p=product(CFOP_ENTRADA="1102"))["CFOP_ENTRADA"]["state"] == ALREADY
def test_cfop_entrada_mixed_scope_is_operation_dependent_and_never_invents_a_cfop():
mixed = [{"scope": "COMPANY", "scope_key": "", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC"}]
r = run(p=product(CFOP_ENTRADA=None), ctx=mixed)
e = r["CFOP_ENTRADA"]
assert e["state"] == MANUAL and e["proposed"] is None and e.get("input_key") is None
assert e["reason"] == "CFOP_ENTRADA_OPERATION_DEPENDENT_MIXED_SCOPE"
r2 = run(p=product(CFOP_ENTRADA="2102"), ctx=mixed)
assert r2["CFOP_ENTRADA"]["state"] == ALREADY and r2["CFOP_ENTRADA"]["reason"] == "DECLARED_VALID_OPERATION_DEPENDENT_MIXED_SCOPE"
from central.app.context_keys import validate_entry
assert validate_entry({"scope": "COMPANY", "key": "entry_scope", "value": "MIXED_OR_DYNAMIC"})[0]["value"] == "MIXED_OR_DYNAMIC"
def test_icms_code_fields_follow_the_regime():
r1 = run()
assert r1["CST"]["state"] == NA and r1["CST_ENTRADA"]["state"] == NA and r1["CSOSN"]["state"] == ALREADY
+130
View File
@@ -0,0 +1,130 @@
"""Real LLM provider contract: strict answers, batching safety, cache, metrics attribution."""
import json
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from central.app.models import Base, AiEvalCache
def _item(work_item_id="w1", candidates=None):
from central.app.ai.llm_provider import LlmEvalItem
return LlmEvalItem(work_item_id=work_item_id, description="AGUA MINERAL 500ML", normalized_description="AGUA MINERAL 500ML",
brand=None, unit="ML", segment="mercado", candidates=candidates or [("22011000", "AGUAS MINERAIS", 0.9), ("22019000", "AGUAS OUTRAS", 0.4)])
def _answer(work_item_id="w1", ncm="22011000", confidence=0.9, abstain=False):
return {"work_item_id": work_item_id, "selected_ncm": ncm, "confidence": confidence, "abstain": abstain, "reason_short": "desc match"}
def test_validate_answer_accepts_only_candidate_or_abstain():
from central.app.ai.llm_provider import LlmAnswerError, validate_answer
item = _item()
assert validate_answer(_answer(), item)["selected_ncm"] == "22011000"
abstain = validate_answer(_answer(ncm=None, abstain=True), item)
assert abstain["abstain"] is True and abstain["selected_ncm"] is None
for bad in (_answer(ncm="99999999"), _answer(work_item_id="other"), _answer(ncm=None), _answer(confidence=1.5), _answer(ncm="22011000", abstain=True)):
try:
validate_answer(bad, item)
raise AssertionError("must reject")
except LlmAnswerError:
pass
def test_provider_batch_requires_1_to_1_answers():
from central.app.ai.llm_provider import LlmAnswerError, OpenAICompatibleProvider
def transport(payload):
content = json.dumps([_answer(), _answer()]) # two answers for one item
return {"choices": [{"message": {"content": content}}], "usage": {"prompt_tokens": 10, "completion_tokens": 5}}
provider = OpenAICompatibleProvider(api_key="test-key", model="m", transport=transport)
try:
provider.select_batch([_item()])
raise AssertionError("must reject")
except LlmAnswerError:
pass
assert provider.http_calls == 1 # contract violations are NOT retried
def test_provider_retries_transport_failures_and_counts_usage():
from central.app.ai.llm_provider import OpenAICompatibleProvider
calls = {"n": 0}
def transport(payload):
calls["n"] += 1
if calls["n"] == 1:
raise ConnectionError("boom")
content = json.dumps([_answer()])
return {"choices": [{"message": {"content": content}}], "usage": {"prompt_tokens": 100, "completion_tokens": 20, "prompt_tokens_details": {"cached_tokens": 50}}}
provider = OpenAICompatibleProvider(api_key="k", model="m", transport=transport)
answers = provider.select_batch([_item()])
assert answers[0]["selected_ncm"] == "22011000"
assert provider.http_calls == 2 and provider.retries == 1
assert provider.usage == {"input_tokens": 100, "output_tokens": 20, "cached_tokens": 50}
def test_prompt_hides_gold_and_carries_candidates():
from central.app.ai.llm_provider import candidate_set_hash, input_fingerprint, item_prompt_payload
item = _item()
payload = item_prompt_payload(item)
assert "22011000" in json.dumps(payload) # candidates present
assert item.normalized_description in json.dumps(payload)
assert len(candidate_set_hash(item)) == 64 and len(input_fingerprint(item)) == 64
changed = _item(candidates=[("11111111", "X", 0.1)])
assert candidate_set_hash(changed) != candidate_set_hash(item)
def test_eval_cache_roundtrip_deduplicates():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
with Session(engine) as session:
row = AiEvalCache(input_fingerprint="f", candidate_set_hash="c", prompt_version="p", model="m", dataset_version_ncm="d", phase="gold", answer={"selected_ncm": "22011000"}, gold_ncm="22011000")
session.add(row); session.commit()
existing = session.scalar(select(AiEvalCache).where(AiEvalCache.input_fingerprint == "f", AiEvalCache.candidate_set_hash == "c", AiEvalCache.prompt_version == "p", AiEvalCache.model == "m", AiEvalCache.dataset_version_ncm == "d"))
assert existing.answer["selected_ncm"] == "22011000"
def test_metrics_and_error_attribution():
from central.app.ai.eval_metrics import attribution, confidence_buckets, metrics
rows = [
{"gold_in_candidates": True, "selected": "A", "gold": "A", "confidence": 0.99, "invalid": False},
{"gold_in_candidates": True, "selected": "B", "gold": "A", "confidence": 0.97, "invalid": False},
{"gold_in_candidates": True, "selected": None, "gold": "A", "confidence": 0.5, "invalid": False},
{"gold_in_candidates": False, "selected": "B", "gold": "A", "confidence": 0.9, "invalid": False},
{"gold_in_candidates": True, "selected": "zzz", "gold": "A", "confidence": 0.9, "invalid": True},
]
m = metrics(rows)
assert m["TOTAL"] == 5 and m["CORRECT"] == 1 and m["WRONG"] == 2 and m["ABSTAIN"] == 1 and m["INVALID"] == 1
assert m["GOLD_IN_CANDIDATES"] == 4
assert abs(m["SELECTION_ACCURACY_WHEN_GOLD_AVAILABLE"] - 1 / 3) < 1e-4 # correct / (correct+wrong) among valid answers
assert abs(m["ABSTENTION_RATE"] - 0.2) < 1e-9
a = attribution(rows)
assert a["GOLD_NOT_IN_CANDIDATES"] == 1 and a["GOLD_IN_CANDIDATES_BUT_WRONG"] == 1 and a["GOLD_IN_CANDIDATES_BUT_ABSTAIN"] == 1
buckets = confidence_buckets(rows)
assert buckets[">=0.99"] == {"total": 1, "correct": 1} and buckets["0.95-0.9899"] == {"total": 1, "correct": 0}
def test_zai_model_selection_rules():
import importlib.util
spec = importlib.util.spec_from_file_location("eval_runner", "central/scripts/run_real_llm_ncm_eval.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
available = ["glm-5.3", "glm-5.3-flash", "glm-4.7", "other-vendor-x"]
assert module._select_model(available, "glm-5.3") == ("glm-5.3", "requested")
# requested missing -> current GLM flash fallback, never another vendor, never invented
assert module._select_model(available, "glm-9.9") == ("glm-5.3-flash", "fallback_glm_flash")
only_generic = ["glm-4", "glm-4.7"]
assert module._select_model(only_generic, "glm-9.9") == ("glm-4.7", "fallback_glm_latest")
assert module._select_model(["gpt-x", "claude-y"], "glm-5.3") == (None, "no_glm_available")
assert module._diagnose_http(401) == "credential invalid (AI_API_KEY)"
def test_budget_guard_unknown_cost_limited_run():
from central.app.ai.provider import BudgetGuard
guard = BudgetGuard(max_run_cost_usd=5.0, price_in_per_m=None, price_out_per_m=None)
plan = guard.estimate(1000, 100)
assert plan["estimated_cost"] is None and plan["cost_unknown"] is True
assert guard.authorize(plan) is False # residual runs refused without prices
assert guard.authorize(plan, allow_unknown_cost=True) is True # explicitly limited GOLD benchmark may opt in
+116
View File
@@ -0,0 +1,116 @@
"""Semantic candidate retrieval V2: hierarchical official corpus + KB-neighbor bridge + hybrid ranking."""
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from central.app.models import Base, Dataset, KbKnowledge, KbObservation, KbProduct, NcmEntry, Tenant
def _engine():
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
return engine
def _seed(session):
ncm = Dataset(kind="NCM", provider="Siscomex", sha256="n" * 64, version_label="v", act="a", source_url="u", status="ACTIVE", record_count=7)
session.add(ncm); session.flush()
rows = [
("22", "BEBIDAS, LIQUIDOS ALCOOLICOS E VINAGRES"),
("2202", "AGUAS, INCLUIDAS AS AGUAS GASEIFICADAS E MINERAIS"),
("2202.10", "AGUAS MINERAIS E GASEIFICADAS"),
("22021000", "AGUAS MINERAIS NAO GASEIFICADAS"),
("61", "VESTUARIO E SEUS ACESSORIOS, DE MALHA"),
("6105", "CAMISAS DE MALHA, DE USO MASCULINO"),
("61051000", "DE ALGODAO"),
]
for code, description in rows:
session.add(NcmEntry(dataset_id=ncm.id, code=code.replace(".", ""), description=description, valid_from="", valid_to=""))
session.flush()
# two tenants already selling mineral water with a validated NCM
tenant_a, tenant_b = Tenant(name="A"), Tenant(name="B")
session.add_all([tenant_a, tenant_b]); session.flush()
p1 = KbProduct(fingerprint="f1", normalized_description="AGUA MINERAL SEM GAS 500ML")
p2 = KbProduct(fingerprint="f2", normalized_description="AGUA MINERAL GARRAFA 1L")
p3 = KbProduct(fingerprint="f3", normalized_description="CAMISA MASCULINA MALHA ALGODAO")
session.add_all([p1, p2, p3]); session.flush()
for product, ncm_value, tenant in ((p1, "22021000", tenant_a), (p2, "22021000", tenant_b), (p3, "61051000", tenant_a)):
session.add(KbKnowledge(kb_product_id=product.id, knowledge_key="validated_ncm", value=ncm_value, source="OFFICIAL_DATASET", confidence=1.0))
session.add(KbObservation(kb_product_id=p1.id, tenant_id=tenant_a.id, company_key="A", erp_code="1", database_label="A", observed_fields={}))
session.add(KbObservation(kb_product_id=p2.id, tenant_id=tenant_b.id, company_key="B", erp_code="2", database_label="B", observed_fields={}))
session.add(KbObservation(kb_product_id=p3.id, tenant_id=tenant_a.id, company_key="A", erp_code="3", database_label="A", observed_fields={}))
session.commit()
return ncm.sha256
def test_enriched_corpus_carries_ancestor_context():
from central.app.ai.retrieval import EnrichedNcmCorpus
engine = _engine()
with Session(engine) as session:
_seed(session)
corpus = EnrichedNcmCorpus.build(session)
doc = corpus.document("22021000")
# chapter + position + subposition context enriches the very short legal text (stemmed)
assert "BEBIDA" in doc and "AGUA" in doc
assert "VESTUARIO" not in doc
def test_hierarchical_retrieval_prefers_right_positions():
from central.app.ai.retrieval import EnrichedNcmCorpus, hierarchical_candidates
engine = _engine()
with Session(engine) as session:
_seed(session)
corpus = EnrichedNcmCorpus.build(session)
codes = [code for code, _, _ in hierarchical_candidates(corpus, "AGUA MINERAL", k=2)]
assert codes[0] == "22021000"
def test_kb_neighbor_bridge_maps_commercial_to_ncm():
from central.app.ai.retrieval import KbNeighborIndex, kb_neighbor_candidates
engine = _engine()
with Session(engine) as session:
_seed(session)
index = KbNeighborIndex.build(session)
neighbors = kb_neighbor_candidates(index, "AGUA MINERAL PET 500ML", k=5)
assert neighbors and neighbors[0][0] == "22021000"
# cross-tenant agreement is reported, never auto-majority
assert neighbors[0][3] >= 2
def test_hybrid_merges_official_and_kb_rankings():
from central.app.ai.retrieval import EnrichedNcmCorpus, KbNeighborIndex, hybrid_candidates
engine = _engine()
with Session(engine) as session:
_seed(session)
corpus = EnrichedNcmCorpus.build(session)
index = KbNeighborIndex.build(session)
hybrid = hybrid_candidates(corpus, index, "CAMISA MASCULINA ALGODAO", k=2)
assert hybrid[0][0] == "61051000"
scores = hybrid_candidates(corpus, index, "AGUA MINERAL SEM GAS", k=2)
assert scores[0][0] == "22021000"
def test_lexicon_expands_abbreviations_supported_by_kb():
from central.app.ai.retrieval import derive_abbreviation_lexicon, expand_stems
corpus = {
"REFRIGERANTE COCA 2L": "22021000",
"REFRI GUARANA 1L": "22021000",
"REFRI LARANJA 350ML": "22021000",
"CAMISA POLO ALGODAO": "61051000",
"CAM POLO MASC": "61051000",
"CAM POLO AZUL": "61051000",
"BOTA COURO MASC": "64039900",
}
lexicon = derive_abbreviation_lexicon(corpus, min_support=2)
assert lexicon.get("REFRI") == "REFRIGERANTE"
assert lexicon.get("CAM") == "CAMISA"
expanded = expand_stems({"REFRI", "COLA"}, lexicon)
assert "REFRIGERANTE" in expanded
def test_commercial_tokens_strip_units_and_sizes():
from central.app.ai.retrieval import commercial_tokens
tokens = commercial_tokens("CREME TRATAMENTO NOVEX 1KG CX 12 COR AZUL TAM M REF 453")
assert "NOVEX" in tokens and "CREME" in tokens and "TRATAMENTO" in tokens
assert "1KG" not in tokens and "CX" not in tokens and "COR" not in tokens and "TAM" not in tokens
assert "AZUL" not in tokens and "REF" not in tokens and "453" not in tokens
assert "12" not in tokens