Pular para conteúdo

Máquina de estados

Fonte e invariantes

src/core/domain/states.py define 9 estados, 19 transições legais e 2 estados terminais. LEGAL_TRANSITIONS é a tabela de verdade. Session.transition_to(), em src/core/domain/session.py, chama validate_transition() e lança IllegalStateTransitionError para qualquer aresta ausente.

stateDiagram-v2
    [*] --> NEW
    NEW --> TRIAGE
    TRIAGE --> TRIAGE
    TRIAGE --> QUALIFYING
    TRIAGE --> HANDOFF_PENDING
    TRIAGE --> OUT_OF_SCOPE
    TRIAGE --> CLOSED
    QUALIFYING --> QUALIFYING
    QUALIFYING --> DATA_COLLECT
    QUALIFYING --> HANDOFF_PENDING
    QUALIFYING --> CLOSED
    DATA_COLLECT --> DATA_COLLECT
    DATA_COLLECT --> CONFIRM
    DATA_COLLECT --> HANDOFF_PENDING
    DATA_COLLECT --> CLOSED
    CONFIRM --> DATA_COLLECT
    CONFIRM --> HANDOFF_PENDING
    CONFIRM --> CLOSED
    HANDOFF_PENDING --> HUMAN
    HUMAN --> CLOSED
    CLOSED --> [*]
    OUT_OF_SCOPE --> [*]

As setas de início/fim são convenções visuais; as 19 arestas são as setas entre estados nomeados. HANDOFF_PENDING encerra o processamento automático, mas não é terminal no enum.

Estados e responsabilidade

Estado Finalidade e criação Agente/consumidor Efeito relevante
NEW Session.new() cria uma conversa com UUID Caso de uso Persiste identidade; sobe para triagem antes de chamar IA
TRIAGE Recepção e classificação Triagem Define segment/intent, pede contexto ou encaminha
QUALIFYING Escolha do serviço e parâmetros técnicos Qualificação Faz merge de qualification_data; valida catálogo antes de avançar
DATA_COLLECT Cadastro ou correção Coleta Faz merge de cadastral_data e pede campos faltantes
CONFIRM Resumo apresentado, aguardando aceite Coleta Valida dados programaticamente quando recebe s=4
HANDOFF_PENDING Encaminhamento aguardando humano Webhook, notificador, renotificação, inbox Suspende IA, mantém recebimento, guarda resumo e motivo
HUMAN Operador assumiu a conversa human_conversation.py Respostas manuais; entradas não chamam IA
CLOSED Atendimento encerrado ou assunto substituído Operador, troca de assunto, debug Terminal; transition_to preenche closed_at
OUT_OF_SCOPE Demanda fora do portfólio Triagem s=4 Terminal; registra OutOfPortfolioDemand

Tabela completa de transições

Origem Destino Gatilho normal Efeitos/observações
NEW TRIAGE Primeiro processamento current_agent=triagem
TRIAGE TRIAGE s=1, s=5, s=2 sem intent=servico Resposta; no último caso atualiza classificação
TRIAGE QUALIFYING s=2, intent=servico Invoca Qualificação no mesmo turno
TRIAGE HANDOFF_PENDING s=3 ou fallback Resumo, motivo, persistência, notificação
TRIAGE OUT_OF_SCOPE s=4 Demanda fora do portfólio e encerramento
TRIAGE CLOSED Troca confirmada s=98 Resumo e nova sessão vinculada
QUALIFYING QUALIFYING s=1, s=3, s=5 ou s=2 incompleto Permanece coletando parâmetros
QUALIFYING DATA_COLLECT s=2 completo e serviço conhecido Guarda serviço e chama Coleta no mesmo turno
QUALIFYING HANDOFF_PENDING s=4, serviço desconhecido ou fallback Humano decide a demanda
QUALIFYING CLOSED Troca confirmada s=98 Nova sessão independente
DATA_COLLECT DATA_COLLECT s=1, s=3, s=6 Coleta, correção ou dúvida respondida
DATA_COLLECT CONFIRM s=2 Resumo para aceite; ainda sem validação programática completa
DATA_COLLECT HANDOFF_PENDING s=5 ou fallback Encaminhamento
DATA_COLLECT CLOSED Troca confirmada s=98 Nova sessão independente
CONFIRM DATA_COLLECT s=3; dispatcher também permite s=1/s=6 Retorna ao cadastro, preservando campos
CONFIRM HANDOFF_PENDING s=4 confirmado, s=5, erro ou limite de validação Integrações conforme caminho de confirmação
CONFIRM CLOSED Troca confirmada s=98 Nova sessão independente
HANDOFF_PENDING HUMAN Takeover autorizado Atribui operador por operação condicional
HUMAN CLOSED Finalização autorizada Exige ownership e respeito ao lock de envio

s=99 carrega conhecimento e invoca novamente sem transição; s=97 mantém o estado e guarda o texto de possível novo assunto. Permanecer em CONFIRM durante validação recusada ou s=97 não chama uma transição CONFIRM → CONFIRM: essa aresta não existe. _apply_reply_and_stay() evita transition_to() quando origem e destino são iguais.

Troca de assunto e nova sessão

s=98 não reabre um objeto terminal. _apply_subject_change() resume e fecha a sessão anterior, cria outra com previous_session_id, preserva a instância e reconhece o nome anterior. O novo histórico contém a mensagem que iniciou o assunto, e não todo o histórico da sessão antiga.

A primeira mudança percebida deve retornar s=97. O guard do dispatcher transforma s=98 sem sinal de confirmação em pedido de confirmação; tecnicamente aceita subject_change_confirmed=true ou session.pending_subject_change_text já preenchido. A confirmação semântica depende do comportamento do agente, não de um classificador determinístico da palavra “sim”.

Erros, concorrência e exceções administrativas

Transição ilegal no domínio e conflito atômico são diferentes. A primeira significa uma aresta proibida; DynamoConditionFailedError indica que o estado/operador/pointer mudou no banco enquanto uma operação rodava. O Orchestrator evita gravar resultados que perderam ownership. O painel mapeia conflitos esperados para resposta de conflito.

DynamoSessionRepository.transition_state() aplica condições de concorrência, mas não chama validate_transition() por conta própria. Seus consumidores devem validar o caminho de negócio. O comando de debug pode fechar estados ativos diretamente no repository, incluindo HANDOFF_PENDING; é uma exceção operacional à tabela do domínio, e não uma nova aresta legal a acrescentar ao diagrama.

Campos de identidade (session_id, phone_number, whatsapp_instance, created_at, previous_session_id) são imutáveis após construção. Merges conservam chaves anteriores e sobrescrevem valores específicos; não há garantia genérica de que um valor vazio nunca substitua um campo preenchido. Validação de completude pertence ao dispatcher/confirmador.

Testes e manutenção

tests/unit/domain/test_states.py enumera pares legais e ilegais, terminais, saída única de NEW/HANDOFF_PENDING e total 19. test_session.py cobre identidade, merges e timestamps. test_action_mapping.py, test_confirm_action.py e test_human_conversation.py ligam códigos e operações à máquina.

Para um novo estado, atualize enum, tabela, seleção de agente, índices/serialização, comportamento do webhook, mapeamento comercial, UI e testes. Não adicione apenas uma string ao prompt: o dispatcher pode rejeitá-la ou o repository persistir um valor que outros consumidores não reconhecem.

Divergência documental

A docstring de SessionState diz “eight states”; o enum contém nove. O comentário inicial também generaliza fechamento por s=98 para qualquer estado ativo, mas HANDOFF_PENDING só tem saída legal para HUMAN, e HUMAN é controlado por operador.

Consulte a referência compacta dos estados e os códigos.