Ir para o conteúdo

Graph

src.graph

Grafo de fluxo de atendimento do Pede AI.

Define o estado compartilhado e os nós de processamento para o grafo de atendimento (LangGraph).

Example
from src.graph import State, MODOS, ACOES
from src.graph import node_verificar_modo, node_handler_saudacao
from src.graph import criar_graph

MODOS = Literal['ocioso', 'coletando', 'clarificando', 'confirmando', 'finalizado'] module-attribute

Literal com todos os modos validos do fluxo de atendimento.

Renomeado de 'ETAPAS' para 'MODOS' no dispatcher. 'clarificando_variante' → 'clarificando'.

ACOES = Literal['adicionar_item', 'remover_item', 'trocar_variante', 'sem_entidade'] module-attribute

Literal com todas as acoes validas do dispatcher.

State

Bases: TypedDict

Estado compartilhado entre os nos do grafo de atendimento.

Attributes:

Name Type Description
mensagem_atual str

Ultima mensagem recebida do usuario.

intent str

Intencao classificada da mensagem atual.

confidence float

Confidence da classificacao (0-1).

itens_extraidos list

Lista de dicts de itens extraidos da mensagem.

carrinho list

Lista de dicts de itens adicionados ao pedido.

fila_clarificacao list

Fila de dicts de itens que precisam de clarificacao.

modo MODOS

Modo atual do fluxo de atendimento (renomeado de 'etapa').

resposta str

Resposta gerada para o usuario.

tentativas_clarificacao int

Contador de tentativas para o item atual.

acao ACOES

Decisao do dispatcher (novo).

origem_intent str

Origem da classificacao: 'contexto'|'lookup'|'rag_forte'|'llm_rag'|'llm_fixo' (novo).

dados_extracao dict

Output de extrair_itens_troca() ou carrinho matches (novo).

criar_graph(checkpointer: SqliteSaver, classificador: ClassificadorIntencoes) -> StateGraph

Constroi e compila o grafo de atendimento.

Parameters:

Name Type Description Default
checkpointer SqliteSaver

Checkpointer para persistencia de estado (SqliteSaver).

required
classificador ClassificadorIntencoes

Instancia de ClassificadorIntencoes injetada.

required

Returns:

Type Description
StateGraph

Grafo compilado pronto para uso com invoke().

Source code in src/graph/builder.py
def criar_graph(
    checkpointer: SqliteSaver,
    classificador: ClassificadorIntencoes,
) -> StateGraph:
    """Constroi e compila o grafo de atendimento.

    Args:
        checkpointer: Checkpointer para persistencia de estado (SqliteSaver).
        classificador: Instancia de ClassificadorIntencoes injetada.

    Returns:
        Grafo compilado pronto para uso com invoke().
    """
    node_router = _criar_node_router(classificador)
    builder = StateGraph(State)

    # 1. registra nodes
    builder.add_node('verificar_modo', node_verificar_modo)
    builder.add_node('resolver_contexto', node_resolver_contexto)
    builder.add_node('router', node_router)
    builder.add_node('extrator', node_extrator)
    builder.add_node('dispatcher_modificar', node_dispatcher_modificar)
    builder.add_node('clarificacao', node_clarificacao)
    builder.add_node('handler_pedir', node_handler_pedir)
    builder.add_node('handler_adicionar', node_handler_adicionar)
    builder.add_node('handler_saudacao', node_handler_saudacao)
    builder.add_node('handler_carrinho', node_handler_carrinho)
    builder.add_node('handler_confirmar', node_handler_confirmar)
    builder.add_node('handler_cancelar', node_handler_cancelar)
    builder.add_node('handler_remover', node_handler_remover)
    builder.add_node('handler_trocar', node_handler_trocar)
    builder.add_node('handler_desconhecido', node_handler_desconhecido)

    # 2. entry point + edge condicional de entrada
    builder.set_entry_point('verificar_modo')
    builder.add_conditional_edges(
        'verificar_modo',
        _decidir_entrada,
        {'clarificacao': 'clarificacao', 'resolver_contexto': 'resolver_contexto'},
    )

    # 3. resolver_contexto → handler direto ou router
    builder.add_conditional_edges(
        'resolver_contexto',
        _decidir_por_intent,
        {**{v: v for v in _INTENT_TO_NODE.values()}, 'router': 'router'},
    )

    # 4. edge condicional por intent (derivado do mapping centralizado)
    handler_destinos = {v: v for v in _INTENT_TO_NODE.values()}
    builder.add_conditional_edges(
        'router',
        _decidir_por_intent,
        handler_destinos,  # type: ignore[arg-type]
    )

    # 5. dispatcher → handlers por ação
    builder.add_conditional_edges(
        'dispatcher_modificar',
        _decidir_por_acao,
        {v: v for v in _ACAO_TO_NODE.values()},
    )

    # 6. edges simples
    builder.add_edge('extrator', 'handler_pedir')
    for node_name in _HANDLER_NODES:
        builder.add_edge(node_name, END)
    builder.add_edge('clarificacao', END)

    # 7. compila
    return builder.compile(checkpointer=checkpointer)  # pyright: ignore[reportReturnType]

node_clarificacao(state: State) -> RetornoNode

Processa resposta do usuario durante clarificacao de variante.

Source code in src/graph/nodes.py
def node_clarificacao(state: State) -> RetornoNode:
    """Processa resposta do usuario durante clarificacao de variante."""
    thread_id = _get_thread_id()
    resultado = clarificar(
        fila=state.get('fila_clarificacao', []),
        mensagem=state.get('mensagem_atual', ''),
        tentativas=state.get('tentativas_clarificacao', 0),
        thread_id=thread_id,
    )
    carrinho_atualizado = state.get('carrinho', []) + resultado.carrinho
    return {
        'carrinho': carrinho_atualizado,
        'fila_clarificacao': resultado.fila,
        'resposta': resultado.resposta,
        'modo': resultado.modo,
    }

node_extrator(state: State) -> RetornoNode

Extrai itens do cardapio da mensagem do usuario.

Source code in src/graph/nodes.py
def node_extrator(state: State) -> RetornoNode:
    """Extrai itens do cardapio da mensagem do usuario."""
    if state.get('intent') == 'pedir':
        mensagem = state.get('mensagem_atual', '')
        inicio = time.monotonic()
        itens = extrair(mensagem)
        tempo_ms = (time.monotonic() - inicio) * 1000

        ext_logger = get_extracao_logger()
        if ext_logger:
            ext_logger.registrar(
                thread_id=_get_thread_id(),
                mensagem=mensagem,
                itens_extraidos=itens,
                tempo_ms=tempo_ms,
            )
        return {'itens_extraidos': itens}
    return {'itens_extraidos': []}

node_handler_cancelar(state: State) -> RetornoNode

Processa cancelamento do pedido.

Source code in src/graph/nodes.py
def node_handler_cancelar(state: State) -> RetornoNode:
    """Processa cancelamento do pedido."""
    carrinho = state.get('carrinho', [])
    return processar_cancelamento(carrinho)

node_handler_carrinho(state: State) -> RetornoNode

Gera resposta com o conteudo atual do carrinho.

Source code in src/graph/nodes.py
def node_handler_carrinho(state: State) -> RetornoNode:
    """Gera resposta com o conteudo atual do carrinho."""
    carrinho = state.get('carrinho', [])
    return processar_carrinho(carrinho)

node_handler_confirmar(state: State) -> RetornoNode

Processa confirmacao do pedido pelo usuario.

Source code in src/graph/nodes.py
def node_handler_confirmar(state: State) -> RetornoNode:
    """Processa confirmacao do pedido pelo usuario."""
    carrinho = state.get('carrinho', [])
    return processar_confirmacao(carrinho)

node_handler_pedir(state: State) -> RetornoNode

Processa itens extraidos e os adiciona ao carrinho.

Source code in src/graph/nodes.py
def node_handler_pedir(state: State) -> RetornoNode:
    """Processa itens extraidos e os adiciona ao carrinho."""
    itens_extraidos = state.get('itens_extraidos') or []
    carrinho = state.get('carrinho', [])
    resultado = processar_pedido(itens_extraidos, carrinho)
    return resultado.to_dict()

node_handler_saudacao(state: State) -> RetornoNode

Gera resposta de saudacao com o nome do restaurante.

Source code in src/graph/nodes.py
def node_handler_saudacao(state: State) -> RetornoNode:
    """Gera resposta de saudacao com o nome do restaurante."""
    return processar_saudacao()

node_router(state: State) -> RetornoNode

Classifica a intencao da mensagem e atualiza o estado.

Versao standalone para testes — usa _classificar_intencao que pode ser mockado.

Source code in src/graph/nodes.py
def node_router(state: State) -> RetornoNode:
    """Classifica a intencao da mensagem e atualiza o estado.

    Versao standalone para testes — usa _classificar_intencao
    que pode ser mockado.
    """
    mensagem = state.get('mensagem_atual', '')
    thread_id = _get_thread_id()
    inicio = time.monotonic()
    modo_anterior = state.get('modo', 'ocioso')

    resultado = _classificar_intencao(mensagem, thread_id=thread_id)

    obs_logger = get_obs_logger()
    obs_logger.registrar(
        thread_id=thread_id,
        mensagem=mensagem,
        mensagem_norm=resultado['mensagem_norm'],
        intent=resultado['intent'],
        confidence=resultado['confidence'],
        caminho=resultado['caminho'],
        top1_texto=resultado['top1_texto'],
        top1_intencao=resultado['top1_intencao'],
    )

    funil_logger = get_funil_logger()
    if funil_logger:
        funil_logger.registrar(
            thread_id=thread_id,
            modo_anterior=modo_anterior,
            modo_atual='roteado',
            intent=resultado['intent'],
            carrinho_size=len(state.get('carrinho', [])),
        )

    tempo_ms = (time.monotonic() - inicio) * 1000

    handler_logger = get_handler_logger()
    if handler_logger:
        handler_logger.registrar(
            thread_id=thread_id,
            handler='node_router',
            intent=resultado['intent'],
            input_dados={'mensagem': mensagem},
            output_dados=resultado,
            tempo_ms=tempo_ms,
        )

    return {
        'intent': resultado['intent'],
        'confidence': resultado['confidence'],
    }

node_verificar_modo(state: State) -> RetornoNode

No de verificacao de modo do fluxo.

Apenas passa adiante sem modificar o estado. A decisao de qual caminho seguir e feita pela edge condicional _decidir_entrada no builder.

Source code in src/graph/nodes.py
def node_verificar_modo(state: State) -> RetornoNode:
    """No de verificacao de modo do fluxo.

    Apenas passa adiante sem modificar o estado. A decisao
    de qual caminho seguir e feita pela edge condicional
    ``_decidir_entrada`` no builder.
    """
    return {}

src.graph.builder

Builder do grafo LangGraph.

Constroi e compila o grafo de atendimento com nodes, arestas condicionais e roteamento por intent.

Example
from langgraph.checkpoint.sqlite import SqliteSaver
from src.graph.builder import criar_graph
import sqlite3

conn = sqlite3.connect(':memory:')
checkpointer = SqliteSaver(conn)
graph = criar_graph(checkpointer)

criar_graph(checkpointer: SqliteSaver, classificador: ClassificadorIntencoes) -> StateGraph

Constroi e compila o grafo de atendimento.

Parameters:

Name Type Description Default
checkpointer SqliteSaver

Checkpointer para persistencia de estado (SqliteSaver).

required
classificador ClassificadorIntencoes

Instancia de ClassificadorIntencoes injetada.

required

Returns:

Type Description
StateGraph

Grafo compilado pronto para uso com invoke().

Source code in src/graph/builder.py
def criar_graph(
    checkpointer: SqliteSaver,
    classificador: ClassificadorIntencoes,
) -> StateGraph:
    """Constroi e compila o grafo de atendimento.

    Args:
        checkpointer: Checkpointer para persistencia de estado (SqliteSaver).
        classificador: Instancia de ClassificadorIntencoes injetada.

    Returns:
        Grafo compilado pronto para uso com invoke().
    """
    node_router = _criar_node_router(classificador)
    builder = StateGraph(State)

    # 1. registra nodes
    builder.add_node('verificar_modo', node_verificar_modo)
    builder.add_node('resolver_contexto', node_resolver_contexto)
    builder.add_node('router', node_router)
    builder.add_node('extrator', node_extrator)
    builder.add_node('dispatcher_modificar', node_dispatcher_modificar)
    builder.add_node('clarificacao', node_clarificacao)
    builder.add_node('handler_pedir', node_handler_pedir)
    builder.add_node('handler_adicionar', node_handler_adicionar)
    builder.add_node('handler_saudacao', node_handler_saudacao)
    builder.add_node('handler_carrinho', node_handler_carrinho)
    builder.add_node('handler_confirmar', node_handler_confirmar)
    builder.add_node('handler_cancelar', node_handler_cancelar)
    builder.add_node('handler_remover', node_handler_remover)
    builder.add_node('handler_trocar', node_handler_trocar)
    builder.add_node('handler_desconhecido', node_handler_desconhecido)

    # 2. entry point + edge condicional de entrada
    builder.set_entry_point('verificar_modo')
    builder.add_conditional_edges(
        'verificar_modo',
        _decidir_entrada,
        {'clarificacao': 'clarificacao', 'resolver_contexto': 'resolver_contexto'},
    )

    # 3. resolver_contexto → handler direto ou router
    builder.add_conditional_edges(
        'resolver_contexto',
        _decidir_por_intent,
        {**{v: v for v in _INTENT_TO_NODE.values()}, 'router': 'router'},
    )

    # 4. edge condicional por intent (derivado do mapping centralizado)
    handler_destinos = {v: v for v in _INTENT_TO_NODE.values()}
    builder.add_conditional_edges(
        'router',
        _decidir_por_intent,
        handler_destinos,  # type: ignore[arg-type]
    )

    # 5. dispatcher → handlers por ação
    builder.add_conditional_edges(
        'dispatcher_modificar',
        _decidir_por_acao,
        {v: v for v in _ACAO_TO_NODE.values()},
    )

    # 6. edges simples
    builder.add_edge('extrator', 'handler_pedir')
    for node_name in _HANDLER_NODES:
        builder.add_edge(node_name, END)
    builder.add_edge('clarificacao', END)

    # 7. compila
    return builder.compile(checkpointer=checkpointer)  # pyright: ignore[reportReturnType]

src.graph.state

Estado compartilhado do grafo de atendimento.

Define o TypedDict State utilizado por todos os nos do grafo LangGraph para compartilhar informacoes durante o fluxo de atendimento.

Example
from src.graph.state import State, MODOS, ACOES, RetornoNode

state: State = {
    'mensagem_atual': '',
    'intent': '',
    'itens_extraidos': [],
    'carrinho': [],
    'fila_clarificacao': [],
    'modo': 'ocioso',
    'resposta': '',
    'acao': 'adicionar_item',
    'origem_intent': '',
    'dados_extracao': {},
}

MODOS = Literal['ocioso', 'coletando', 'clarificando', 'confirmando', 'finalizado'] module-attribute

Literal com todos os modos validos do fluxo de atendimento.

Renomeado de 'ETAPAS' para 'MODOS' no dispatcher. 'clarificando_variante' → 'clarificando'.

ACOES = Literal['adicionar_item', 'remover_item', 'trocar_variante', 'sem_entidade'] module-attribute

Literal com todas as acoes validas do dispatcher.

State

Bases: TypedDict

Estado compartilhado entre os nos do grafo de atendimento.

Attributes:

Name Type Description
mensagem_atual str

Ultima mensagem recebida do usuario.

intent str

Intencao classificada da mensagem atual.

confidence float

Confidence da classificacao (0-1).

itens_extraidos list

Lista de dicts de itens extraidos da mensagem.

carrinho list

Lista de dicts de itens adicionados ao pedido.

fila_clarificacao list

Fila de dicts de itens que precisam de clarificacao.

modo MODOS

Modo atual do fluxo de atendimento (renomeado de 'etapa').

resposta str

Resposta gerada para o usuario.

tentativas_clarificacao int

Contador de tentativas para o item atual.

acao ACOES

Decisao do dispatcher (novo).

origem_intent str

Origem da classificacao: 'contexto'|'lookup'|'rag_forte'|'llm_rag'|'llm_fixo' (novo).

dados_extracao dict

Output de extrair_itens_troca() ou carrinho matches (novo).

RetornoNode

Bases: TypedDict

Tipo de retorno parcial dos nos do grafo.

Cada no retorna apenas as chaves que atualiza. O LangGraph faz o merge com o State completo.

Campos identicos ao State — mantidos aqui para type safety com total=False (todos opcionais).

src.graph.nodes

Nos de processamento do grafo de atendimento.

Cada funcao representa um no no grafo LangGraph, recebendo e retornando atualizacoes parciais do estado.

Example
from src.graph.nodes import node_handler_saudacao

state = {
    'mensagem_atual': 'oi',
    'intent': 'saudacao',
    'itens_extraidos': [],
    'carrinho': [],
    'fila_clarificacao': [],
    'modo': 'ocioso',
    'resposta': '',
}
result = node_handler_saudacao(state)
'resposta' in result
True

node_router(state: State) -> RetornoNode

Classifica a intencao da mensagem e atualiza o estado.

Versao standalone para testes — usa _classificar_intencao que pode ser mockado.

Source code in src/graph/nodes.py
def node_router(state: State) -> RetornoNode:
    """Classifica a intencao da mensagem e atualiza o estado.

    Versao standalone para testes — usa _classificar_intencao
    que pode ser mockado.
    """
    mensagem = state.get('mensagem_atual', '')
    thread_id = _get_thread_id()
    inicio = time.monotonic()
    modo_anterior = state.get('modo', 'ocioso')

    resultado = _classificar_intencao(mensagem, thread_id=thread_id)

    obs_logger = get_obs_logger()
    obs_logger.registrar(
        thread_id=thread_id,
        mensagem=mensagem,
        mensagem_norm=resultado['mensagem_norm'],
        intent=resultado['intent'],
        confidence=resultado['confidence'],
        caminho=resultado['caminho'],
        top1_texto=resultado['top1_texto'],
        top1_intencao=resultado['top1_intencao'],
    )

    funil_logger = get_funil_logger()
    if funil_logger:
        funil_logger.registrar(
            thread_id=thread_id,
            modo_anterior=modo_anterior,
            modo_atual='roteado',
            intent=resultado['intent'],
            carrinho_size=len(state.get('carrinho', [])),
        )

    tempo_ms = (time.monotonic() - inicio) * 1000

    handler_logger = get_handler_logger()
    if handler_logger:
        handler_logger.registrar(
            thread_id=thread_id,
            handler='node_router',
            intent=resultado['intent'],
            input_dados={'mensagem': mensagem},
            output_dados=resultado,
            tempo_ms=tempo_ms,
        )

    return {
        'intent': resultado['intent'],
        'confidence': resultado['confidence'],
    }

node_verificar_modo(state: State) -> RetornoNode

No de verificacao de modo do fluxo.

Apenas passa adiante sem modificar o estado. A decisao de qual caminho seguir e feita pela edge condicional _decidir_entrada no builder.

Source code in src/graph/nodes.py
def node_verificar_modo(state: State) -> RetornoNode:
    """No de verificacao de modo do fluxo.

    Apenas passa adiante sem modificar o estado. A decisao
    de qual caminho seguir e feita pela edge condicional
    ``_decidir_entrada`` no builder.
    """
    return {}

node_clarificacao(state: State) -> RetornoNode

Processa resposta do usuario durante clarificacao de variante.

Source code in src/graph/nodes.py
def node_clarificacao(state: State) -> RetornoNode:
    """Processa resposta do usuario durante clarificacao de variante."""
    thread_id = _get_thread_id()
    resultado = clarificar(
        fila=state.get('fila_clarificacao', []),
        mensagem=state.get('mensagem_atual', ''),
        tentativas=state.get('tentativas_clarificacao', 0),
        thread_id=thread_id,
    )
    carrinho_atualizado = state.get('carrinho', []) + resultado.carrinho
    return {
        'carrinho': carrinho_atualizado,
        'fila_clarificacao': resultado.fila,
        'resposta': resultado.resposta,
        'modo': resultado.modo,
    }

node_extrator(state: State) -> RetornoNode

Extrai itens do cardapio da mensagem do usuario.

Source code in src/graph/nodes.py
def node_extrator(state: State) -> RetornoNode:
    """Extrai itens do cardapio da mensagem do usuario."""
    if state.get('intent') == 'pedir':
        mensagem = state.get('mensagem_atual', '')
        inicio = time.monotonic()
        itens = extrair(mensagem)
        tempo_ms = (time.monotonic() - inicio) * 1000

        ext_logger = get_extracao_logger()
        if ext_logger:
            ext_logger.registrar(
                thread_id=_get_thread_id(),
                mensagem=mensagem,
                itens_extraidos=itens,
                tempo_ms=tempo_ms,
            )
        return {'itens_extraidos': itens}
    return {'itens_extraidos': []}

node_handler_pedir(state: State) -> RetornoNode

Processa itens extraidos e os adiciona ao carrinho.

Source code in src/graph/nodes.py
def node_handler_pedir(state: State) -> RetornoNode:
    """Processa itens extraidos e os adiciona ao carrinho."""
    itens_extraidos = state.get('itens_extraidos') or []
    carrinho = state.get('carrinho', [])
    resultado = processar_pedido(itens_extraidos, carrinho)
    return resultado.to_dict()

node_handler_saudacao(state: State) -> RetornoNode

Gera resposta de saudacao com o nome do restaurante.

Source code in src/graph/nodes.py
def node_handler_saudacao(state: State) -> RetornoNode:
    """Gera resposta de saudacao com o nome do restaurante."""
    return processar_saudacao()

node_handler_carrinho(state: State) -> RetornoNode

Gera resposta com o conteudo atual do carrinho.

Source code in src/graph/nodes.py
def node_handler_carrinho(state: State) -> RetornoNode:
    """Gera resposta com o conteudo atual do carrinho."""
    carrinho = state.get('carrinho', [])
    return processar_carrinho(carrinho)

node_handler_confirmar(state: State) -> RetornoNode

Processa confirmacao do pedido pelo usuario.

Source code in src/graph/nodes.py
def node_handler_confirmar(state: State) -> RetornoNode:
    """Processa confirmacao do pedido pelo usuario."""
    carrinho = state.get('carrinho', [])
    return processar_confirmacao(carrinho)

node_handler_cancelar(state: State) -> RetornoNode

Processa cancelamento do pedido.

Source code in src/graph/nodes.py
def node_handler_cancelar(state: State) -> RetornoNode:
    """Processa cancelamento do pedido."""
    carrinho = state.get('carrinho', [])
    return processar_cancelamento(carrinho)

node_handler_remover(state: State) -> RetornoNode

Processa remocao de itens do pedido.

Source code in src/graph/nodes.py
def node_handler_remover(state: State) -> RetornoNode:
    """Processa remocao de itens do pedido."""
    carrinho = state.get('carrinho', [])
    mensagem = state.get('mensagem_atual', '')
    return processar_remocao(carrinho, mensagem).to_dict()

node_handler_trocar(state: State) -> RetornoNode

Processa troca de variante de item no pedido.

Source code in src/graph/nodes.py
def node_handler_trocar(state: State) -> RetornoNode:
    """Processa troca de variante de item no pedido."""
    carrinho = state.get('carrinho', [])
    mensagem = state.get('mensagem_atual', '')
    return processar_troca(carrinho, mensagem).to_dict()

node_dispatcher_modificar(state: State) -> RetornoNode

Decide qual ação executar para intent modificar_pedido.

Chama os extratores na ordem correta e roteia para a ação certa. Ordem de prioridade: troca > remoção > adição > sem_entidade.

Parameters:

Name Type Description Default
state State

Estado atual do grafo.

required

Returns:

Type Description
RetornoNode

RetornoNode com 'acao' e dados de extração preenchidos.

Source code in src/graph/nodes.py
def node_dispatcher_modificar(state: State) -> RetornoNode:  # noqa: PLR0911
    """Decide qual ação executar para intent modificar_pedido.

    Chama os extratores na ordem correta e roteia para a ação certa.
    Ordem de prioridade: troca > remoção > adição > sem_entidade.

    Args:
        state: Estado atual do grafo.

    Returns:
        RetornoNode com 'acao' e dados de extração preenchidos.
    """
    mensagem = state['mensagem_atual']
    carrinho = state.get('carrinho', [])

    # ── Passo 1: TrocaExtrator ──────────────────────────────────────────────
    trocas = extrair_itens_troca(mensagem, carrinho)
    caso = trocas['caso']
    item_original = trocas['item_original']
    variante_nova = trocas['variante_nova']

    # Caso A: 2+ ITEMs mencionados
    # Não usa carrinho como critério — decide pelo que extrair() retorna.
    # "2 xtudo e 1 coca" → adição | "troca isso por aquilo" → sem_entidade
    if caso == 'A':
        itens = extrair(mensagem)
        if itens:
            return {'acao': 'adicionar_item', 'itens_extraidos': itens}
        # Nenhum item reconhecido → sem_entidade
        return {'acao': 'sem_entidade', 'dados_extracao': trocas}

    # Caso B: 1 ITEM mencionado
    elif caso == 'B':
        if item_original is not None and variante_nova is not None:
            # Item no carrinho + variante destino → troca completa
            return {'acao': 'trocar_variante', 'dados_extracao': trocas}

        if item_original is not None and variante_nova is None:
            # Item no carrinho mas sem variante destino
            if _parece_remocao(mensagem):
                # Verbo de remoção → remover item do carrinho
                return {'acao': 'remover_item', 'dados_extracao': trocas}
            else:
                # Verbo de troca sem destino → troca incompleta
                return {'acao': 'sem_entidade', 'dados_extracao': trocas}

        # item_original is None → item não está no carrinho → adição
        # Cai para extrair() abaixo

    # Caso C: 0 ITEMs + 1 VARIANTE isolada
    elif caso == 'C' and carrinho:
        # Carrinho tem itens — trocar variante de algum item
        return {'acao': 'trocar_variante', 'dados_extracao': trocas}
    # Caso C com carrinho vazio — pode ser item com nome igual a variante
    # Cai para extrair() abaixo

    # Caso vazio: TrocaExtrator não encontrou nada
    # Cai para extrair_item_carrinho + extrair() abaixo

    # ── Passo 2: Remoção de item do carrinho ────────────────────────────────
    if carrinho and _parece_remocao(mensagem):
        remocoes = extrair_item_carrinho(mensagem, carrinho)
        if remocoes:
            return {
                'acao': 'remover_item',
                'dados_extracao': {'matches': remocoes},
            }

    # ── Passo 3: Adição de item novo ────────────────────────────────────────
    itens = extrair(mensagem)
    if itens:
        return {'acao': 'adicionar_item', 'itens_extraidos': itens}

    # ── Passo 4: Nada encontrado ────────────────────────────────────────────
    return {'acao': 'sem_entidade', 'dados_extracao': trocas}