Ir para o conteúdo

Extratores

src.extratores

Modulo de extratores do Pede AI.

Fornece ferramentas de processamento de linguagem natural (NLP) para extrair informacoes de mensagens do usuario.

Example
from src.extratores import extrair, extrair_variante, extrair_item_carrinho

resultado = extrair('um x-salada sem tomate')
extrair_variante('duplo', 'lanche_001')
'duplo'

extrair(mensagem: str) -> list[dict]

Extrai itens do cardapio de uma mensagem do usuario.

API compativel com a versao procedural — retorna list[dict].

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required

Returns:

Type Description
list[dict]

Lista de dicionarios com as chaves: - item_id: ID do item no cardapio. - quantidade: Quantidade solicitada. - variante: Variante selecionada (ou None). - remocoes: Lista de ingredientes a remover.

Example
from src.extratores import extrair

extrair('2 x-bacon sem cebola')
[
    {
        'item_id': 'lanche_003',
        'quantidade': 2,
        'variante': None,
        'remocoes': ['cebola'],
    }
]
Source code in src/extratores/extrator.py
def extrair(mensagem: str) -> list[dict]:
    """Extrai itens do cardapio de uma mensagem do usuario.

    API compativel com a versao procedural — retorna list[dict].

    Args:
        mensagem: Texto da mensagem do usuario.

    Returns:
        Lista de dicionarios com as chaves:
            - item_id: ID do item no cardapio.
            - quantidade: Quantidade solicitada.
            - variante: Variante selecionada (ou None).
            - remocoes: Lista de ingredientes a remover.

    Example:
        ```python
        from src.extratores import extrair

        extrair('2 x-bacon sem cebola')
        [
            {
                'item_id': 'lanche_003',
                'quantidade': 2,
                'variante': None,
                'remocoes': ['cebola'],
            }
        ]
        ```
    """
    itens = _get_extrator().extrair(mensagem)
    return [asdict(item) for item in itens]

extrair_variante(mensagem: str, item_id: str) -> str | None

Extrai e valida uma variante de uma mensagem para um item especifico.

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required
item_id str

ID do item no cardapio para validacao.

required

Returns:

Type Description
str | None

Texto da variante valida ou None.

Source code in src/extratores/extrator.py
def extrair_variante(mensagem: str, item_id: str) -> str | None:
    """Extrai e valida uma variante de uma mensagem para um item especifico.

    Args:
        mensagem: Texto da mensagem do usuario.
        item_id: ID do item no cardapio para validacao.

    Returns:
        Texto da variante valida ou None.
    """
    return _get_extrator().extrair_variante(mensagem, item_id)

extrair_item_carrinho(mensagem: str, carrinho: list) -> list[dict]

Extrai itens do carrinho para remocao.

API compativel com a versao procedural — retorna list[dict].

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required
carrinho list

Lista de itens no carrinho atual.

required

Returns:

Type Description
list[dict]

Lista de dicionarios com item_id, variante, indices.

Source code in src/extratores/__init__.py
def extrair_item_carrinho(mensagem: str, carrinho: list) -> list[dict]:
    """Extrai itens do carrinho para remocao.

    API compativel com a versao procedural — retorna list[dict].

    Args:
        mensagem: Texto da mensagem do usuario.
        carrinho: Lista de itens no carrinho atual.

    Returns:
        Lista de dicionarios com item_id, variante, indices.
    """
    from dataclasses import asdict  # noqa: PLC0415

    from src.config import get_cardapio  # noqa: PLC0415
    from src.extratores.carrinho_extrator import CarrinhoExtrator  # noqa: PLC0415
    from src.extratores.config import get_extrator_config  # noqa: PLC0415
    from src.extratores.nlp_engine import NlpEngine  # noqa: PLC0415

    config = get_extrator_config()
    cardapio = get_cardapio()
    engine = NlpEngine(config, cardapio)
    extrator = CarrinhoExtrator(engine, config)
    matches = extrator.extrair(mensagem, carrinho)
    return [asdict(m) for m in matches]

extrair_itens_troca(mensagem: str, carrinho: list[dict]) -> dict

Extrai informacoes de troca da mensagem.

API compativel com a versao procedural — retorna dict.

Parameters:

Name Type Description Default
mensagem str

Mensagem do usuario.

required
carrinho list[dict]

Carrinho atual.

required

Returns:

Type Description
dict

Dict com 'caso', 'item_original' e 'variante_nova'.

Source code in src/extratores/__init__.py
def extrair_itens_troca(mensagem: str, carrinho: list[dict]) -> dict:
    """Extrai informacoes de troca da mensagem.

    API compativel com a versao procedural — retorna dict.

    Args:
        mensagem: Mensagem do usuario.
        carrinho: Carrinho atual.

    Returns:
        Dict com 'caso', 'item_original' e 'variante_nova'.
    """
    from dataclasses import asdict  # noqa: PLC0415

    from src.config import get_cardapio  # noqa: PLC0415
    from src.extratores.config import get_extrator_config  # noqa: PLC0415
    from src.extratores.nlp_engine import NlpEngine  # noqa: PLC0415
    from src.extratores.troca_extrator import TrocaExtrator  # noqa: PLC0415

    config = get_extrator_config()
    cardapio = get_cardapio()
    engine = NlpEngine(config, cardapio)
    extrator = TrocaExtrator(engine, config)
    resultado = extrator.extrair(mensagem, carrinho)

    return {
        'caso': resultado.caso,
        'item_original': (
            asdict(resultado.item_original) if resultado.item_original else None
        ),
        'variante_nova': resultado.variante_nova,
    }

src.extratores.extrator

Extrator principal de itens do cardapio.

API publica com compatibilidade retroativa — retorna dicts para nao quebrar consumidores existentes.

Example
from src.extratores.extrator import extrair, extrair_variante

extrair('2 x-bacon sem cebola')
[
    {
        'item_id': 'lanche_003',
        'quantidade': 2,
        'variante': None,
        'remocoes': ['cebola'],
    }
]

Extrator(engine: NlpEngine, config: ExtratorConfig, cardapio: dict)

Extrator de itens do cardapio via spaCy + fuzzy fallback.

Inicializa o extrator.

Parameters:

Name Type Description Default
engine NlpEngine

NlpEngine com modelo spaCy lazy-loaded.

required
config ExtratorConfig

Configuracao do extrator.

required
cardapio dict

Dados do cardapio (get_cardapio()).

required
Source code in src/extratores/extrator.py
def __init__(
    self,
    engine: NlpEngine,
    config: ExtratorConfig,
    cardapio: dict,
) -> None:
    """Inicializa o extrator.

    Args:
        engine: NlpEngine com modelo spaCy lazy-loaded.
        config: Configuracao do extrator.
        cardapio: Dados do cardapio (get_cardapio()).
    """
    self._engine = engine
    self._config = config
    self._itens_ids = build_itens_ids(cardapio)
    self._cardapio = cardapio

extrair(mensagem: str) -> list[ItemExtraido]

Extrai itens do cardapio de uma mensagem.

Tenta EntityRuler (spaCy) primeiro. Se nao encontrar itens, usa fuzzy matching como fallback para tolerar typos.

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required

Returns:

Type Description
list[ItemExtraido]

Lista de ItemExtraido.

Source code in src/extratores/extrator.py
def extrair(self, mensagem: str) -> list[ItemExtraido]:
    """Extrai itens do cardapio de uma mensagem.

    Tenta EntityRuler (spaCy) primeiro. Se nao encontrar itens,
    usa fuzzy matching como fallback para tolerar typos.

    Args:
        mensagem: Texto da mensagem do usuario.

    Returns:
        Lista de ItemExtraido.
    """
    # Verifica negacao primeiro — se usuario esta cancelando o pedido
    if detectar_negacao(mensagem):
        return []

    doc = self._engine.processar(mensagem)

    itens_spacy = self._extrair_spacy(doc)

    # Fuzzy para regioes nao cobertas pelo EntityRuler
    itens_fuzzy = self._extrair_fuzzy_nao_coberto(doc, itens_spacy)

    if itens_spacy or itens_fuzzy:
        return itens_spacy + itens_fuzzy

    # Fallback total: fuzzy na mensagem inteira
    from src.extratores.fuzzy_extrator import extrair_item_fuzzy  # noqa: PLC0415

    qtd, _ = extrair_quantidade_do_texto(mensagem, self._config)
    return extrair_item_fuzzy(mensagem, int(qtd) if qtd else 1)

extrair_variante(mensagem: str, item_id: str) -> str | None

Extrai e valida uma variante de uma mensagem para um item especifico.

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required
item_id str

ID do item no cardapio para validacao.

required

Returns:

Type Description
str | None

Texto da variante valida ou None.

Source code in src/extratores/extrator.py
def extrair_variante(self, mensagem: str, item_id: str) -> str | None:
    """Extrai e valida uma variante de uma mensagem para um item especifico.

    Args:
        mensagem: Texto da mensagem do usuario.
        item_id: ID do item no cardapio para validacao.

    Returns:
        Texto da variante valida ou None.
    """
    if not mensagem or not mensagem.strip():
        return None

    doc = self._engine.processar(mensagem)
    for ent in doc.ents:
        if ent.label_ == 'VARIANTE' and ent.ent_id_ == item_id:
            return ent.text

    return None

extrair(mensagem: str) -> list[dict]

Extrai itens do cardapio de uma mensagem do usuario.

API compativel com a versao procedural — retorna list[dict].

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required

Returns:

Type Description
list[dict]

Lista de dicionarios com as chaves: - item_id: ID do item no cardapio. - quantidade: Quantidade solicitada. - variante: Variante selecionada (ou None). - remocoes: Lista de ingredientes a remover.

Example
from src.extratores import extrair

extrair('2 x-bacon sem cebola')
[
    {
        'item_id': 'lanche_003',
        'quantidade': 2,
        'variante': None,
        'remocoes': ['cebola'],
    }
]
Source code in src/extratores/extrator.py
def extrair(mensagem: str) -> list[dict]:
    """Extrai itens do cardapio de uma mensagem do usuario.

    API compativel com a versao procedural — retorna list[dict].

    Args:
        mensagem: Texto da mensagem do usuario.

    Returns:
        Lista de dicionarios com as chaves:
            - item_id: ID do item no cardapio.
            - quantidade: Quantidade solicitada.
            - variante: Variante selecionada (ou None).
            - remocoes: Lista de ingredientes a remover.

    Example:
        ```python
        from src.extratores import extrair

        extrair('2 x-bacon sem cebola')
        [
            {
                'item_id': 'lanche_003',
                'quantidade': 2,
                'variante': None,
                'remocoes': ['cebola'],
            }
        ]
        ```
    """
    itens = _get_extrator().extrair(mensagem)
    return [asdict(item) for item in itens]

extrair_variante(mensagem: str, item_id: str) -> str | None

Extrai e valida uma variante de uma mensagem para um item especifico.

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required
item_id str

ID do item no cardapio para validacao.

required

Returns:

Type Description
str | None

Texto da variante valida ou None.

Source code in src/extratores/extrator.py
def extrair_variante(mensagem: str, item_id: str) -> str | None:
    """Extrai e valida uma variante de uma mensagem para um item especifico.

    Args:
        mensagem: Texto da mensagem do usuario.
        item_id: ID do item no cardapio para validacao.

    Returns:
        Texto da variante valida ou None.
    """
    return _get_extrator().extrair_variante(mensagem, item_id)

src.extratores.nlp_engine

Engine NLP com lazy initialization.

Wrapper do spaCy que elimina side effects no import. O modelo so e carregado na primeira chamada de processar().

Example
from src.extratores.config import get_extrator_config
from src.extratores.nlp_engine import NlpEngine

engine = NlpEngine(get_extrator_config(), cardapio)
doc = engine.processar('quero um x-bacon')

NlpEngine(config: ExtratorConfig, cardapio: dict)

Wrapper do spaCy com lazy initialization.

Elimina side effects no import. O modelo spaCy e o EntityRuler so sao inicializados na primeira chamada de processar().

Attributes:

Name Type Description
config

Configuracao do extrator.

cardapio

Dados do cardapio para gerar patterns.

Inicializa o engine sem carregar o modelo.

Parameters:

Name Type Description Default
config ExtratorConfig

Configuracao com thresholds e parametros.

required
cardapio dict

Dados do cardapio para gerar patterns.

required
Source code in src/extratores/nlp_engine.py
def __init__(self, config: ExtratorConfig, cardapio: dict) -> None:
    """Inicializa o engine sem carregar o modelo.

    Args:
        config: Configuracao com thresholds e parametros.
        cardapio: Dados do cardapio para gerar patterns.
    """
    self._config = config
    self._cardapio = cardapio
    self._nlp: spacy.language.Language | None = None
    self._patterns_gerados: bool = False

inicializado: bool property

Retorna True se o modelo ja foi carregado.

nlp: spacy.language.Language | None property

Retorna o objeto spaCy (ou None se nao inicializado).

Para uso interno — prefira processar().

processar(mensagem: str) -> Doc

Processa mensagem com o pipeline NLP.

Carrega o modelo na primeira chamada (lazy).

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required

Returns:

Type Description
Doc

Documento spaCy processado com entidades.

Source code in src/extratores/nlp_engine.py
def processar(self, mensagem: str) -> Doc:
    """Processa mensagem com o pipeline NLP.

    Carrega o modelo na primeira chamada (lazy).

    Args:
        mensagem: Texto da mensagem do usuario.

    Returns:
        Documento spaCy processado com entidades.
    """
    self._inicializar()
    return self._nlp(mensagem)  # type: ignore[return-value]

src.extratores.patterns

Geracao de patterns para o EntityRuler do spaCy.

Funcoes puras — nao dependem de estado global.

Example
from src.extratores.patterns import gerar_patterns
from src.extratores.normalizador import normalizar_para_busca

patterns = gerar_patterns(cardapio, normalizar_para_busca)

gerar_patterns(cardapio: dict, normalizar: Callable[[str], str]) -> list[dict]

Gera patterns de entidade para o EntityRuler.

Cria patterns para todos os itens, aliases, variantes do cardapio e numeros escritos por extenso.

Parameters:

Name Type Description Default
cardapio dict

Dicionario com dados do cardapio.

required
normalizar Callable[[str], str]

Funcao de normalizacao (ex: normalizar_para_busca).

required

Returns:

Type Description
list[dict]

Lista de patterns no formato spaCy EntityRuler.

Source code in src/extratores/patterns.py
def gerar_patterns(
    cardapio: dict,
    normalizar: Callable[[str], str],
) -> list[dict]:
    """Gera patterns de entidade para o EntityRuler.

    Cria patterns para todos os itens, aliases, variantes do cardapio
    e numeros escritos por extenso.

    Args:
        cardapio: Dicionario com dados do cardapio.
        normalizar: Funcao de normalizacao (ex: normalizar_para_busca).

    Returns:
        Lista de patterns no formato spaCy EntityRuler.
    """
    config = get_extrator_config()
    patterns: list[dict] = []
    vistos: set = set()

    itens = cardapio.get('itens', [])
    for item in itens:
        _adicionar_pattern(
            patterns, vistos, 'ITEM', item.get('nome'), item.get('id'), normalizar
        )

        for alias in item.get('aliases') or []:
            _adicionar_pattern(
                patterns, vistos, 'ITEM', alias, item.get('id'), normalizar
            )

        for variante in item.get('variantes') or []:
            opcao = variante.get('opcao', '')
            # Pattern completo: "limao 300ml"
            _adicionar_pattern(
                patterns, vistos, 'VARIANTE', opcao, item.get('id'), normalizar
            )
            # Patterns parciais para matching flexivel
            # Ex: "limao 300ml" -> "limao" e "laranja 500ml" -> "laranja"
            # Digitos nao geram partial patterns — NUM_PENDING cuida disso
            if ' ' in opcao:
                palavra_chave = opcao.split()[0]
                if palavra_chave.lower() not in {'ml'} and not palavra_chave.isdigit():
                    _adicionar_pattern(
                        patterns,
                        vistos,
                        'VARIANTE',
                        palavra_chave,
                        item.get('id'),
                        normalizar,
                    )

    patterns.extend(
        [
            {'label': 'NUM_PENDING', 'pattern': [{'LOWER': palavra}]}
            for palavra in config.numeros_escritos
        ]
    )

    return patterns

src.extratores.normalizador

Normalizacao de texto para extracao.

Duas funcoes nomeadas com semantica clara: - normalizar_para_busca: para EntityRuler (remove pontuacao, troca hifen) - normalizar_para_fuzzy: para fuzzy matching (so unicode + lowercase)

Example
from src.extratores.normalizador import normalizar_para_busca, normalizar_para_fuzzy

normalizar_para_busca('X-Tudo!')
'xtudo'
normalizar_para_fuzzy('Hambúrguer!')
'hamburguer!'

normalizar_para_busca(texto: str) -> str

Normaliza para busca no EntityRuler.

Aplica lowercase, normalizacao Unicode (remove acentos), remove pontuacao e normaliza espacos. Troca hifen por espaco.

Parameters:

Name Type Description Default
texto str

Texto original.

required

Returns:

Type Description
str

Texto normalizado em minusculas sem acentos ou pontuacao.

Example
normalizar_para_busca('X-Tudo!')
'xtudo'
Source code in src/extratores/normalizador.py
def normalizar_para_busca(texto: str) -> str:
    """Normaliza para busca no EntityRuler.

    Aplica lowercase, normalizacao Unicode (remove acentos),
    remove pontuacao e normaliza espacos. Troca hifen por espaco.

    Args:
        texto: Texto original.

    Returns:
        Texto normalizado em minusculas sem acentos ou pontuacao.

    Example:
        ```python
        normalizar_para_busca('X-Tudo!')
        'xtudo'
        ```
    """
    texto = texto.lower()
    texto = unicodedata.normalize('NFKD', texto)
    texto = texto.encode('ascii', 'ignore').decode('utf-8')
    texto = _PUNCTUATION_RE.sub('', texto)
    texto = texto.replace('-', ' ')
    texto = _WHITESPACE_RE.sub(' ', texto).strip()
    return texto

normalizar_para_fuzzy(texto: str) -> str

Normaliza para fuzzy matching.

Aplica lowercase e normalizacao Unicode (remove acentos). Preserva pontuacao interna e espacos.

Parameters:

Name Type Description Default
texto str

Texto original.

required

Returns:

Type Description
str

Texto normalizado em minusculas sem acentos.

Example
normalizar_para_fuzzy('Hambúrguer!')
'hamburguer!'
Source code in src/extratores/normalizador.py
def normalizar_para_fuzzy(texto: str) -> str:
    """Normaliza para fuzzy matching.

    Aplica lowercase e normalizacao Unicode (remove acentos).
    Preserva pontuacao interna e espacos.

    Args:
        texto: Texto original.

    Returns:
        Texto normalizado em minusculas sem acentos.

    Example:
        ```python
        normalizar_para_fuzzy('Hambúrguer!')
        'hamburguer!'
        ```
    """
    texto = texto.lower()
    texto = unicodedata.normalize('NFKD', texto)
    texto = texto.encode('ascii', 'ignore').decode('utf-8')
    return texto.strip()

src.extratores.config

Configuracao do extrator de itens do cardapio.

Centraliza thresholds, stopwords, numeros escritos e parametros do spaCy.

Example
from src.extratores.config import get_extrator_config

config = get_extrator_config()
config.fuzzy_item_cutoff  # 75

ExtratorConfig(fuzzy_item_cutoff: int = 75, fuzzy_variante_cutoff: int = 75, ambiguidade_limite: int = 5, palavras_remocao: frozenset[str] = (lambda: frozenset({'sem', 'tira', 'remove', 'retira', 'nao coloca'}))(), palavras_parada: frozenset[str] = (lambda: frozenset({',', '.', 'com'}))(), conectivos: frozenset[str] = (lambda: frozenset({'e', 'ou'}))(), pos_ignoraveis: frozenset[str] = (lambda: frozenset({'DET', 'ADP'}))(), numeros_escritos: Mapping[str, int] = (lambda: {'um': 1, 'uma': 1, 'dois': 2, 'duas': 2, 'tres': 3, 'quatro': 4, 'cinco': 5, 'seis': 6, 'sete': 7, 'oito': 8, 'nove': 9, 'dez': 10})(), stop_words: frozenset[str] = (lambda: frozenset({'quero', 'quer', 'me', 'da', 'de', 'do', 'um', 'uma', 'uns', 'umas', 'o', 'a', 'os', 'as', 'e', 'ou', 'pra', 'para', 'por', 'pelo', 'pela', 'no', 'na', 'nos', 'nas', 'muda', 'mudar', 'troca', 'trocar', 'coloca', 'bota', 'veja', 'ver', 'mostra', 'mostrar', 'pode', 'favor', 'por favor', 'aqui', 'ali', 'isso', 'isto', 'esse', 'essa'}))(), spacy_model: str = 'pt_core_news_sm', palavras_complemento: frozenset[str] = (lambda: frozenset({'com', 'extra', 'adicional'}))(), numeros_fracionarios: Mapping[str, float] = (lambda: {'meio': 0.5, 'meia': 0.5, 'um e meio': 1.5, 'uma e meia': 1.5})(), palavras_negacao: frozenset[str] = (lambda: frozenset({'nao', 'não', 'nem', 'quero nao', 'quero não', 'esquece', 'esqueça', 'cancela', 'cancelar', 'deixa pra la', 'deixa para lá', 'deixa', 'desisto', 'muda de ideia'}))(), palavras_filtro_remocao: frozenset[str] = (lambda: frozenset({'favor', 'gentileza', 'obrigado', 'obrigada', 'também', 'tambem', 'ainda', 'mais', 'so', 'só', 'apenas', 'alem', 'além', 'depois', 'antes', 'durante', 'basico', 'básico', 'normal', 'padrao', 'padrão', 'nada', 'tudo', 'algo', 'coisa', 'pode', 'ser', 'poder', 'será', 'sera', 'por', 'pra', 'para', 'com', 'sem', 'de', 'do', 'da'}))()) dataclass

Configuracao imutavel do extrator.

Attributes:

Name Type Description
fuzzy_item_cutoff int

Score minimo para fuzzy match de itens (0-100).

fuzzy_variante_cutoff int

Score minimo para fuzzy match de variantes (0-100).

ambiguidade_limite int

Diferenca maxima entre top-2 scores para considerar ambiguo.

palavras_remocao frozenset[str]

Palavras que indicam remocao de ingrediente.

palavras_parada frozenset[str]

Palavras que interrompem captura de remocoes.

conectivos frozenset[str]

Conectivos que podem separar remocoes.

pos_ignoraveis frozenset[str]

POS tags do spaCy a ignorar (DET, ADP).

numeros_escritos Mapping[str, int]

Mapeamento de numeros por extenso para inteiros.

stop_words frozenset[str]

Stop words para fuzzy matching.

spacy_model str

Nome do modelo spaCy a carregar.

palavras_complemento frozenset[str]

Palavras que indicam complementos de um item.

numeros_fracionarios Mapping[str, float]

Mapeamento de numeros fracionarios por extenso.

palavras_negacao frozenset[str]

Palavras e expressoes que indicam negacao/cancelamento.

palavras_filtro_remocao frozenset[str]

Palavras comuns que NUNCA devem ser capturadas como remocoes.

get_extrator_config() -> ExtratorConfig

Retorna configuracao do extrator (cached).

Returns:

Type Description
ExtratorConfig

ExtratorConfig com todos os parametros.

Source code in src/extratores/config.py
def get_extrator_config() -> ExtratorConfig:
    """Retorna configuracao do extrator (cached).

    Returns:
        ExtratorConfig com todos os parametros.
    """
    return _ExtratorCache.carregar()

src.extratores.modelos

Value objects imutaveis para extracao de itens do cardapio.

Todos os models sao frozen dataclasses — representam valores, nao entidades.

Example
from src.extratores.modelos import ItemExtraido

item = ItemExtraido(
    item_id='lanche_001',
    quantidade=2,
    variante='duplo',
    remocoes=['cebola'],
)
item.item_id
'lanche_001'

ItemExtraido(item_id: str, quantidade: int | float, variante: str | None, remocoes: list[str], complementos: list[str] = list(), observacoes: list[str] = list(), confianca: float = 1.0, fonte: Literal['ruler', 'fuzzy', 'llm', 'slot_fill'] = 'ruler') dataclass

Item extraido da mensagem do usuario.

Attributes:

Name Type Description
item_id str

ID do item no cardapio.

quantidade int | float

Quantidade solicitada (inteira ou fracionaria).

variante str | None

Variante selecionada (ou None).

remocoes list[str]

Lista de ingredientes a remover.

complementos list[str]

Lista de complementos adicionados ao item.

observacoes list[str]

Lista de observacoes/modificadores do item.

confianca float

Nivel de confianca na extracao (0.0 a 1.0).

fonte Literal['ruler', 'fuzzy', 'llm', 'slot_fill']

Fonte que realizou a extracao.

ItemOriginal(item_id: str, nome: str, indices: list[int]) dataclass

Item do carrinho identificado para troca.

Attributes:

Name Type Description
item_id str

ID do item no cardapio.

nome str

Nome legivel do item.

indices list[int]

Indices no carrinho que matcham este item.

ExtracaoTroca(caso: Literal['A', 'B', 'C', 'vazio'], item_original: ItemOriginal | None, variante_nova: str | None) dataclass

Resultado da extracao de troca.

Attributes:

Name Type Description
caso Literal['A', 'B', 'C', 'vazio']

Tipo de troca ('A', 'B', 'C', 'vazio').

item_original ItemOriginal | None

Item do carrinho a ser trocado (ou None).

variante_nova str | None

Nova variante desejada (ou None).

MatchCarrinho(item_id: str, variante: str | None, indices: list[int]) dataclass

Match de item mencionado com item do carrinho.

Attributes:

Name Type Description
item_id str

ID do item no cardapio.

variante str | None

Variante do item no carrinho (ou None).

indices list[int]

Indices no carrinho que matcham.

ItemMencionado(texto: str, variante: str | None, ent_id: str) dataclass

Item mencionado na mensagem (uso interno).

Attributes:

Name Type Description
texto str

Texto normalizado do item mencionado.

variante str | None

Variante mencionada junto com o item (ou None).

ent_id str

ID da entidade no spaCy (ent_id_).

src.extratores.remocoes

Captura de remocoes de ingredientes.

Funcao pura para capturar itens a remover apos sinais como 'sem', 'tira', etc.

Example
from src.extratores.remocoes import capturar_remocoes, capturar_remocoes_v2
from src.extratores.config import get_extrator_config

config = get_extrator_config()
remocoes = capturar_remocoes(doc, config)

# Versao scope-aware (filtra ITEMs do cardapio):
itens_ids = frozenset({'hamburguer', 'batata', 'coca'})
remocoes = capturar_remocoes_v2(doc, config, itens_ids)

capturar_remocoes(doc: Doc, config: ExtratorConfig) -> list[tuple[str, int]]

Captura itens a remover apos sinais como 'sem', 'tira', etc.

Parameters:

Name Type Description Default
doc Doc

Documento spaCy processado.

required
config ExtratorConfig

Configuracao com palavras de remocao, conectivos, etc.

required

Returns:

Type Description
list[tuple[str, int]]

Lista de tuplas (texto, indice_do_token).

Source code in src/extratores/remocoes.py
def capturar_remocoes(doc: Doc, config: ExtratorConfig) -> list[tuple[str, int]]:
    """Captura itens a remover apos sinais como 'sem', 'tira', etc.

    Args:
        doc: Documento spaCy processado.
        config: Configuracao com palavras de remocao, conectivos, etc.

    Returns:
        Lista de tuplas (texto, indice_do_token).
    """
    remocoes: list[tuple[str, int]] = []
    tokens = list(doc)
    indice = 0

    while indice < len(tokens):
        token = tokens[indice]

        if token.text.lower() not in config.palavras_remocao:
            indice += 1
            continue

        # Encontrou sinal de remocao, captura itens seguintes
        indice += 1
        while indice < len(tokens):
            token = tokens[indice]

            # Conectivos: decide se para ou continua
            if token.text.lower() in config.conectivos:
                if _deve_parar_no_conectivo(
                    tokens, indice, config.palavras_remocao, config.pos_ignoraveis
                ):
                    break
                indice += 1
                continue

            # Palavras de parada obrigatoria
            if token.text.lower() in config.palavras_parada:
                break

            # FILTRO: stop words que nunca sao remocoes
            if token.text.lower() in config.palavras_filtro_remocao:
                indice += 1
                continue

            # Ignora artigos/preposicoes
            if token.pos_ in config.pos_ignoraveis:
                indice += 1
                continue

            # Token relevante: adiciona as remocoes
            remocoes.append((token.text, token.i))
            indice += 1

    return remocoes

capturar_remocoes_v2(doc: Doc, config: ExtratorConfig, itens_ids: frozenset[str]) -> list[tuple[str, int]]

Captura remocoes filtrando tokens que sao ITEMs do cardapio.

Diferenca para capturar_remocoes(): filtra tokens cujo nome normalizado esta em itens_ids, evitando que nomes de lanches/bebidas/acompanhamentos sejam capturados como ingredientes a remover.

Parameters:

Name Type Description Default
doc Doc

Documento spaCy processado.

required
config ExtratorConfig

Configuracao com palavras de remocao, conectivos, etc.

required
itens_ids frozenset[str]

Set de nomes + aliases do cardapio (normalizados).

required

Returns:

Type Description
list[tuple[str, int]]

Lista de tuplas (texto, indice_do_token).

Source code in src/extratores/remocoes.py
def capturar_remocoes_v2(
    doc: Doc,
    config: ExtratorConfig,
    itens_ids: frozenset[str],
) -> list[tuple[str, int]]:
    """Captura remocoes filtrando tokens que sao ITEMs do cardapio.

    Diferenca para capturar_remocoes(): filtra tokens cujo nome normalizado
    esta em itens_ids, evitando que nomes de lanches/bebidas/acompanhamentos
    sejam capturados como ingredientes a remover.

    Args:
        doc: Documento spaCy processado.
        config: Configuracao com palavras de remocao, conectivos, etc.
        itens_ids: Set de nomes + aliases do cardapio (normalizados).

    Returns:
        Lista de tuplas (texto, indice_do_token).
    """
    from src.extratores.normalizador import normalizar_para_busca  # noqa: PLC0415

    remocoes: list[tuple[str, int]] = []
    tokens = list(doc)
    indice = 0

    while indice < len(tokens):
        token = tokens[indice]

        if token.text.lower() not in config.palavras_remocao:
            indice += 1
            continue

        # Encontrou sinal de remocao, captura itens seguintes
        indice += 1
        while indice < len(tokens):
            token = tokens[indice]

            # Conectivos: decide se para ou continua
            if token.text.lower() in config.conectivos:
                if _deve_parar_no_conectivo(
                    tokens, indice, config.palavras_remocao, config.pos_ignoraveis
                ):
                    break
                indice += 1
                continue

            # Palavras de parada obrigatoria
            if token.text.lower() in config.palavras_parada:
                break

            # FILTRO: stop words que nunca sao remocoes
            if token.text.lower() in config.palavras_filtro_remocao:
                indice += 1
                continue

            # Ignora artigos/preposicoes
            if token.pos_ in config.pos_ignoraveis:
                indice += 1
                continue

            # FILTRO CRITICO: se e ITEM do cardapio, nao e remocao
            if normalizar_para_busca(token.text) in itens_ids:
                indice += 1
                continue

            # Token relevante: adiciona as remocoes
            remocoes.append((token.text, token.i))
            indice += 1

    return remocoes

src.extratores.carrinho_extrator

Extrator de itens do carrinho para remocao.

Substitui extrair_item_carrinho() do modulo procedural.

Example
from src.extratores.carrinho_extrator import CarrinhoExtrator

extrator = CarrinhoExtrator(engine, config)
extrator.extrair('tira a coca', carrinho)

CarrinhoExtrator(engine: NlpEngine, config: ExtratorConfig)

Extrai itens do carrinho para remocao.

Inicializa o extrator de carrinho.

Parameters:

Name Type Description Default
engine NlpEngine

NlpEngine com modelo spaCy lazy-loaded.

required
config ExtratorConfig

Configuracao do extrator.

required
Source code in src/extratores/carrinho_extrator.py
def __init__(self, engine: NlpEngine, config: ExtratorConfig) -> None:
    """Inicializa o extrator de carrinho.

    Args:
        engine: NlpEngine com modelo spaCy lazy-loaded.
        config: Configuracao do extrator.
    """
    self._engine = engine
    self._config = config

extrair(mensagem: str, carrinho: list[dict]) -> list[MatchCarrinho]

Extrai itens do carrinho para remocao.

Parameters:

Name Type Description Default
mensagem str

Texto da mensagem do usuario.

required
carrinho list[dict]

Lista de itens no carrinho atual.

required

Returns:

Type Description
list[MatchCarrinho]

Lista de MatchCarrinho com item_id, variante e indices.

Source code in src/extratores/carrinho_extrator.py
def extrair(self, mensagem: str, carrinho: list[dict]) -> list[MatchCarrinho]:
    """Extrai itens do carrinho para remocao.

    Args:
        mensagem: Texto da mensagem do usuario.
        carrinho: Lista de itens no carrinho atual.

    Returns:
        Lista de MatchCarrinho com item_id, variante e indices.
    """
    if not mensagem or not mensagem.strip():
        return []

    if not carrinho:
        return []

    # Caso especial: "tira tudo"
    msg_norm = normalizar_para_busca(mensagem)
    if 'tira tudo' in msg_norm or 'remove tudo' in msg_norm:
        return [
            MatchCarrinho(
                item_id=item['item_id'],
                variante=item.get('variante'),
                indices=[i],
            )
            for i, item in enumerate(carrinho)
        ]

    # Extrair itens mencionados na mensagem
    doc = self._engine.processar(mensagem)
    itens_mencionados: list[ItemMencionado] = []

    for ent in doc.ents:
        if ent.label_ == 'ITEM':
            itens_mencionados.append(
                ItemMencionado(
                    texto=normalizar_para_busca(ent.text),
                    variante=None,
                    ent_id=ent.ent_id_,
                )
            )
        elif ent.label_ == 'VARIANTE':
            if itens_mencionados:
                ultimo = itens_mencionados[-1]
                itens_mencionados[-1] = ItemMencionado(
                    texto=ultimo.texto,
                    variante=normalizar_para_busca(ent.text),
                    ent_id=ultimo.ent_id,
                )
            else:
                itens_mencionados.append(
                    ItemMencionado(
                        texto='',
                        variante=normalizar_para_busca(ent.text),
                        ent_id=ent.ent_id_,
                    )
                )

    return _buscar_matches_no_carrinho(itens_mencionados, carrinho)

src.extratores.troca_extrator

Extrator de informacoes de troca de itens.

Substitui extrair_itens_troca() do modulo procedural.

Example
from src.extratores.troca_extrator import TrocaExtrator

extrator = TrocaExtrator(engine, config)
extrator.extrair('muda pra lata', carrinho)

TrocaExtrator(engine: NlpEngine, config: ExtratorConfig)

Extrai informacoes de troca da mensagem.

Inicializa o extrator de troca.

Parameters:

Name Type Description Default
engine NlpEngine

NlpEngine com modelo spaCy lazy-loaded.

required
config ExtratorConfig

Configuracao do extrator.

required
Source code in src/extratores/troca_extrator.py
def __init__(self, engine: NlpEngine, config: ExtratorConfig) -> None:
    """Inicializa o extrator de troca.

    Args:
        engine: NlpEngine com modelo spaCy lazy-loaded.
        config: Configuracao do extrator.
    """
    self._engine = engine
    self._config = config

extrair(mensagem: str, carrinho: list[dict]) -> ExtracaoTroca

Extrai informacoes de troca da mensagem.

Classifica em casos: - 'A': 2+ ITEMs (troca item por item) - 'B': 1 ITEM (com ou sem variante, busca no carrinho) - 'C': 0 ITEMs + 1 VARIANTE isolada - 'vazio': nenhuma entidade relevante encontrada

Parameters:

Name Type Description Default
mensagem str

Mensagem do usuario.

required
carrinho list[dict]

Carrinho atual.

required

Returns:

Type Description
ExtracaoTroca

ExtracaoTroca com caso, item_original e variante_nova.

Source code in src/extratores/troca_extrator.py
def extrair(self, mensagem: str, carrinho: list[dict]) -> ExtracaoTroca:
    """Extrai informacoes de troca da mensagem.

    Classifica em casos:
    - 'A': 2+ ITEMs (troca item por item)
    - 'B': 1 ITEM (com ou sem variante, busca no carrinho)
    - 'C': 0 ITEMs + 1 VARIANTE isolada
    - 'vazio': nenhuma entidade relevante encontrada

    Args:
        mensagem: Mensagem do usuario.
        carrinho: Carrinho atual.

    Returns:
        ExtracaoTroca com caso, item_original e variante_nova.
    """
    if not mensagem or not mensagem.strip():
        return ExtracaoTroca(caso='vazio', item_original=None, variante_nova=None)

    doc = self._engine.processar(mensagem)

    # Coletar entidades em ordem
    itens_mencionados: list[ItemMencionado] = []
    variantes_sozinhas: list[str] = []

    for ent in doc.ents:
        if ent.label_ == 'ITEM':
            itens_mencionados.append(
                ItemMencionado(
                    texto=normalizar_para_busca(ent.text),
                    variante=None,
                    ent_id=ent.ent_id_,
                )
            )
        elif ent.label_ == 'VARIANTE':
            # Se tem ITEM antes, associa a ele
            if itens_mencionados:
                ultimo = itens_mencionados[-1]
                itens_mencionados[-1] = ItemMencionado(
                    texto=ultimo.texto,
                    variante=normalizar_para_busca(ent.text),
                    ent_id=ultimo.ent_id,
                )
            else:
                # Variante sem ITEM = variante isolada (caso C)
                variantes_sozinhas.append(normalizar_para_busca(ent.text))

    # Classificar caso
    num_items = len(itens_mencionados)
    num_variantes = len(variantes_sozinhas)

    # Caso A: 2+ ITEMs (troca item por item)
    if num_items >= 2:
        return ExtracaoTroca(caso='A', item_original=None, variante_nova=None)

    # Caso B: 1 ITEM (com ou sem variante)
    if num_items == 1:
        return self._processar_caso_b(itens_mencionados[0], carrinho, mensagem)

    # Caso C: 0 ITEMs + 1 VARIANTE isolada
    if num_items == 0 and num_variantes == 1:
        return ExtracaoTroca(
            caso='C', item_original=None, variante_nova=variantes_sozinhas[0]
        )

    # Fallback fuzzy: se nada foi extrai do, tentar fuzzy match
    return self._fallback_fuzzy_completo(mensagem, carrinho)

src.extratores.fuzzy_extrator

Fuzzy matching para fallback do extrator spaCy.

Funcoes puras para correcao de typos em nomes de itens e variantes do cardapio usando rapidfuzz.

Example
from src.extratores.fuzzy_extrator import fuzzy_match_item

aliases = {'hamburguer': 'lanche_001', 'coca': 'bebida_001'}
fuzzy_match_item('amburguer', aliases)
('hamburguer', 94.7, 'lanche_001')

match_variante_numerica(typo: str, variantes: list[str]) -> str | None

Resolve typos em variantes numericas do tipo NNNml.

Usa substring matching: se o numero do typo e substring do numero da variante, e um match. Ex: '50' em '500' → match.

Parameters:

Name Type Description Default
typo str

Texto digitado pelo usuario.

required
variantes list[str]

Lista de variantes validas do cardapio.

required

Returns:

Type Description
str | None

Variante matchada, ou None se ambiguo ou sem match.

Example
match_variante_numerica('50ml', ['300ml', '500ml'])
'500ml'
match_variante_numerica('50ml', ['350ml', '500ml'])
None  # ambiguo: 50 esta em ambos
Source code in src/extratores/fuzzy_extrator.py
def match_variante_numerica(typo: str, variantes: list[str]) -> str | None:
    """Resolve typos em variantes numericas do tipo NNNml.

    Usa substring matching: se o numero do typo e substring do numero
    da variante, e um match. Ex: '50' em '500' → match.

    Args:
        typo: Texto digitado pelo usuario.
        variantes: Lista de variantes validas do cardapio.

    Returns:
        Variante matchada, ou None se ambiguo ou sem match.

    Example:
        ```python
        match_variante_numerica('50ml', ['300ml', '500ml'])
        '500ml'
        match_variante_numerica('50ml', ['350ml', '500ml'])
        None  # ambiguo: 50 esta em ambos
        ```
    """
    typo_n = normalizar_para_fuzzy(typo)
    match_ml = re.match(r'^(\d+)ml$', typo_n)
    if not match_ml:
        return None
    typo_num = match_ml.group(1)
    candidatos = []
    for var in variantes:
        var_n = normalizar_para_fuzzy(var)
        var_match = re.match(r'^(\d+)ml$', var_n)
        if var_match and typo_num in var_match.group(1):
            candidatos.append(var)
    return candidatos[0] if len(candidatos) == 1 else None

extrair_tokens_significativos(texto: str) -> list[str]

Remove stop words e retorna tokens relevantes.

Parameters:

Name Type Description Default
texto str

Texto da mensagem do usuario.

required

Returns:

Type Description
list[str]

Lista de tokens significativos.

Example
extrair_tokens_significativos('quero um hamburguer sem cebola')
['hamburguer', 'sem', 'cebola']
Source code in src/extratores/fuzzy_extrator.py
def extrair_tokens_significativos(texto: str) -> list[str]:
    """Remove stop words e retorna tokens relevantes.

    Args:
        texto: Texto da mensagem do usuario.

    Returns:
        Lista de tokens significativos.

    Example:
        ```python
        extrair_tokens_significativos('quero um hamburguer sem cebola')
        ['hamburguer', 'sem', 'cebola']
        ```
    """
    texto_n = normalizar_para_fuzzy(texto)
    texto_n = re.sub(r'[^\w\s]', ' ', texto_n)
    tokens = texto_n.split()
    config = get_extrator_config()
    return [t for t in tokens if t not in config.stop_words and len(t) > 1]

fuzzy_match_item(texto: str, alias_para_id: dict[str, str], cutoff: int | None = None) -> tuple[str | None, float, str | None]

Fuzzy match de texto contra aliases do cardapio.

Parameters:

Name Type Description Default
texto str

Texto digitado pelo usuario.

required
alias_para_id dict[str, str]

Mapeamento alias → item_id.

required
cutoff int | None

Score minimo (0-100). Usa config se None.

None

Returns:

Type Description
tuple[str | None, float, str | None]

Tupla (alias_match, score, item_id) ou (None, 0, None).

Source code in src/extratores/fuzzy_extrator.py
def fuzzy_match_item(
    texto: str,
    alias_para_id: dict[str, str],
    cutoff: int | None = None,
) -> tuple[str | None, float, str | None]:
    """Fuzzy match de texto contra aliases do cardapio.

    Args:
        texto: Texto digitado pelo usuario.
        alias_para_id: Mapeamento alias → item_id.
        cutoff: Score minimo (0-100). Usa config se None.

    Returns:
        Tupla (alias_match, score, item_id) ou (None, 0, None).
    """
    config = get_extrator_config()
    cutoff = cutoff if cutoff is not None else config.fuzzy_item_cutoff

    tokens = extrair_tokens_significativos(texto)
    if not tokens:
        return None, 0, None

    candidatos = list(alias_para_id.keys())
    melhor_alias: str | None = None
    melhor_score = 0.0

    for token in [*tokens, normalizar_para_fuzzy(texto)]:
        resultado = process.extractOne(
            token, candidatos, scorer=fuzz.ratio, score_cutoff=cutoff
        )
        if resultado and resultado[1] > melhor_score:
            melhor_alias = resultado[0]
            melhor_score = resultado[1]

    if melhor_alias:
        return melhor_alias, melhor_score, alias_para_id.get(melhor_alias)
    return None, 0, None

fuzzy_match_variante(texto: str, variantes: list[str], cutoff: int | None = None) -> tuple[str | None, float]

Fuzzy match de texto contra variantes validas.

Usa match_variante_numerica primeiro para variantes do tipo NNNml. Depois tenta fuzzy normal com deteccao de ambiguidade.

Parameters:

Name Type Description Default
texto str

Texto digitado pelo usuario.

required
variantes list[str]

Lista de variantes validas do cardapio.

required
cutoff int | None

Score minimo (0-100). Usa config se None.

None

Returns:

Type Description
tuple[str | None, float]

Tupla (variante_match, score) ou (None, 0).

Source code in src/extratores/fuzzy_extrator.py
def fuzzy_match_variante(
    texto: str,
    variantes: list[str],
    cutoff: int | None = None,
) -> tuple[str | None, float]:
    """Fuzzy match de texto contra variantes validas.

    Usa match_variante_numerica primeiro para variantes do tipo NNNml.
    Depois tenta fuzzy normal com deteccao de ambiguidade.

    Args:
        texto: Texto digitado pelo usuario.
        variantes: Lista de variantes validas do cardapio.
        cutoff: Score minimo (0-100). Usa config se None.

    Returns:
        Tupla (variante_match, score) ou (None, 0).
    """
    if not texto or not texto.strip():
        return None, 0

    config = get_extrator_config()
    cutoff = cutoff if cutoff is not None else config.fuzzy_variante_cutoff

    texto_n = normalizar_para_fuzzy(texto)

    # Tentar matching numerico primeiro
    resultado_num = match_variante_numerica(texto_n, variantes)
    if resultado_num:
        return resultado_num, 95.0

    # Fuzzy normal
    resultado = process.extractOne(
        texto_n, variantes, scorer=fuzz.ratio, score_cutoff=cutoff
    )
    if not resultado:
        return None, 0

    # Verificar ambiguidade: se top-2 estao dentro de AMBIGUIDADE_LIMITE pontos
    todos_matches = process.extract(
        texto_n, variantes, scorer=fuzz.ratio, score_cutoff=cutoff, limit=2
    )
    if len(todos_matches) >= 2:
        _, score1, _ = resultado
        _, score2, _ = todos_matches[1]
        if score1 - score2 < config.ambiguidade_limite:
            return None, 0  # ambiguo, melhor nao arriscar

    return resultado[0], resultado[1]

extrair_item_fuzzy(mensagem: str, quantidade: int = 1) -> list[ItemExtraido]

Fallback fuzzy completo quando EntityRuler nao encontra itens.

Tenta match do item via fuzzy e extrai variante dos tokens significativos da mensagem.

Parameters:

Name Type Description Default
mensagem str

Mensagem original do usuario.

required
quantidade int

Quantidade extraida de outra fonte (ou 1).

1

Returns:

Type Description
list[ItemExtraido]

Lista com 0 ou 1 ItemExtraido encontrado via fuzzy.

Example
extrair_item_fuzzy('quero 3 hanburgers duplos')
[ItemExtraido(item_id='lanche_001', quantidade=3, variante='duplo')]
Source code in src/extratores/fuzzy_extrator.py
def extrair_item_fuzzy(
    mensagem: str,
    quantidade: int = 1,
) -> list[ItemExtraido]:
    """Fallback fuzzy completo quando EntityRuler nao encontra itens.

    Tenta match do item via fuzzy e extrai variante dos tokens
    significativos da mensagem.

    Args:
        mensagem: Mensagem original do usuario.
        quantidade: Quantidade extraida de outra fonte (ou 1).

    Returns:
        Lista com 0 ou 1 ItemExtraido encontrado via fuzzy.

    Example:
        ```python
        extrair_item_fuzzy('quero 3 hanburgers duplos')
        [ItemExtraido(item_id='lanche_001', quantidade=3, variante='duplo')]
        ```
    """
    from src.config import get_cardapio  # noqa: PLC0415 — lazy loading

    cardapio = get_cardapio()
    alias_para_id: dict[str, str] = {}
    for item in cardapio.get('itens', []):
        alias_para_id[item['nome'].lower()] = item['id']
        for alias in item.get('aliases', []):
            alias_para_id[alias.lower()] = item['id']

    alias, _score, item_id = fuzzy_match_item(mensagem, alias_para_id)
    if item_id is None:
        return []

    # Tenta extrair variante dos tokens significativos,
    # filtrando o token do item (mesmo com typo).
    item_cfg = next((i for i in cardapio.get('itens', []) if i['id'] == item_id), None)
    variante = None
    if item_cfg and item_cfg.get('variantes'):
        variantes = [v['opcao'] for v in item_cfg['variantes']]

        alias_norm = normalizar_para_busca(alias or '')

        def _similar_ao_alias(token: str) -> bool:
            return fuzz.ratio(normalizar_para_busca(token), alias_norm) >= 70

        tokens = extrair_tokens_significativos(mensagem)
        candidatos = [
            t
            for t in tokens
            if not t.isdigit()
            and t not in {'um', 'uma', 'dois', 'duas', 'tres'}
            and not _similar_ao_alias(t)
        ]
        # Melhor score entre todos os candidatos
        melhor_var: str | None = None
        melhor_score = 0.0
        for t in candidatos:
            var_match, var_score = fuzzy_match_variante(t, variantes)
            if var_match and var_score > melhor_score:
                melhor_var = var_match
                melhor_score = var_score
        variante = melhor_var

    return [
        ItemExtraido(
            item_id=item_id,
            quantidade=quantidade,
            variante=variante,
            remocoes=[],
        )
    ]

normalizar(texto: str) -> str

Normaliza para fuzzy matching.

Aplica lowercase e normalizacao Unicode (remove acentos). Preserva pontuacao interna e espacos.

Parameters:

Name Type Description Default
texto str

Texto original.

required

Returns:

Type Description
str

Texto normalizado em minusculas sem acentos.

Example
normalizar_para_fuzzy('Hambúrguer!')
'hamburguer!'
Source code in src/extratores/normalizador.py
def normalizar_para_fuzzy(texto: str) -> str:
    """Normaliza para fuzzy matching.

    Aplica lowercase e normalizacao Unicode (remove acentos).
    Preserva pontuacao interna e espacos.

    Args:
        texto: Texto original.

    Returns:
        Texto normalizado em minusculas sem acentos.

    Example:
        ```python
        normalizar_para_fuzzy('Hambúrguer!')
        'hamburguer!'
        ```
    """
    texto = texto.lower()
    texto = unicodedata.normalize('NFKD', texto)
    texto = texto.encode('ascii', 'ignore').decode('utf-8')
    return texto.strip()