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.