Projeto do Agent Contas ORACLE

This commit is contained in:
2026-08-19 09:35:50 -03:00
commit 950a2bcd33
1366 changed files with 177217 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
"""Caracterização do matcher com erros de transcrição sobre o catálogo VAS real
(embutido em ``_CATALOG_CSV`` abaixo).
Mostra como o ranking de similaridade (frase/token/som) se sai HOJE: para uma
menção com erro de grafia/pronúncia, o topo do bloco de candidatos renderizado
deve ser uma variante da família esperada, competindo contra o catálogo inteiro
(~190 variantes). Casos sabidamente não cobertos são ``xfail`` com o motivo —
documentam limitações atuais, não bugs a esconder.
"""
from __future__ import annotations
import csv
import io
import re
from typing import Any
import pytest
from app.domain.contas.item_matcher import SimilarityItemMatcher
# Catálogo VAS real (nome_base,n_variantes,variantes) — antes a fixture
# data/termos_complexos_3.csv. Mantido como CSV literal: pra atualizar, basta
# colar/editar linhas. A mojibake ("Padrão") é do dump original e é inócua aqui
# (a normalização tira acento; o que importa é casar a família, não o sufixo).
_CATALOG_CSV = """\
nome_base,n_variantes,variantes
Fluid,18,"Fluid Mensal, Fluid Mensal 10, Fluid Mensal 11, Fluid Mensal 12, Fluid Mensal 13, Fluid Mensal 2, Fluid Mensal 3, Fluid Mensal 4, Fluid Mensal 5, Fluid Mensal 6, Fluid Mensal 7, Fluid Mensal 8, Fluid Mensal 9, Fluid Semanal, Fluid Semanal 2, Fluid Semanal 3, Fluid Semanal 4, Fluid Semanal 5"
TapLingo,13,"TapLingo 1 Idioma Diario, TapLingo 1 Idioma Mensal, TapLingo 1 Idioma Semanal, TapLingo 1 Idioma Semanal 1, TapLingo 1 Idioma Semanal 2, TapLingo 2 Idiomas Mensal, TapLingo 2 Idiomas Semanal, TapLingo 2 Idiomas Semanal 1, TapLingo 2 Idiomas Semanal 2, TapLingo Idiomas Mensal, TapLingo Idiomas Semanal, TapLingo Idiomas Semanal 1, TapLingo Idiomas Semanal 2"
MasterChef,9,"MasterChef Mensal, MasterChef Mensal 1, MasterChef Mensal 2, MasterChef Semanal, MasterChef Semanal 1, MasterChef Semanal 2, MasterChef Semanal 3, MasterChef Semanal 4, MasterChef Semanal 5"
Gameloft,7,"Gameloft mensal 1, Gameloft mensal 2, Gameloft mensal 3, Gameloft semanal 1, Gameloft semanal 2, Gameloft semanal 3, Gameloft semanal 4"
Prime Tube,7,"Prime Tube - Diario, Prime Tube - Mes 1, Prime Tube - Semanal 1, Prime Tube - Semanal 2, Prime Tube - Semanal 3, Prime Tube - avulso 1, Prime Tube - avulso 2"
Tamboro,7,"Tamboro Mensal, Tamboro Mensal 1, Tamboro Semanal, Tamboro Semanal 1, Tamboro Semanal 2, Tamboro Semanal 3, Tamboro Semanal 4"
Kids Clube,6,"Kids Clube Mes, Kids Clube Mes1, Kids Clube Semana, Kids Clube Semana1, Kids Clube Semana2, Kids Clube Semana3"
Playkids,6,"Playkids - Mensal Promo, Playkids - Semanal, Playkids - Semanal 1, Playkids - Semanal 2, Playkids - Semanal 3, Playkids - Semanal 4"
Dog Lovers,5,"Dog Lovers Mes 1, Dog Lovers Semanal 1, Dog Lovers Semanal 2, Dog Lovers Semanal 3, Dog Lovers Semanal 4"
Fit Me APP,5,"Fit Me APP Premium Mensal, Fit Me APP Premium Semanal, Fit Me APP Premium Semanal 1, Fit Me APP Premium Semanal 2, Fit Me APP Premium Semanal 3"
Fuze Forge,5,"Fuze Forge Mensal, Fuze Forge Semanal, Fuze Forge Semanal 1, Fuze Forge Semanal 2, Fuze Forge Semanal 3"
LiveFC,5,"LiveFC Mensal, LiveFC Semanal 1, LiveFC Semanal 2, LiveFC Semanal 3, LiveFC Semanal 4"
Woman Fitness,5,"Woman Fitness Mes 1, Woman Fitness Semanal 1, Woman Fitness Semanal 2, Woman Fitness Semanal 3, Woman Fitness Semanal 4"
Audiobooks UBOOK,4,"Audiobooks UBOOK Light Mes, Audiobooks UBOOK Mes, Audiobooks UBOOK Platinum Mes, Audiobooks UBOOK Premium Mes"
Disney+,4,"Disney+ Padrão, Disney+ Premium, Upgrade Disney+ Padrao, Upgrade Disney+ Premium"
Pocoyo,4,"Pocoyo Mensal, Pocoyo Mensal 1, Pocoyo Semanal 3, Pocoyo Semanal 4"
Superclipe,4,"Superclipe Mensal, Superclipe Semanal, Superclipe Semanal 1, Superclipe Semanal 2"
Truecaller,4,"Truecaller Semanal, Truecaller Semanal 1, Truecaller Semanal 2, Truecaller Semanal 3"
Globoplay,3,"Globoplay Padrão com anúncio, Globoplay Premium, Upgrade Globoplay Premium"
HBO Max,3,"HBO Max Basico com Anuncios, HBO Max Platinum, Upgrade HBO Max Standard"
Netflix,3,"Netflix Padrão, Netflix Padrão Anúncios, Netflix Premium"
TIM Kids,3,"TIM Kids - Semanal - 2, TIM Kids - Semanal - 3, TIM Kids - Semanal - 4"
+ MOB,2,"+ MOB - Semanal 1, + MOB - Semanal 2"
Chat Lovme,2,"Chat Lovme - Semanal 1, Chat Lovme - Semanal 2"
Clube Gameloft,2,"Clube Gameloft mensal, Clube Gameloft semanal"
Deezer,2,"Deezer PROMO SVA diario, Upgrade Deezer Premium"
MVR360,2,"MVR360 - Mes 1, MVR360 - Semanal 4"
MVR Joker,2,"MVR Joker Mensal, MVR Joker Semanal"
Star,2,"Star Mensal, Star Promocional"
Apple Music,1,Apple Music SVA Mes
Babbel,1,Babbel Mes
Dindo HIT,1,Dindo HIT - Mes 1
Espn,1,Espn mensal
HBO Max Ads,1,Upgrade HBO Max Ads-Platinum
Instarena,1,Instarena mensal
Jokoo,1,Jokoo Mes 1
Kwiz,1,Kwiz - Mes 1
Loja Gameloft,1,Loja Gameloft SVA mensal
Max Mobile,1,Max Mobile Mensal
MobiVideos,1,MobiVideos - Mes 1
Mundome,1,Mundome Mensal
Nordeste FC,1,Nordeste FC
Paramount+,1,Paramount+ Mensal
Premiere GB,1,Premiere GB
TIM Games Club,1,TIM Games Club
TIM Games for Kids,1,TIM Games for Kids
TIM Music,1,TIM Music Upsell SVA Semanal
Zodiack,1,Zodiack - Mes 1
"""
def _load_catalog() -> dict[str, str]:
"""variante -> nome_base, a partir do catálogo embutido (``_CATALOG_CSV``)."""
variant_base: dict[str, str] = {}
for row in csv.DictReader(io.StringIO(_CATALOG_CSV)):
for variant in (v.strip() for v in row["variantes"].split(",")):
variant_base[variant] = row["nome_base"]
return variant_base
_VARIANT_BASE = _load_catalog()
_UNIVERSE = list(_VARIANT_BASE)
class _Resp:
content = '{"matches": []}'
tool_calls: list[Any] = []
additional_kwargs: dict[str, Any] = {"reasoning_content": "ok"}
class _CapturingLLM:
"""Não decide nada; só captura o prompt renderizado por ``match`` p/ inspeção."""
def __init__(self) -> None:
self.last_prompt = ""
def invoke(self, messages: list[Any], config: dict[str, Any] | None = None) -> _Resp:
self.last_prompt = str(getattr(messages[-1], "content", ""))
return _Resp()
def _rendered_descs(mention: str) -> list[str]:
"""Top-K determinístico do matcher migrado contra o mesmo catálogo real."""
return [name for name, _score in SimilarityItemMatcher(top_k=10).ranked(mention, _UNIVERSE)]
def _top_base(mention: str) -> str:
"""Família (nome_base) do candidato no TOPO do bloco renderizado."""
return _VARIANT_BASE[_rendered_descs(mention)[0]]
# (erro_de_transcricao, nome_base_esperado) — erros plausíveis de grafia/pronúncia.
_CASES = [
("fluidi", "Fluid"),
("teplingo", "TapLingo"),
("master chefe", "MasterChef"),
("mestershef", "MasterChef"),
("gueimiloft", "Gameloft"),
("praim tube", "Prime Tube"),
("tamboro", "Tamboro"),
("kidsclube", "Kids Clube"),
("plei kids", "Playkids"),
("dog lovers", "Dog Lovers"),
("fitimiap", "Fit Me APP"),
("fuze forge", "Fuze Forge"),
("laivifci", "LiveFC"),
("uman fitness", "Woman Fitness"),
("ubook", "Audiobooks UBOOK"),
("disnei", "Disney+"),
("pocoio", "Pocoyo"),
("superclip", "Superclipe"),
("trucoler", "Truecaller"),
("trucaller", "Truecaller"),
("globoplei", "Globoplay"),
("netiflix", "Netflix"),
("chat lovme", "Chat Lovme"),
("dizer", "Deezer"),
("star", "Star"),
("apou", "Apple Music"),
("babel", "Babbel"),
("ispian", "Espn"),
("paramaunt", "Paramount+"),
("zodiaque", "Zodiack"),
("instarena", "Instarena"),
("mundome", "Mundome"),
("jokô", "Jokoo"),
("agebeo max", "HBO Max"),
("tim miusic", "TIM Music"),
]
# Casos hoje não cobertos pelo ranking de similaridade — documentados, não escondidos.
_XFAIL = {}
@pytest.mark.parametrize("mention, base", _CASES, ids=[c[0] for c in _CASES])
def test_erro_de_transcricao_ranqueia_familia_no_topo(mention: str, base: str) -> None:
if mention in _XFAIL:
pytest.xfail(_XFAIL[mention])
assert _top_base(mention) == base
@pytest.mark.parametrize("mention, base", _CASES, ids=[c[0] for c in _CASES])
def test_top_k_nao_perde_a_familia(mention: str, base: str) -> None:
# Mesmo truncando o prompt aos N mais próximos, a família esperada continua
# presente (recall@K). Vale até para os casos cujo TOP-1 está errado (xfail
# acima): eles caem por volta da posição 6, ainda dentro do top-K.
bases = {_VARIANT_BASE[d] for d in _rendered_descs(mention)}
assert base in bases