mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 10:13:46 +00:00
Ajustes conforme relatorio de testes 2026-08-27
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
@@ -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())
|
||||
@@ -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.
|
||||
Binary file not shown.
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
version: 1
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
Binary file not shown.
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user