Files
agent_contas/docs/MANUAL_DESENVOLVEDOR_WORKFLOWS_CONTAS.md

1882 lines
39 KiB
Markdown

# Manual do Desenvolvedor — Workflows do Agente Contas
> **Projeto de referência:** `agent_contas_fechado_COMPLETO_corrigido_v4`
> **Pasta documentada:** `/workflows`
> **Público:** desenvolvedores que precisam criar, alterar, depurar ou revisar jornadas determinísticas do agente Contas.
---
## 1. Objetivo deste manual
A pasta `workflows/` contém a **orquestração declarativa de jornadas de negócio** do agente Contas. Ela não é apenas uma coleção de YAMLs: cada arquivo descreve um pequeno grafo de execução que o runtime genérico do `agent_framework_oci` carrega, valida, executa, pausa, retoma e finaliza.
O princípio central é:
> **O workflow decide a sequência, as condições e os pontos de pausa. A action executa a operação de domínio. O framework fornece o motor genérico.**
Isso evita que regras de jornada fiquem escondidas em `if/else` dentro dos agentes ou do framework.
Este documento explica:
- como o versionamento dos workflows funciona;
- como ler um arquivo `.vN.yaml`;
- o significado de `name`, `version`, `start`, `nodes`, `edges`, `when`, `priority`, `pause`, `expected_input` e `resume_from`;
- como funcionam `$.input`, `$.vars`, `$.output` e o estado interno;
- como uma `action` declarada no YAML se conecta a Python;
- como um workflow pausa e continua em outro turno;
- como o classificador semântico de `expected_input` funciona;
- como um workflow termina sem trocar o `session_id`;
- como cada arquivo atual da pasta `workflows/` funciona, ponto a ponto;
- como adicionar uma nova versão com segurança.
---
## 2. Estrutura atual da pasta
```text
workflows/
├── buscar_fatura.active.yaml
├── buscar_fatura.v1.yaml
├── buscar_informacao.active.yaml
├── buscar_informacao.v2.yaml
├── cancelamento_vas_avulso.active.yaml
├── cancelamento_vas_avulso.v1.yaml
├── contestacao_tool.active.yaml
├── contestacao_tool.v2.yaml
├── finalizar_atendimento.active.yaml
├── finalizar_atendimento.v1.yaml
├── invoice_explanation.active.yaml
├── invoice_explanation.v2.yaml
├── pro_rata.active.yaml
├── pro_rata.v3.yaml
├── termino_desconto.active.yaml
├── termino_desconto.v1.yaml
├── valor_divergente.active.yaml
├── valor_divergente.v1.yaml
├── vas_estrategico.active.yaml
└── vas_estrategico.v3.yaml
```
Há sempre dois papéis diferentes:
1. **`<nome>.active.yaml`** — marcador da versão ativa;
2. **`<nome>.vN.yaml`** — definição completa e versionada do grafo.
Exemplo:
```yaml
# invoice_explanation.active.yaml
version: 2
```
Esse arquivo não contém a lógica. Ele informa ao `FileWorkflowRepository` que, quando alguém pedir o workflow ativo `invoice_explanation`, deve ser carregado:
```text
invoice_explanation.v2.yaml
```
### Regra prática de versionamento
Não altere silenciosamente a semântica de uma versão já publicada quando a mudança for incompatível ou material. Prefira:
```text
invoice_explanation.v2.yaml # versão atual
invoice_explanation.v3.yaml # nova implementação
invoice_explanation.active.yaml -> version: 3
```
Assim rollback e auditoria permanecem simples.
---
## 3. Quem faz o quê
A arquitetura pode ser entendida em quatro camadas.
```mermaid
flowchart LR
U[Usuário] --> AG[Agente / Router]
AG --> W[Workflow YAML]
W --> RT[WorkflowRuntime do framework]
RT --> A[Actions Python do domínio Contas]
A --> S[Services / MCP / APIs legadas]
A --> RT
RT --> AG
```
### 3.1 Workflow YAML
Responsável por:
- sequência dos passos;
- branching;
- prioridade das transições;
- definição de pausa;
- contrato da resposta esperada do usuário;
- nó de retomada;
- declaração de valores fixos da jornada;
- escolha de qual action executar.
### 3.2 `WorkflowRuntime`
É genérico e pertence ao framework. Ele:
- carrega o YAML;
- valida o grafo;
- resolve expressões `$.…`;
- executa actions;
- mantém `vars`, `output`, `trace` e estado;
- ordena edges por `priority`;
- avalia `when`;
- implementa pause/resume;
- usa checkpoint do LangGraph em produção;
- retorna `COMPLETED`, `PAUSED` ou `FAILED`.
O runtime não deve conhecer regras específicas da TIM ou do Contas.
### 3.3 Actions Python
No Contas, a maioria das actions declaradas nos YAMLs é registrada em:
```text
app/domain/contas/workflow_actions.py
```
por meio de:
```python
reg = WorkflowActionRegistry()
@reg.action("nome_da_action")
def nome_da_action(params, state):
...
return {...}
```
Uma action deve receber:
```python
params: dict
state: dict
```
E deve retornar **sempre um `dict`**.
### 3.4 Services / MCP / legado
As actions podem chamar `ContasDomainService`, clientes HTTP, integrações TIM, mocks ou outros componentes. O YAML não deve conter código de transporte.
---
## 4. Anatomia de um workflow
Um workflow mínimo é:
```yaml
name: exemplo
version: 1
start: primeiro
nodes:
- id: primeiro
action: minha_action
input:
msisdn: $.input.msisdn
edges:
- from: primeiro
to: END
```
### 4.1 `name`
Nome lógico do workflow.
Precisa ser coerente com o nome do arquivo:
```text
exemplo.v1.yaml
name: exemplo
version: 1
```
O repository valida essa correspondência.
### 4.2 `version`
Número inteiro da versão do contrato do workflow.
### 4.3 `start`
ID do primeiro nó executado.
### 4.4 `nodes`
Cada nó representa uma unidade de execução.
```yaml
- id: preparar
action: preparar_invoice_explanation
input:
msisdn: $.input.msisdn
```
O `id` é o nome do nó dentro do grafo. A `action` é o nome registrado no `WorkflowActionRegistry`.
### 4.5 `edges`
Definem para onde o grafo segue após um nó.
```yaml
- from: preparar
to: formatar
```
ou condicionalmente:
```yaml
- from: preparar
to: formatar
priority: 10
when:
eq: [$.vars.preparar.success, true]
```
### 4.6 `END`
`END` representa término do grafo.
```yaml
- from: finalizar
to: END
```
---
## 5. Modelo de estado e expressões `$.…`
Essa é uma das partes mais importantes para quem altera a pasta `workflows/`.
### 5.1 `$.input`
Representa os dados de entrada da execução.
Exemplo:
```yaml
input:
msisdn: $.input.msisdn
invoice_id: $.input.invoice_id
```
Se o workflow foi iniciado com:
```json
{
"msisdn": "11999999999",
"invoice_id": "3000131180"
}
```
os dois valores serão passados à action.
### 5.2 `$.vars.<node>`
Após cada action retornar um dicionário, o runtime armazena o resultado em:
```text
$.vars.<id_do_no>
```
Exemplo:
```yaml
- id: registrar_protocolo
action: registrar_protocolo
```
Se a action retornar:
```json
{
"success": true,
"protocolo_id": "1234567890"
}
```
então outro nó pode usar:
```yaml
protocolo_id: $.vars.registrar_protocolo.protocolo_id
```
### 5.3 `$.output`
Aponta para o último resultado de action colocado como output corrente.
É muito usado em `pause.return_from`:
```yaml
pause:
return_from: $.output.mensagem
```
### 5.4 `$.nodes`
O runtime também mantém resultados por nó em uma estrutura de nós. Na maior parte dos workflows do Contas, `$.vars` é a forma declarativa utilizada para encadear dados.
### 5.5 Exemplo encadeado
```yaml
- id: preparar
action: preparar
- id: formatar
action: formatar
input:
dados: $.vars.preparar.dados
```
Fluxo:
```text
preparar() -> {dados: X}
|
v
$.vars.preparar.dados
|
v
formatar(dados=X)
```
---
## 6. Conditions e prioridade de edges
O runtime agrupa as edges por nó de origem e ordena por `priority` crescente.
Portanto:
```yaml
priority: 10
```
é avaliada antes de:
```yaml
priority: 99
```
### 6.1 Fallback padrão
Um padrão comum é:
```yaml
- from: action_x
to: caminho_especial
priority: 10
when:
eq: [$.vars.action_x.alguma_flag, true]
- from: action_x
to: caminho_padrao
priority: 99
```
A edge de prioridade 99 funciona como fallback porque não possui `when`.
### 6.2 Operadores encontrados nos workflows atuais
Exemplos:
```yaml
when:
eq: [$.vars.preparar.success, true]
```
```yaml
when:
neq: [$.vars.x.barcode, ""]
```
```yaml
when:
exists: $.vars.x.barcode
```
```yaml
when:
all:
- eq: [$.vars.x.a, true]
- eq: [$.vars.x.b, true]
```
```yaml
when:
any:
- eq: [$.vars.x.a, true]
- eq: [$.vars.x.b, true]
```
### Regra importante
As edges devem ser mutuamente compreensíveis. Se nenhuma edge corresponder, o runtime pode falhar com:
```text
Nenhuma transição do workflow correspondeu ao estado
```
Por isso fluxos condicionais normalmente possuem uma edge final sem `when`.
---
## 7. Pause / resume
Workflows conversacionais podem parar no meio da execução para pedir uma resposta ao usuário.
Exemplo simplificado:
```yaml
- id: formatar
action: formatar_invoice_explanation
pause:
enabled: true
return_from: $.output.mensagem
expected_input:
key: resposta_usuario
allowed_values: ["SIM", "NAO"]
normalize: upper_strip
resume_from: decisao
```
### 7.1 O que ocorre no primeiro turno
1. `formatar_invoice_explanation` é executada;
2. a mensagem produzida é obtida de `$.output.mensagem`;
3. o runtime persiste o checkpoint;
4. retorna `status=PAUSED`;
5. a mensagem é enviada ao usuário;
6. o workflow fica aguardando input.
### 7.2 O que ocorre no turno seguinte
A nova fala é validada contra `expected_input`.
Se aceita:
```text
resposta do usuário
normalize
$.input.resposta_usuario
resume_from: decisao
```
### 7.3 Por que pause é separado da action
No runtime atual, pause/resume é implementado em um nó técnico separado. Isso é deliberado.
Ao retomar, **a action anterior não é reexecutada**. Isso evita repetir efeitos externos como:
- abrir protocolo duas vezes;
- cancelar duas vezes;
- enviar SMS novamente;
- criar duas SRs.
---
## 8. `expected_input`
Exemplo:
```yaml
expected_input:
key: resposta_usuario
allowed_values: ["SIM", "NAO", "OUTRO"]
normalize: upper_strip
```
### `key`
Nome em que o valor normalizado será gravado em `$.input`.
### `allowed_values`
Valores internos permitidos para a decisão do workflow.
Esses valores são **tokens de controle**, não necessariamente texto exibido ao cliente.
### `normalize: upper_strip`
Remove espaços laterais e converte para maiúsculas.
Exemplo:
```text
" sim " -> "SIM"
```
### `reprompt`
Mensagem utilizada quando o input não pode ser interpretado pelo contrato.
---
## 9. Semantic classifier de `expected_input`
O `invoice_explanation` possui um classificador semântico declarativo.
Ele existe porque respostas reais do usuário raramente são somente `sim` ou `não`.
Exemplo:
```text
"entendi, obrigado, era só isso"
```
semanticamente é `SIM`.
Já:
```text
"então no mês que vem vou pagar menos?"
```
não é `SIM`, mesmo contendo sinal de compreensão; é uma continuação da pergunta.
O YAML define:
```yaml
semantic_classifier:
enabled: true
include_relevant_context: true
option_actions:
CONTINUAR:
action: contextual_reentry
prompt: |
...
```
### 9.1 Responsabilidade correta
- **Framework:** executa o classificador e garante que a saída esteja entre os valores permitidos.
- **Workflow/agente:** define o significado de `SIM`, `NAO`, `CONTINUAR`.
### 9.2 `contextual_reentry`
Quando a opção classificada possui:
```yaml
CONTINUAR:
action: contextual_reentry
```
o workflow pausado não deve simplesmente tratar a fala como confirmação. A utterance é liberada para nova interpretação pelo roteamento normal, com contexto delimitado.
Isso é particularmente importante para evitar que hipóteses do usuário virem fatos confirmados.
---
## 10. Estado terminal e nova interação na mesma sessão
Um workflow pode terminar sem encerrar tecnicamente o `session_id`.
Isso significa:
```text
mesma sessão técnica
!=
mesmo workflow ativo
```
No `invoice_explanation`, por exemplo:
```yaml
workflow_response_final: true
```
indica que aquela action produz a resposta final daquele workflow.
Depois de uma execução terminal, o framework deve eliminar o latch operacional do workflow para que o próximo turno seja uma nova entrada, ainda na mesma sessão.
Deve permanecer:
- `session_id`;
- `session_key`;
- `conversation_key`;
- identidade do cliente;
- auditoria e telemetria;
- long-term memory.
Não deve continuar controlando a próxima entrada:
- `pending_domain_workflow`;
- `expected_input`;
- pause antigo;
- active transaction antiga;
- confirmação antiga;
- route stickiness da jornada terminada;
- short-term operational context do workflow fechado.
Esse detalhe é fundamental ao depurar cenários como:
```text
Usuário: entendi, obrigado, era só isso
Agente: Seu número de protocolo é ...
Usuário: ah espera
```
O terceiro turno é uma nova entrada na mesma sessão, e não um resume do workflow anterior.
---
# 11. Workflows atuais — explicação arquivo por arquivo
---
## 11.1 `buscar_fatura.active.yaml`
```yaml
version: 1
```
Seleciona `buscar_fatura.v1.yaml` como versão ativa.
## 11.2 `buscar_fatura.v1.yaml`
### Objetivo
Executar uma busca de fatura em um único passo.
### Cabeçalho
```yaml
name: buscar_fatura
version: 1
start: buscar_fatura
```
### Nó `buscar_fatura`
```yaml
- id: buscar_fatura
action: buscar_fatura
input:
invoice_id: $.input.invoice_id
msisdn: $.input.msisdn
customer_id: $.input.customer_id
output: $.input.output
```
A action Python está em `app/domain/contas/workflow_actions.py`.
Com `invoice_id` e `customer_id`, ela tenta buscar a fatura detalhada. Sem os identificadores necessários, usa `consultar_faturas` como fallback.
### Edge
```yaml
- from: buscar_fatura
to: END
```
Não há branch nem pausa.
### Modelo mental
```text
INPUT
|
v
buscar_fatura
|
v
END
```
### Quando usar
Quando a operação é atômica e não precisa de confirmação do usuário.
---
## 11.3 `buscar_informacao.active.yaml`
```yaml
version: 2
```
Ativa `buscar_informacao.v2.yaml`.
## 11.4 `buscar_informacao.v2.yaml`
### Objetivo
Preparar uma consulta RAG e, em seguida, preparar a resposta para composição pelo framework.
### Nó 1 — `buscar_informacao`
```yaml
id: buscar_informacao
action: buscar_informacao_rag
```
Recebe:
- `query`;
- `queries`;
- `top_k`;
- `segment`.
A action atual devolve uma estrutura declarando `delegate_to_framework_rag=true`, isto é, o domínio sinaliza que a capacidade RAG deve ser executada pelo framework.
### Nó 2 — `reescrever_resposta`
Consome os resultados do primeiro nó:
```yaml
queries: $.vars.buscar_informacao.queries
documents: $.vars.buscar_informacao.documents
answer: $.vars.buscar_informacao.answer
noMatchRag: $.vars.buscar_informacao.noMatchRag
ragRetrievedDocuments: $.vars.buscar_informacao.ragRetrievedDocuments
ragSelectedDocuments: $.vars.buscar_informacao.ragSelectedDocuments
```
A action `reescrever_resposta_buscar_informacao` devolve a mensagem e `delegate_to_framework_llm=true`.
### Fluxo
```text
buscar_informacao_rag
|
v
reescrever_resposta_buscar_informacao
|
v
END
```
### Conceito importante
A pasta `workflows/` orquestra, mas não deve implementar o mecanismo RAG. O framework continua responsável pelo runtime RAG.
---
## 11.5 `cancelamento_vas_avulso.active.yaml`
```yaml
version: 1
```
## 11.6 `cancelamento_vas_avulso.v1.yaml`
### Objetivo
Executar cancelamento em lote de VAS avulso.
### Nó único
```yaml
id: cancelar_vas_avulso
action: cancelamento_vas_avulso_batch
```
Entradas:
- `items`;
- `csp_id`;
- `channel`;
- `social_sec_no`;
- `data_credito_proxima_fatura`;
- `idempotency_key`.
O YAML também fixa valores do contrato operacional:
```yaml
request_status: "Fechado"
status: "CLOSED"
```
### Fluxo
```text
cancelamento_vas_avulso_batch -> END
```
### Ponto de atenção
É uma operação transacional. A confirmação do usuário e as políticas de tool podem ocorrer antes da entrada no workflow. Não mova para o YAML uma duplicação de confirmation policy que pertença ao framework.
O `idempotency_key` é especialmente importante em operações com efeito externo.
---
## 11.7 `contestacao_tool.active.yaml`
```yaml
version: 2
```
## 11.8 `contestacao_tool.v2.yaml`
Este é o workflow mais complexo da pasta atual.
### Objetivo
Orquestrar a contestação de cobrança, incluindo protocolo, status da fatura, abertura da contestação, SMS quando aplicável, regra de corte, Conta Certa Manual e atualização de status.
### Visão geral
```mermaid
flowchart TD
A[registrar_protocolo] --> B[check_invoice_status]
B --> C[abrir_contestacao_cliente]
C -->|success=false| Z[END]
C -->|tem barcode| D[enviar_sms]
C -->|sem SMS| E[consultar_contrato_corte]
D --> E
E -->|Conta Certa Manual elegível| F[abrir_sr_conta_certa_manual]
E -->|caso padrão| G[atualizar_status_sr]
F --> H[atualizar_status_sr_registro]
H --> Z
G --> Z
```
### Nó 1 — `registrar_protocolo`
Primeiro efeito da jornada:
```yaml
action: registrar_protocolo
```
Configura:
```yaml
scenario: "contestacao"
request_status: "Aberto"
status: "OPENED"
```
O protocolo retornado fica acessível em:
```text
$.vars.registrar_protocolo.protocolo_id
```
### Nó 2 — `check_invoice_status`
Consulta ou reaproveita o `CompleteInvoices` já obtido em prefetch.
O comentário do YAML deixa clara a intenção arquitetural: evitar uma segunda chamada desnecessária quando o payload já existe na sessão.
### Nó 3 — `abrir_contestacao_cliente`
Recebe grande parte do contexto necessário para a operação financeira, inclusive:
- cliente;
- fatura;
- serviço/item;
- valor;
- descrição;
- protocolo;
- tipo da contestação;
- motivo do ajuste;
- opção de devolução;
- regras de Conta Certa Manual;
- `double_refund`;
- dados de atendimento;
- `skip_invoice_item_validation`.
O `invoice_status` vem do nó anterior:
```yaml
invoice_status: $.vars.check_invoice_status.invoice_status
```
### Branch de falha financeira
```yaml
- from: abrir_contestacao_cliente
to: END
priority: 1
when:
eq: [$.vars.abrir_contestacao_cliente.success, false]
```
É avaliado primeiro. Se a validação financeira bloquear a contestação, não deve executar SMS, contrato ou SR.
### Branch de SMS
```yaml
when:
all:
- exists: $.vars.abrir_contestacao_cliente.barcode
- neq: [$.vars.abrir_contestacao_cliente.barcode, ""]
```
Se a contestação produzir código de boleto, o fluxo passa por `enviar_sms`.
Caso contrário, a edge `priority: 99` segue diretamente para `consultar_contrato_corte`.
### Nó `consultar_contrato_corte`
Determina a regra de data de corte e também trata particularidade de item dependente de plano família.
### Branch Conta Certa Manual
O branch é propositalmente composto:
```yaml
when:
any:
- all:
- apos_data_corte == true
- contestation_success == true
- manual_conta_certa_indicator == true
- all:
- dependent_invoice_item == true
- contestation_registered == true
```
Isso expressa duas formas de elegibilidade:
1. regra normal após data de corte + indicador manual;
2. item de dependente cuja contestação foi registrada.
### `abrir_sr_conta_certa_manual`
Cria a SR de Conta Certa Manual.
### `atualizar_status_sr`
Caminho padrão, fecha/atualiza o protocolo principal.
### `atualizar_status_sr_registro`
Caminho usado após Conta Certa Manual, atualizando a SR correspondente.
### Pontos de atenção
- Não trocar prioridades sem revisar todos os branches.
- `success=false` precisa continuar precedendo qualquer efeito posterior.
- Reaproveitamento de prefetch evita chamadas duplicadas.
- É um workflow com múltiplos efeitos externos; qualquer retry deve ser analisado com idempotência.
---
## 11.9 `finalizar_atendimento.active.yaml`
```yaml
version: 1
```
## 11.10 `finalizar_atendimento.v1.yaml`
### Objetivo
Centralizar o fechamento final do atendimento.
### Nó único `finalizar`
```yaml
action: finalizar_atendimento_action
```
Entradas relevantes:
- `status`;
- `summary`;
- `msisdn`;
- `social_sec_no`;
- `message_id`;
- tipos informacionais de VAS;
- protocolo;
- flags de supressão/deferimento de eventos.
### Fluxo
```text
finalizar_atendimento_action -> END
```
### Observação
Finalizar atendimento é conceitualmente diferente de simplesmente alcançar `END` em qualquer workflow. `END` encerra aquele grafo; `finalizar_atendimento_action` implementa a semântica de negócio de fechamento de atendimento.
---
## 11.11 `invoice_explanation.active.yaml`
```yaml
version: 2
```
## 11.12 `invoice_explanation.v2.yaml`
### Objetivo
Explicar variação de fatura, aguardar a confirmação semântica do cliente e então:
- registrar aceite e protocolo final; ou
- registrar negativa e transferir para atendimento humano.
Também possui caminhos de falha de serviço e validação de tentativa.
### Visão principal
```mermaid
flowchart TD
A[preparar] -->|success| B[formatar]
A -->|service_failed| F[resposta_falha_servico]
A -->|success=false| C[checar_tentativa]
C -->|limite excedido| D[fim_intencao_invalida]
C -->|caso contrário| E[texto_intencao_invalida]
B --> P{{PAUSE}}
P --> G[decisao]
G -->|SIM| H[registrar_sim]
H --> I[registrar_protocolo_aceite]
I --> Z[END]
G -->|NAO| J[registrar_nao]
J --> K[handoff_pos_explicacao_nao]
K --> Z
```
### Nó `preparar`
Action:
```text
preparar_invoice_explanation
```
Responsável por obter ou reutilizar a explicação base.
Recebe dados da fatura atual/passada e também:
```yaml
tentativa_anterior: $.vars.preparar.tentativa
```
Isso permite controlar tentativas dentro do estado do workflow.
### Nó `formatar`
Action:
```text
formatar_invoice_explanation
```
Ela monta a mensagem, mas **não controla a pausa**. A pausa está declarada no YAML.
Essa separação é intencional: apresentação e controle de fluxo não devem ficar acoplados.
### Pause do `formatar`
Sempre pausa após apresentar a explicação.
```yaml
allowed_values: ["SIM", "NAO", "CONTINUAR"]
```
O semantic classifier diferencia confirmação, negativa e continuação contextual.
#### Exemplos
```text
"sim" -> SIM
"entendi, obrigado" -> SIM
"não resolveu" -> NAO
"é a cobrança de 14,99" -> CONTINUAR
"mês que vem fica mais barato?" -> CONTINUAR
```
### `decisao`
É um `no_op`. Sua função é fornecer um ponto explícito no grafo para branching após o resume.
### Caminho SIM
```text
decisao
-> registrar_sim
-> registrar_protocolo_aceite
-> END
```
`registrar_protocolo_aceite` usa:
```yaml
workflow_response_final: true
```
para indicar que a mensagem retornada é a resposta final do workflow.
### Caminho NAO
```text
decisao
-> registrar_nao
-> handoff_pos_explicacao_nao
-> END
```
`preparar_handoff_invoice_explanation` materializa:
```text
session_control = HUMAN_HANDOFF
```
A decisão de jornada está no workflow do Contas; a primitive de handoff é do framework.
### Caminhos de erro
`preparar` distingue:
- `success=true`;
- `service_failed=true`;
- `success=false` por validação/tentativa.
`checar_tentativa` decide se ainda pode pedir novamente ou se o limite foi excedido.
### Nós `checar_vas_variacao` e `finalizar_nao_resolvido`
Esses nós estão declarados para a política de VAS variado/não resolvido. Observe que, na versão atual, não existe edge de entrada para `checar_vas_variacao` partindo do fluxo principal SIM/NAO mostrado acima. Antes de reutilizar ou alterar esses nós, valide a intenção de jornada e os testes associados.
### Ponto crítico de lifecycle
Quando `registrar_protocolo_aceite` produz `workflow_response_final=true`, o workflow deve ser considerado terminal mesmo que alguma integração legada devolva metadata antiga com `PAUSED`. O próximo turno na mesma sessão não deve reutilizar `expected_input` desse workflow.
---
## 11.13 `pro_rata.active.yaml`
```yaml
version: 3
```
## 11.14 `pro_rata.v3.yaml`
### Objetivo
Explicar cobrança proporcional (`pro rata`) e tratar de forma diferente clientes com Plano Controle.
### Nó `preparar`
Action:
```text
preparar_pro_rata
```
Recebe planos e `has_plano_controle`.
A action decide se a jornada precisa interagir com o usuário:
```text
await_user_input = true/false
```
### Nó `formatar`
Possui pause condicional:
```yaml
pause:
enabled: true
when:
eq: [$.vars.preparar.await_user_input, true]
```
Portanto, diferente do `invoice_explanation`, este workflow só pausa quando necessário.
### Expected input
```yaml
allowed_values: ["SIM", "NAO", "OUTRO"]
```
### `decisao_esclarecimento`
Branch:
- `SIM` -> `registrar_aceitou`;
- `NAO` -> `devolver_orquestrador`;
- qualquer outra situação -> `reperguntar_esclarecimento`.
### `reperguntar_esclarecimento`
Formata novamente e pausa de novo, retomando em `decisao_esclarecimento`.
Isso forma um pequeno loop conversacional controlado:
```text
reperguntar
|
pause
|
+----> decisao_esclarecimento
```
### Caso sem Plano Controle
Se `await_user_input=false`, a edge de prioridade 20 sai de `formatar` para:
```text
registrar_nao_controle -> END
```
### Caso SIM
```text
registrar_aceitou -> END
```
Essa action também registra protocolo/evento apropriado.
---
## 11.15 `termino_desconto.active.yaml`
```yaml
version: 1
```
## 11.16 `termino_desconto.v1.yaml`
### Objetivo
Formatar a resposta da capability de término de desconto.
### Nó único
```yaml
id: formatar
action: formatar_capability_resposta
```
Com:
```yaml
tipo: termino_desconto
```
Além de dados do plano/fatura/evidência de desconto.
### Conceito
A mesma action genérica `formatar_capability_resposta` é parametrizada pelo `tipo` da capability.
### Fluxo
```text
formatar_capability_resposta(tipo=termino_desconto) -> END
```
---
## 11.17 `valor_divergente.active.yaml`
```yaml
version: 1
```
## 11.18 `valor_divergente.v1.yaml`
### Objetivo
Formatar resposta para a capability de valor divergente.
### Nó único
```yaml
action: formatar_capability_resposta
input:
tipo: valor_divergente
msisdn: $.input.msisdn
```
É estruturalmente semelhante ao `termino_desconto`, mas com outro `tipo` e conjunto de inputs.
### Ponto de atenção
Se a capability passar a exigir dados adicionais, prefira explicitá-los no YAML, mantendo claro o contrato entre workflow e action.
---
## 11.19 `vas_estrategico.active.yaml`
```yaml
version: 3
```
## 11.20 `vas_estrategico.v3.yaml`
### Objetivo
Tratar VAS estratégico e bundle, apresentar explicação, coletar aceite/negativa e registrar o resultado.
### Visão geral
```mermaid
flowchart TD
A[preparar] -->|await_user_input| P{{PAUSE}}
A -->|sem pausa| B[resposta_bundle]
P --> C[decisao]
C -->|SIM| D[resposta_sim]
C -->|NAO e estratégico| E[explicar_cancelamento]
C -->|NAO bundle puro| B
C -->|fallback| F[registrar_outro]
D --> G[registrar_sim]
E --> H[registrar_nao]
B --> I[registrar_bundle]
G --> Z[END]
H --> Z
I --> Z
F --> Z
```
### Nó `preparar`
Action:
```text
preparar_vas_estrategico
```
Recebe `items` e `linhas`.
Possui pausa condicionada à saída da própria action:
```yaml
when:
eq: [$.output.await_user_input, true]
```
### Pause
```yaml
allowed_values: ["SIM", "NAO", "OUTRO"]
resume_from: decisao
```
### `resposta_bundle`
Monta texto a partir de:
```yaml
$.vars.preparar.mensagem_bundle_fechamento
```
### `decisao`
É o ponto de branching depois da pausa.
#### SIM
Sempre vai para `resposta_sim`, seja bundle ou estratégico.
#### NAO + estratégico
```yaml
all:
- has_estrategico_items == true
- resposta_usuario == NAO
```
vai para `explicar_cancelamento`.
#### NAO + bundle puro
Quando não há item estratégico, vai para `resposta_bundle`.
#### fallback
`priority: 99` -> `registrar_outro`.
O comentário do arquivo ressalta que, em operação normal, `OUTRO` deveria ser interceptado/reperguntado pelo runtime antes de entrar nessa decisão; o fallback continua existindo como proteção.
### Registro final
Há actions diferentes para preservar o caminho de negócio:
- `registrar_sim`;
- `registrar_nao`;
- `registrar_bundle`;
- `registrar_outro`.
Todas terminam em `END`.
---
# 12. Mapa workflow -> actions Python
| Workflow | Action(s) principais | Implementação |
|---|---|---|
| `buscar_fatura` | `buscar_fatura` | `app/domain/contas/workflow_actions.py` |
| `buscar_informacao` | `buscar_informacao_rag`, `reescrever_resposta_buscar_informacao` | mesmo arquivo |
| `cancelamento_vas_avulso` | `cancelamento_vas_avulso_batch` | mesmo arquivo |
| `contestacao_tool` | `registrar_protocolo`, `check_invoice_status`, `abrir_contestacao_cliente`, `enviar_sms`, `consultar_contrato_corte`, `abrir_sr_conta_certa_manual`, `atualizar_status_sr` | mesmo arquivo |
| `finalizar_atendimento` | `finalizar_atendimento_action` | mesmo arquivo |
| `invoice_explanation` | `preparar_invoice_explanation`, `formatar_invoice_explanation`, `checar_tentativa_cvn`, `registrar_atendimento_invoice_explanation`, `registrar_protocolo_inicio`, `preparar_handoff_invoice_explanation`, `checar_vas_variado` | mesmo arquivo |
| `pro_rata` | `preparar_pro_rata`, `formatar_pro_rata`, `registrar_atendimento_pro_rata` | mesmo arquivo |
| `termino_desconto` | `formatar_capability_resposta` | mesmo arquivo |
| `valor_divergente` | `formatar_capability_resposta` | mesmo arquivo |
| `vas_estrategico` | `preparar_vas_estrategico`, `montar_resposta_texto`, `montar_explicacao_cancelamento_vas_estrategico`, `registrar_atendimento_vas_estrategico` | mesmo arquivo |
`no_op` e `montar_resposta_texto` são actions utilitárias registradas pelo mesmo registry de domínio.
---
# 13. Como criar um novo workflow
## Passo 1 — definir a responsabilidade
Pergunte:
- há mais de uma etapa?
- existe branching?
- existe efeito externo?
- existe pausa conversacional?
- precisa ser retomado em outro turno?
Se a operação for uma única função sem jornada, talvez uma tool/action simples seja suficiente.
## Passo 2 — registrar as actions
Em `workflow_actions.py`:
```python
@reg.action("consultar_exemplo")
def consultar_exemplo(params, state):
result = service.consultar(...)
return {
"success": True,
"dados": result,
}
```
## Passo 3 — criar `nome.v1.yaml`
```yaml
name: meu_workflow
version: 1
start: consultar
nodes:
- id: consultar
action: consultar_exemplo
input:
msisdn: $.input.msisdn
edges:
- from: consultar
to: END
```
## Passo 4 — criar marcador ativo
```yaml
# meu_workflow.active.yaml
version: 1
```
## Passo 5 — adicionar branches
Sempre pense em fallback explícito.
```yaml
- from: consultar
to: sucesso
priority: 10
when:
eq: [$.vars.consultar.success, true]
- from: consultar
to: falha
priority: 99
```
## Passo 6 — adicionar pause somente quando a jornada exige input
Não coloque pausa dentro da lógica Python da action se ela faz parte do contrato do fluxo.
## Passo 7 — testar
No mínimo:
- happy path;
- cada branch;
- falha da integração;
- input ausente;
- pause;
- resume;
- resposta inválida;
- idempotência de efeitos externos;
- terminalidade;
- novo turno após finalização.
---
# 14. Como criar uma nova versão
Suponha que `vas_estrategico.v3.yaml` precise mudar materialmente.
1. copie para `vas_estrategico.v4.yaml`;
2. altere internamente `version: 4`;
3. implemente/teste a nova lógica;
4. mantenha v3 disponível;
5. altere somente depois:
```yaml
# vas_estrategico.active.yaml
version: 4
```
### Rollback
Basta voltar o marker:
```yaml
version: 3
```
sem apagar a v4.
---
# 15. Como depurar um workflow
## 15.1 Comece pelo status
Procure:
```text
COMPLETED
PAUSED
FAILED
```
## 15.2 Confira `workflow_name` e `workflow_version`
Isso confirma qual YAML realmente foi carregado.
## 15.3 Confira `trace`
Exemplo:
```text
preparar -> COMPLETED
formatar -> COMPLETED
formatar -> pause_resume RESUMED
decisao -> COMPLETED
registrar_sim -> COMPLETED
registrar_protocolo_aceite -> COMPLETED
```
O trace responde rapidamente:
- qual action executou;
- qual nó foi o último;
- se houve resume;
- se alguma action foi repetida.
## 15.4 Confira `vars`
Ao investigar uma edge:
```yaml
when:
eq: [$.vars.consultar_contrato_corte.apos_data_corte, true]
```
primeiro valide o conteúdo real de:
```text
vars.consultar_contrato_corte.apos_data_corte
```
Não conclua que a edge está errada sem verificar a saída da action.
## 15.5 Confira `pause.expected_input`
Se o sistema está tratando uma frase como resposta de um fluxo anterior, procure:
```text
pending_domain_workflow
expected_input
transaction_status
workflow_resume
```
Após workflow terminal, esses latches não devem sequestrar o próximo turno.
## 15.6 Confira o marker `.active.yaml`
Um erro comum é editar `v3.yaml`, mas o marker continuar apontando para v2.
---
# 16. Regras de desenho recomendadas
## 16.1 Workflow orquestra; action executa
Bom:
```yaml
when:
eq: [$.vars.validar.success, false]
```
Action retorna a evidência; YAML escolhe o próximo passo.
Evite colocar toda a jornada dentro de uma única action gigante.
## 16.2 Não colocar regra TIM no runtime genérico
Se a regra pertence a contestação, VAS ou fatura, ela deve ficar no domínio/configuração do agente, não hardcoded no framework.
## 16.3 Side effects precisam de idempotência
Especialmente:
- cancelamento;
- contestação;
- protocolo;
- SMS;
- criação de SR.
## 16.4 Pausa não deve reexecutar action anterior
Mantenha o desenho em que o pause é um contrato do nó e o resume segue para `resume_from`.
## 16.5 Prioridade deve ser intencional
Use números que deixem clara a hierarquia:
```text
1 bloqueio terminal crítico
10 caminho específico
20 segundo caminho específico
99 fallback
```
## 16.6 Não use output textual para decidir operação financeira
Branching deve usar campos estruturados como:
```text
success
barcode
apos_data_corte
dependent_invoice_item
```
não palavras encontradas em uma frase produzida por LLM.
---
# 17. Anti-patterns
### 17.1 Alterar `.active.yaml` sem criar a versão
Errado:
```yaml
version: 4
```
sem existir `nome.v4.yaml`.
### 17.2 Action não registrada
Se o YAML contém:
```yaml
action: minha_action
```
mas o registry não possui esse nome, o runtime falhará com action não registrada.
### 17.3 Referenciar `$.vars` de nó que ainda não executou
Exemplo incorreto:
```yaml
start: B
B:
input:
protocolo: $.vars.A.protocolo
```
se `A` nunca foi executado.
### 17.4 Branch sem fallback
Pode provocar falha de transição.
### 17.5 Usar `pause` para esconder estado de domínio
`pause` deve indicar interação com usuário, não substituir persistência correta de transação.
### 17.6 Reutilizar workflow terminal como contexto ativo
Um workflow terminado pode permanecer no histórico para auditoria, mas não deve continuar fornecendo `expected_input` ao próximo turno.
---
# 18. Checklist de code review
Antes de aprovar alteração em `workflows/`:
- [ ] `name` corresponde ao arquivo;
- [ ] `version` corresponde ao sufixo `.vN`;
- [ ] `active.yaml` aponta para uma versão existente;
- [ ] `start` existe;
- [ ] IDs de nós são únicos;
- [ ] todas as actions estão registradas;
- [ ] todos os `$.input` necessários são fornecidos pelo caller;
- [ ] referências `$.vars.<node>` apontam para nós que executaram antes;
- [ ] branches específicos têm prioridade anterior ao fallback;
- [ ] existe fallback quando necessário;
- [ ] `END` está alcançável;
- [ ] effects externos são idempotentes ou protegidos;
- [ ] pause não reexecuta action de efeito externo;
- [ ] `resume_from` existe;
- [ ] `allowed_values` são tokens internos coerentes;
- [ ] semantic classifier não transforma pergunta/hipótese em confirmação;
- [ ] workflow terminal limpa latch operacional;
- [ ] próximo turno na mesma sessão é testado;
- [ ] testes de happy path e todos os branches existem.
---
# 19. Resumo conceitual para novos desenvolvedores
Se você lembrar somente destas dez regras, já consegue navegar pela pasta com segurança:
1. **`.active.yaml` escolhe a versão; `.vN.yaml` contém a lógica.**
2. **`nodes` executam actions; `edges` decidem o próximo nó.**
3. **`$.input` é entrada; `$.vars.<node>` é resultado de nó anterior.**
4. **Menor `priority` é avaliada primeiro.**
5. **Uma edge sem `when` normalmente é o fallback.**
6. **`pause` suspende a jornada; `resume_from` determina onde continuar.**
7. **Tokens `SIM/NAO/CONTINUAR/OUTRO` são controle interno, não fraseologia.**
8. **Actions fazem domínio/integração; o YAML faz orquestração.**
9. **`END` termina o grafo; finalização de atendimento pode envolver action própria.**
10. **Workflow terminado não deve controlar o próximo turno, mesmo quando o `session_id` permanece igual.**
---
# 20. Referências de código dentro do projeto
Para aprofundar a implementação:
```text
/workflows/
definições declarativas do Contas
/app/domain/contas/workflow_actions.py
implementação das actions usadas pelos workflows
/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/models.py
schema Pydantic do DSL
/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/repository.py
resolução de active version
/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/runtime.py
executor, branching, pause/resume e LangGraph
/agent_framework_oci/libs/agent_framework/src/agent_framework/workflows/registry.py
registro e resolução das actions
/app/workflows/agent_graph.py
grafo principal do agente Contas e integração com router/guardrails/judges
```
---
## Apêndice A — Exemplo completo comentado
```yaml
name: exemplo_confirmacao
version: 1
start: preparar
nodes:
# Executa domínio e produz mensagem + dados estruturados.
- id: preparar
action: preparar_exemplo
input:
msisdn: $.input.msisdn
# Apenas apresenta a mensagem e pausa.
- id: apresentar
action: montar_resposta_texto
input:
dados:
texto_usuario: $.vars.preparar.mensagem
pause:
enabled: true
return_from: $.output.mensagem
expected_input:
key: resposta_usuario
allowed_values: ["SIM", "NAO"]
normalize: upper_strip
resume_from: decidir
# Nó estrutural para branch pós-resume.
- id: decidir
action: no_op
input: {}
- id: confirmar
action: executar_exemplo
input:
msisdn: $.input.msisdn
- id: cancelar
action: montar_resposta_texto
input:
dados:
texto_usuario: "Operação não realizada."
edges:
- from: preparar
to: apresentar
- from: apresentar
to: decidir
- from: decidir
to: confirmar
priority: 10
when:
eq: [$.input.resposta_usuario, SIM]
- from: decidir
to: cancelar
priority: 20
when:
eq: [$.input.resposta_usuario, NAO]
- from: confirmar
to: END
- from: cancelar
to: END
```
Leitura em português simples:
> Prepare os dados, mostre uma mensagem, pare e aguarde SIM/NAO. Quando o usuário responder, continue em `decidir`. Se SIM, execute a operação; se NAO, responda que nada foi feito. Depois encerre o workflow.
---
**Fim do manual.**