Files
GTI_GERENCIADOR_FISCAL/fiscal/central/tests/test_llm_eval.py
T
gti d21a9d1140 Add 'fiscal/' from commit '102ba84500dbd9655a0471288c3e278652593ca9'
git-subtree-dir: fiscal
git-subtree-mainline: d95165fc8f
git-subtree-split: 102ba84500
2026-09-29 14:43:23 -03:00

131 lines
6.9 KiB
Python

"""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