Ajustes conforme relatorio de testes 2026-08-27

This commit is contained in:
2026-08-29 10:03:42 -03:00
parent 1fd18531c0
commit 81f24d7357
699 changed files with 7989 additions and 2364 deletions

View File

@@ -13,3 +13,46 @@ O `WorkflowRuntime` também preserva o último snapshot persistido do LangGraph
Isso é necessário para workflows transacionais: por exemplo, se um protocolo foi criado e uma chamada posterior falha, o chamador ainda recebe o `protocol_number` persistido e pode executar recuperação/idempotência sem repetir o primeiro side effect.
O runtime não transforma falha em sucesso e não reexecuta automaticamente a action; ele apenas preserva a evidência durável já existente no checkpointer.
## Tratamento genérico de entrada fora das opções (`unmatched`)
`expected_input` mantém compatibilidade com o comportamento anterior.
Sem `semantic_classifier`, qualquer entrada que não pertença literalmente a
`allowed_values` permanece no workflow e recebe o `reprompt`.
Quando o agente precisa aceitar linguagem natural, ele declara um prompt
classificatório cujo resultado deve ser uma das próprias opções dinâmicas:
```yaml
expected_input:
key: resposta_usuario
allowed_values: [SIM, NAO]
normalize: upper_strip
reprompt: "Não entendi. Responda sim ou não."
semantic_classifier:
enabled: true
prompt: |
Classifique a fala em exatamente uma opção de {{ allowed_values }}.
Pergunta pendente: {{ pending_prompt }}
Fala do usuário: {{ user_input }}
Retorne somente uma opção de {{ allowed_values }}.
```
O framework não possui classes fixas. `allowed_values` pode conter duas, três ou
mais opções; o prompt do agente define a semântica de cada uma. O framework
renderiza os placeholders, chama a LLM e rejeita qualquer saída que não pertença
à allowlist, usando `reprompt` nesse caso. O texto original do usuário é mantido
nos metadados da decisão para auditoria.
Nesse modo, `COER` delega a interpretação semântica ao classificador configurado.
Rails de segurança independentes — por exemplo PINJ, toxicidade, PII e limites
de tamanho — continuam podendo bloquear o turno normalmente.
O exemplo executável está em `agent_template_backend/` e, por compatibilidade
com a estrutura histórica desta feature, também em
`agent_template_backend_pause_resume/`.
### Reentrada contextual por opção
Uma opção do `semantic_classifier` pode declarar `option_actions.<OPCAO>.action: contextual_reentry`. Nesse caso o workflow pausado não é retomado: o framework libera a pausa e reexecuta o roteamento usando somente o contexto conversacional ancorado que originou a decisão mais a fala atual. A fala original é preservada para auditoria e o contexto reconstruído não vira evidência de negócio; parâmetros candidatos continuam sujeitos a validação e confirmação normais.

View File

@@ -32,3 +32,49 @@ O exemplo usa `MemorySaver` apenas para ser autocontido. Em aplicações reais u
## Regra arquitetural
Código de domínio não deve importar `langgraph.graph.StateGraph`. Para grafos de agentes use `FrameworkStateGraph`; para workflows determinísticos de negócio use `WorkflowRuntime`.
## Entrada enumerada, reprompt e `semantic_classifier`
O contrato `expected_input` pode declarar qualquer conjunto de opções em
`allowed_values`. O match literal continua determinístico; quando a resposta não
coincide literalmente com uma opção, o agente pode habilitar um classificador
semântico com prompt próprio.
```yaml
expected_input:
key: resposta_usuario
allowed_values: [SIM, NAO]
normalize: upper_strip
reprompt: "Não entendi. Responda sim ou não."
semantic_classifier:
enabled: true
prompt: |
Classifique {{ user_input }} em exatamente uma opção de {{ allowed_values }}.
Para este workflow, aceitação/entendimento => SIM; negação, nova pergunta
ou hipótese factual a validar => NAO.
Retorne somente uma opção de {{ allowed_values }}.
```
O framework não conhece o significado de `SIM`, `NAO` nem de nenhuma outra
opção. Ele apenas injeta `allowed_values`, `pending_prompt` e `user_input`, chama
a LLM e valida estritamente se a saída pertence à lista declarada. Uma saída
fora da lista usa o `reprompt`.
O mesmo mecanismo funciona sem alteração do framework para, por exemplo,
`[CONFIRMAR, ALTERAR, CANCELAR]` ou qualquer outra lista configurada pelo agente.
O rail `COER` delega a semântica ao `semantic_classifier` nesse modo; PINJ,
toxicidade, PII e os demais rails de segurança continuam independentes.
Há dois diretórios equivalentes para facilitar comparação com os demais
cenários de Tuning-Performance:
- `agent_template_backend/` — nome padrão de template;
- `agent_template_backend_pause_resume/` — nome histórico deste exemplo.
Ambos contêm o mesmo workflow `confirmacao.v1.yaml`.
### Reentrada contextual por opção
Uma opção do `semantic_classifier` pode declarar `option_actions.<OPCAO>.action: contextual_reentry`. Nesse caso o workflow pausado não é retomado: o framework libera a pausa e reexecuta o roteamento usando somente o contexto conversacional ancorado que originou a decisão mais a fala atual. A fala original é preservada para auditoria e o contexto reconstruído não vira evidência de negócio; parâmetros candidatos continuam sujeitos a validação e confirmação normais.

View File

@@ -0,0 +1,26 @@
# Agent Template Backend — Pause/Resume Workflow
Exemplo autocontido de um agente que usa o motor genérico de workflows do
`agent_framework_oci`.
O arquivo `workflows/confirmacao.v1.yaml` demonstra:
- `pause`;
- `expected_input`;
- `allowed_values`;
- `normalize`;
- `reprompt`;
- `semantic_classifier`;
- `resume_from`.
O framework não conhece `SIM`, `NAO` nem a regra de negócio. O agente declara
os valores e o prompt no YAML. Quando a fala não corresponde literalmente a uma
opção, `semantic_classifier` classifica usando o prompt do agente e o framework
aceita somente uma saída presente em `allowed_values`; qualquer outra saída usa
o `reprompt`. O mesmo mecanismo funciona com qualquer quantidade de opções.
Execute os testes a partir desta pasta:
```bash
pytest -q
```

View File

@@ -0,0 +1,56 @@
from __future__ import annotations
import asyncio
from pathlib import Path
from agent_framework.workflows import FileWorkflowRepository, WorkflowActionRegistry, WorkflowRuntime
ROOT = Path(__file__).resolve().parents[1]
def build_runtime(*, offline_test_fallback: bool = False) -> WorkflowRuntime:
actions = WorkflowActionRegistry()
async def preparar(params, state):
return {"assunto": params.get("assunto") or "operação"}
async def perguntar(params, state):
return {"mensagem": f"Deseja confirmar {params['assunto']}?"}
async def decidir(params, state):
return {
"mensagem": "Operação confirmada." if params["resposta"] == "SIM" else "Operação cancelada.",
"confirmado": params["resposta"] == "SIM",
}
actions.register("preparar_operacao", preparar)
actions.register("montar_pergunta", perguntar)
actions.register("registrar_decisao", decidir)
checkpointer = None
if not offline_test_fallback:
# Produção/exemplo real continua usando LangGraph + checkpointer. O import
# fica aqui para que a regressão offline do repositório não dependa de rede.
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
return WorkflowRuntime(
FileWorkflowRepository(ROOT / "workflows"),
actions=actions,
checkpointer=checkpointer,
allow_deterministic_fallback=offline_test_fallback,
)
async def main() -> None:
runtime = build_runtime()
first = await runtime.arun("confirmacao", {"assunto": "a alteração do plano"})
print(first.model_dump(mode="json"))
assert first.status == "PAUSED"
resumed = await runtime.aresume("confirmacao", first.execution_id, {"resposta_usuario": "sim"})
print(resumed.model_dump(mode="json"))
assert resumed.status == "COMPLETED"
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,11 @@
# Confirmação Transacional Semântica
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.

View File

@@ -0,0 +1,35 @@
from __future__ import annotations
import pytest
from app.demo import build_runtime
@pytest.mark.asyncio
async def test_pause_resume_does_not_repeat_previous_action():
# Regressão offline: exercita a mesma DSL/WorkflowRuntime sem exigir download
# de LangGraph no builder. Produção continua usando build_runtime() default.
runtime = build_runtime(offline_test_fallback=True)
first = await runtime.arun("confirmacao", {"assunto": "o cancelamento"})
assert first.status == "PAUSED"
assert first.pause["expected_input"]["key"] == "resposta_usuario"
before = [item for item in first.trace if item.get("action") == "preparar_operacao"]
assert len(before) == 1
resumed = await runtime.aresume("confirmacao", first.execution_id, {"resposta_usuario": "SIM"})
assert resumed.status == "COMPLETED"
after = [item for item in resumed.trace if item.get("action") == "preparar_operacao"]
assert len(after) == 1
assert resumed.state["vars"]["decidir"]["confirmado"] is True
def test_pause_resume_example_documents_semantic_classifier():
from pathlib import Path
import yaml
project = Path(__file__).resolve().parents[1]
data = yaml.safe_load((project / "workflows" / "confirmacao.v1.yaml").read_text(encoding="utf-8"))
perguntar = next(node for node in data["nodes"] if node["id"] == "perguntar")
expected = perguntar["pause"]["expected_input"]
assert expected["reprompt"] == "Não entendi. Responda sim ou não."
assert expected["semantic_classifier"]["enabled"] is True
assert "{{ allowed_values }}" in expected["semantic_classifier"]["prompt"]

View File

@@ -0,0 +1,45 @@
name: confirmacao
version: 1
start: preparar
nodes:
- id: preparar
action: preparar_operacao
input:
assunto: $.input.assunto
- id: perguntar
action: montar_pergunta
input:
assunto: $.vars.preparar.assunto
pause:
enabled: true
return_from: $.output.mensagem
expected_input:
key: resposta_usuario
allowed_values: [SIM, NAO]
normalize: upper_strip
reprompt: "Não entendi. Responda sim ou não."
semantic_classifier:
include_relevant_context: true
enabled: true
prompt: |
Classifique a resposta em exatamente uma opção de {{ allowed_values }}.
Pergunta pendente: {{ pending_prompt }}
Contexto relevante:
{{ relevant_conversation_context }}
Resposta do usuário: {{ user_input }}
Neste exemplo, concordância/aceitação corresponde a SIM e recusa, dúvida
adicional ou nova condição corresponde a NAO.
Retorne somente uma opção de {{ allowed_values }}.
resume_from: decidir
- id: decidir
action: registrar_decisao
input:
resposta: $.input.resposta_usuario
assunto: $.vars.preparar.assunto
edges:
- from: preparar
to: perguntar
- from: perguntar
to: END
- from: decidir
to: END

View File

@@ -0,0 +1,11 @@
# Confirmação Transacional Semântica
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.

View File

@@ -20,3 +20,16 @@ async def test_pause_resume_does_not_repeat_previous_action():
after = [item for item in resumed.trace if item.get("action") == "preparar_operacao"]
assert len(after) == 1
assert resumed.state["vars"]["decidir"]["confirmado"] is True
def test_pause_resume_example_documents_semantic_classifier():
from pathlib import Path
import yaml
project = Path(__file__).resolve().parents[1]
data = yaml.safe_load((project / "workflows" / "confirmacao.v1.yaml").read_text(encoding="utf-8"))
perguntar = next(node for node in data["nodes"] if node["id"] == "perguntar")
expected = perguntar["pause"]["expected_input"]
assert expected["reprompt"] == "Não entendi. Responda sim ou não."
assert expected["semantic_classifier"]["enabled"] is True
assert "{{ allowed_values }}" in expected["semantic_classifier"]["prompt"]

View File

@@ -17,6 +17,19 @@ nodes:
key: resposta_usuario
allowed_values: [SIM, NAO]
normalize: upper_strip
reprompt: "Não entendi. Responda sim ou não."
semantic_classifier:
include_relevant_context: true
enabled: true
prompt: |
Classifique a resposta em exatamente uma opção de {{ allowed_values }}.
Pergunta pendente: {{ pending_prompt }}
Contexto relevante:
{{ relevant_conversation_context }}
Resposta do usuário: {{ user_input }}
Neste exemplo, concordância/aceitação corresponde a SIM e recusa, dúvida
adicional ou nova condição corresponde a NAO.
Retorne somente uma opção de {{ allowed_values }}.
resume_from: decidir
- id: decidir
action: registrar_decisao