176 lines
8.2 KiB
Python
176 lines
8.2 KiB
Python
"""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
|