mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 10:13:46 +00:00
Compare commits
4 Commits
b5d2a12953
...
a472daa1e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a472daa1e4 | ||
|
|
d93efd8972 | ||
| 727997aa41 | |||
| 2496e831a1 |
@@ -1,32 +1,102 @@
|
||||
# Transaction parameter precedence fix
|
||||
# Precedência transacional + extração LLM de parâmetros
|
||||
|
||||
Correção para a regressão em que uma resposta curta que preenchia um parâmetro pendente (ex.: `R$ 71,99` para `valor`) era classificada pelo LLM Router como uma nova intent e interrompia a transação.
|
||||
Esta correção remove a extração textual hardcoded de parâmetros transacionais e faz a coleta de `policy.requires` por um extrator LLM genérico.
|
||||
|
||||
## Regra aplicada
|
||||
## Regra de precedência
|
||||
|
||||
Durante `COLLECTING_PARAMETERS` a ordem passa a ser:
|
||||
|
||||
1. resposta compatível com parâmetro pendente -> mantém a política de estado e continua a transação;
|
||||
2. cancelamento explícito -> cancela a transação;
|
||||
3. nova intenção clara/pergunta explícita -> interrompe a transação e volta ao roteamento normal;
|
||||
4. caso ambíguo -> permanece em clarificação.
|
||||
|
||||
Exemplo corrigido:
|
||||
Enquanto existir uma transação ativa, o framework trata o turno nesta ordem:
|
||||
|
||||
```text
|
||||
não fiz essa contratação TIM CTRL Redes Sociais 8.0
|
||||
-> informe valor
|
||||
R$ 71,99
|
||||
-> valor=71.99; continua contestar_cobranca; executa pré-validação
|
||||
ACTIVE_TRANSACTION
|
||||
|
|
||||
+-- COLLECTING_PARAMETERS
|
||||
| |
|
||||
| +-- LLM tenta extrair SOMENTE os parâmetros ainda pendentes
|
||||
| |
|
||||
| +-- extraiu >= 1 ?
|
||||
| |
|
||||
| +-- SIM -> continua a transação; NÃO avalia intent_shift
|
||||
| |
|
||||
| +-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
||||
|
|
||||
+-- AWAITING_CONFIRMATION
|
||||
|
|
||||
+-- reconhece confirmação/rejeição explícita
|
||||
|
|
||||
+-- reconheceu ?
|
||||
|
|
||||
+-- SIM -> continua/cancela a transação; NÃO avalia intent_shift
|
||||
|
|
||||
+-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
||||
```
|
||||
|
||||
A mensagem `R$ 71,99` não pode mais virar `contas_invoice_explanation` enquanto `valor` estiver pendente.
|
||||
## TransactionParameterExtractor
|
||||
|
||||
## Testes
|
||||
Novo componente:
|
||||
|
||||
Foram adicionados testes para:
|
||||
`libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py`
|
||||
|
||||
- valor monetário com LLM sugerindo outra intent;
|
||||
- entidade curta como resposta de parâmetro;
|
||||
- pergunta clara durante coleta ainda interrompendo a transação;
|
||||
- regressões existentes de intent-shift e transactional tool flow.
|
||||
A extração textual dos parâmetros de negócio é feita exclusivamente por LLM. O componente recebe:
|
||||
|
||||
- nome da tool/transação ativa;
|
||||
- parâmetros atualmente pendentes;
|
||||
- argumentos já conhecidos;
|
||||
- schema/tipos declarados em `tools.yaml` quando disponíveis;
|
||||
- descrição da tool;
|
||||
- mensagem atual do usuário.
|
||||
|
||||
Ele não conhece nomes de domínio como `order_id`, `reason`, `subject`, `valor`, TIM ou retail. Não há regex de entidades de negócio.
|
||||
|
||||
A LLM pode interpretar, por exemplo:
|
||||
|
||||
- `PED-1001` quando só há um parâmetro compatível pendente;
|
||||
- `o pedido é PED-1001`;
|
||||
- `PED-1001, desisti da compra` preenchendo dois parâmetros no mesmo turno;
|
||||
- respostas com o nome do parâmetro seguido do valor;
|
||||
- respostas apenas com o valor, quando semanticamente inequívocas.
|
||||
|
||||
Em caso de dúvida, o prompt manda retornar `null`. Uma nova solicitação não deve ser transformada em valor de parâmetro.
|
||||
|
||||
## Separação de responsabilidades
|
||||
|
||||
`tool_policies.yaml` continua sendo a fonte de verdade para `requires`.
|
||||
|
||||
`tools.yaml` pode fornecer tipos via `args_schema` e descrição da tool para melhorar a interpretação sem introduzir código específico de domínio.
|
||||
|
||||
`mcp_parameter_mapping.yaml` continua responsável pelos parâmetros auxiliares/contrato MCP. As strategies do mapper são explicitamente excluídas dos campos presentes em `policy.requires`, para não misturar extração MCP com coleta transacional.
|
||||
|
||||
O `EnterpriseRouter` usa o mesmo extrator LLM apenas como *probe* de precedência. Se pelo menos um parâmetro pendente for encontrado, o turno permanece no estado transacional. Os valores extraídos são colocados no metadata da decisão e reutilizados pelo runtime, evitando uma segunda chamada LLM no mesmo turno.
|
||||
|
||||
## Profile LLM
|
||||
|
||||
Foi adicionado aos templates:
|
||||
|
||||
```yaml
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
```
|
||||
|
||||
Generation/component:
|
||||
|
||||
- `llm.transaction_parameter_extraction`
|
||||
- `transaction_parameter_extraction`
|
||||
|
||||
## Limpeza de estado
|
||||
|
||||
Em `intent_shift`, `transaction_pre_validation` da transação abandonada é removido para não contaminar a nova transação. O resultado de pre-validation continua preservado enquanto pertence à própria transação para auditoria.
|
||||
|
||||
## Testes adicionados
|
||||
|
||||
`tests/test_transaction_parameter_llm_precedence.py`
|
||||
|
||||
Cobertura:
|
||||
|
||||
1. dois parâmetros extraídos no mesmo turno;
|
||||
2. um parâmetro preenchido ganha precedência sobre keyword que indicaria outra intent;
|
||||
3. nenhum parâmetro encontrado libera `intent_shift`;
|
||||
4. ausência do antigo `_extract_action_arguments()` hardcoded;
|
||||
5. confirmação `sim` ganha precedência sobre intent shift.
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -72,3 +72,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
profiles:
|
||||
default:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
max_tokens: 2048
|
||||
supervisor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
route_continuity:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
router:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
guardrail:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 600
|
||||
grl:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
judge:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 800
|
||||
rag_rewriter:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 300
|
||||
rag_compressor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 1200
|
||||
rag_generation:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1800
|
||||
summary_memory:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1200
|
||||
noc:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
billing_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
product_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
backoffice_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
mcp_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
transaction_evidence: list[dict[str, Any]]
|
||||
last_transaction_evidence: dict[str, Any]
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
transaction_pre_validation: dict[str, Any]
|
||||
transaction_evidence: list[dict[str, Any]]
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
224
docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md
Normal file
224
docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Guia do Desenvolvedor — Estado e Transações Multi-turno
|
||||
|
||||
Este documento define o contrato operacional para transações multi-turno no Agent Framework OCI. Ele é normativo para hosts e templates que utilizam `AgentRuntime`, checkpoint LangGraph e tools transacionais.
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Uma transação pode atravessar vários turnos. Exemplo:
|
||||
|
||||
```text
|
||||
Usuário: quero cancelar o pedido
|
||||
Framework: informe o número do pedido
|
||||
Usuário: PED-1001
|
||||
Framework: confirma o cancelamento?
|
||||
Usuário: sim
|
||||
Framework: executa a tool
|
||||
```
|
||||
|
||||
O framework precisa preservar a transação entre todos esses turnos sem depender de reclassificação por LLM, keyword routing ou reextração de parâmetros já obtidos.
|
||||
|
||||
## 2. Fonte canônica do estado transacional
|
||||
|
||||
O estado canônico da transação em andamento é `active_transaction`.
|
||||
|
||||
```python
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
```
|
||||
|
||||
Todo `AgentState` usado por um host que habilita transações multi-turno **DEVE** declarar os dois campos. Como o LangGraph usa o schema do state para persistência/checkpoint, um campo criado apenas dinamicamente pelo runtime não é um contrato durável seguro.
|
||||
|
||||
Exemplo mínimo:
|
||||
|
||||
```python
|
||||
from typing import Any, TypedDict
|
||||
|
||||
class AgentState(TypedDict, total=False):
|
||||
# ...campos normais...
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
missing_parameters: list[str]
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
```
|
||||
|
||||
## 3. Papel de cada campo
|
||||
|
||||
| Campo | Papel | Regra |
|
||||
|---|---|---|
|
||||
| `active_transaction` | Fonte canônica da transação ativa | Deve sobreviver a checkpoint/resume enquanto a transação estiver ativa. |
|
||||
| `last_transaction` | Snapshot da última transação terminal | Usado para auditoria, evidência e continuidade controlada; não reativa automaticamente a transação. |
|
||||
| `transaction_status` | Estado lógico atual | Ex.: `COLLECTING_PARAMETERS`, `AWAITING_CONFIRMATION`, `COMPLETED`, `CANCELLED`, `OUT_OF_SCOPE`. |
|
||||
| `missing_parameters` | Parâmetros ainda necessários | Deve refletir o estado canônico da transação, não apenas a mensagem corrente. |
|
||||
| `selected_tool_call` | Estado auxiliar/compatibilidade | Não deve substituir `active_transaction` como fonte canônica. |
|
||||
| `pending_tool_call` | Estado auxiliar/compatibilidade | Pode ser usado por compatibilidade, mas não como latch principal. |
|
||||
| `next_state` | Orientação de roteamento do workflow | Ajuda a manter o nó/agente correto durante coleta/confirmação. |
|
||||
| `transaction_pre_validation` | Evidência de pré-validação | Mantém resultado de validação antes da confirmação/execução. |
|
||||
| `transaction_evidence` | Evidências da execução | Mantém resultados e trilha de execução da transação. |
|
||||
|
||||
## 4. Ciclo de vida recomendado
|
||||
|
||||
```text
|
||||
IDLE
|
||||
↓ intenção transacional
|
||||
COLLECTING_PARAMETERS
|
||||
↓ parâmetros completos
|
||||
PRE_VALIDATION (quando configurado)
|
||||
↓ elegível
|
||||
AWAITING_CONFIRMATION
|
||||
↓ confirmação positiva
|
||||
EXECUTING
|
||||
↓
|
||||
COMPLETED
|
||||
```
|
||||
|
||||
Saídas terminais alternativas:
|
||||
|
||||
```text
|
||||
CANCELLED
|
||||
OUT_OF_SCOPE
|
||||
FAILED
|
||||
```
|
||||
|
||||
O runtime pode representar algumas fases internamente sem um `transaction_status` público separado. O requisito é preservar o latch e não perder argumentos já coletados.
|
||||
|
||||
## 5. Merge incremental de parâmetros
|
||||
|
||||
Uma resposta posterior deve complementar a transação existente, nunca recriá-la apenas a partir do texto atual.
|
||||
|
||||
```python
|
||||
existing = dict((state.get("active_transaction") or {}).get("arguments") or {})
|
||||
new_values = {"valor": "71.99"}
|
||||
arguments = {**existing, **new_values}
|
||||
```
|
||||
|
||||
Exemplo esperado:
|
||||
|
||||
```text
|
||||
Turno 1: subject = "TIM CTRL Redes Sociais 8.0"
|
||||
Turno 2: valor = "71.99"
|
||||
Resultado: subject + valor permanecem disponíveis
|
||||
```
|
||||
|
||||
## 6. Precedência de roteamento durante transação
|
||||
|
||||
Quando existe `active_transaction` em `COLLECTING_PARAMETERS`, a mensagem deve primeiro ser avaliada como possível resposta aos parâmetros pendentes.
|
||||
|
||||
Precedência normativa:
|
||||
|
||||
1. parâmetro pendente claramente preenchido → continuar a transação;
|
||||
2. cancelamento/abandono explícito → cancelar a transação;
|
||||
3. nova intenção inequívoca → interromper a transação e rotear;
|
||||
4. keyword genérica do mesmo domínio/agente → **não** interromper a transação;
|
||||
5. mensagem ambígua → manter a transação e clarificar.
|
||||
|
||||
Exemplos:
|
||||
|
||||
| Estado atual | Mensagem | Resultado correto |
|
||||
|---|---|---|
|
||||
| `retail_order_cancel`, falta `order_id` | `PED-1001` | Continua cancelamento e preenche `order_id`. |
|
||||
| `retail_order_cancel`, falta `order_id` | `o pedido é o PED-1001` | Continua cancelamento; `pedido` não deve virar tracking. |
|
||||
| contestação, falta `valor` | `R$ 71,99` | Continua contestação e preenche `valor`. |
|
||||
| cancelamento pendente | `esquece, quero ver minha fatura` | Interrupção explícita permitida. |
|
||||
| cancelamento pendente | `quero rastrear pedido` | Mudança inequívoca para tracking permitida. |
|
||||
|
||||
## 7. Checkpoint e retomada
|
||||
|
||||
Antes de executar roteamento normal, o host deve restaurar o checkpoint usando a mesma identidade de conversa (`tenant_id`, `agent_id`, `session_id`/`conversation_key` conforme contrato do host).
|
||||
|
||||
Após a restauração:
|
||||
|
||||
```text
|
||||
active_transaction existe
|
||||
↓
|
||||
status ativo?
|
||||
↓ sim
|
||||
retomar a transação antes de keyword routing / continuity LLM
|
||||
```
|
||||
|
||||
Um estado `COLLECTING_PARAMETERS` sem `active_transaction` deve ser tratado como inconsistência de estado e observado/diagnosticado; não deve silenciosamente reiniciar a tool a partir da mensagem corrente.
|
||||
|
||||
## 8. O que pertence ao framework e ao agente
|
||||
|
||||
Framework:
|
||||
|
||||
- persistência do latch;
|
||||
- merge de argumentos;
|
||||
- estados de coleta/confirmação;
|
||||
- precedência de retomada;
|
||||
- confirmação determinística;
|
||||
- idempotência e evidência;
|
||||
- checkpoint/resume.
|
||||
|
||||
Agente:
|
||||
|
||||
- definição das tools de domínio;
|
||||
- parâmetros obrigatórios e mensagens de domínio;
|
||||
- regras de elegibilidade específicas;
|
||||
- pre-validation específica, quando houver;
|
||||
- resposta final ao cliente.
|
||||
|
||||
O agente não deve implementar um segundo motor transacional paralelo ao `AgentRuntime`.
|
||||
|
||||
## 9. Checklist para novos hosts/templates
|
||||
|
||||
- [ ] `AgentState` declara `active_transaction`.
|
||||
- [ ] `AgentState` declara `last_transaction`.
|
||||
- [ ] `transaction_status` e `missing_parameters` fazem parte do state quando usados.
|
||||
- [ ] O host usa checkpoint compatível com o schema do state.
|
||||
- [ ] A mesma `conversation_key` é usada entre turnos da mesma conversa.
|
||||
- [ ] Parâmetros já coletados são mesclados com novos valores.
|
||||
- [ ] Respostas a parâmetros têm precedência sobre keyword routing genérico.
|
||||
- [ ] Mudança explícita de intenção continua possível.
|
||||
- [ ] O agente usa `transaction_state_patch(state)` ao retornar respostas transacionais quando o template o exige.
|
||||
- [ ] Existem testes multi-turno para coleta, confirmação, interrupção e resume.
|
||||
|
||||
## 10. Testes regressivos mínimos
|
||||
|
||||
```text
|
||||
A. cancelamento de pedido
|
||||
1. "quero cancelar pedido"
|
||||
2. "o pedido é o PED-1001"
|
||||
Esperado: continua retail_order_cancel; não vira retail_order_tracking.
|
||||
|
||||
B. contestação
|
||||
1. "não contratei TIM CTRL Redes Sociais 8.0"
|
||||
2. "R$ 71,99"
|
||||
Esperado: subject e valor chegam juntos à pre-validation.
|
||||
|
||||
C. interrupção explícita
|
||||
1. iniciar transação e deixar parâmetro pendente
|
||||
2. "esquece, quero ver minha fatura"
|
||||
Esperado: transação é interrompida e nova intenção é roteada.
|
||||
|
||||
D. checkpoint/resume
|
||||
1. iniciar transação
|
||||
2. persistir/checkpoint
|
||||
3. reconstruir execução usando a mesma conversation_key
|
||||
4. fornecer o parâmetro faltante
|
||||
Esperado: active_transaction é restaurado e concluído sem reiniciar a tool.
|
||||
```
|
||||
|
||||
## 11. Anti-patterns
|
||||
|
||||
- reconstruir a transação somente a partir da última mensagem;
|
||||
- usar `selected_tool_call` como única fonte do latch;
|
||||
- remover `active_transaction` do `AgentState` por parecer redundante;
|
||||
- permitir uma keyword genérica como `pedido` interromper coleta de `order_id`;
|
||||
- armazenar parâmetros apenas em variáveis locais do nó;
|
||||
- duplicar confirmação transacional no prompt do agente;
|
||||
- limpar o latch antes do estado terminal.
|
||||
|
||||
## 12. Referências no projeto
|
||||
|
||||
- `specs/SPEC-002-Agent-Runtime.md`
|
||||
- `specs/SPEC-010-Agent-Development.md`
|
||||
- `templates/agent_template_backend/app/state.py`
|
||||
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||
- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py`
|
||||
- `Tuning-Performance/Deterministic_Transactional_Workflow/`
|
||||
- `Tuning-Performance/Transaction_Pre_Validation/`
|
||||
- `Tuning-Performance/Transaction_Evidence/`
|
||||
160
docs/TRANSACTION_STATE_DEVELOPER_GUIDE_en.md
Normal file
160
docs/TRANSACTION_STATE_DEVELOPER_GUIDE_en.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Developer Guide — Multi-turn Transaction State
|
||||
|
||||
This document defines the operational contract for multi-turn transactions in Agent Framework OCI. It is normative for hosts and templates that use `AgentRuntime`, LangGraph checkpoints, and transactional tools.
|
||||
|
||||
## 1. Goal
|
||||
|
||||
A transaction may span multiple turns:
|
||||
|
||||
```text
|
||||
User: cancel my order
|
||||
Framework: provide the order number
|
||||
User: PED-1001
|
||||
Framework: confirm cancellation?
|
||||
User: yes
|
||||
Framework: execute the tool
|
||||
```
|
||||
|
||||
The framework must preserve the transaction across all turns without relying on LLM reclassification, keyword routing, or re-extraction of parameters already collected.
|
||||
|
||||
## 2. Canonical transaction state
|
||||
|
||||
The canonical in-flight transaction is `active_transaction`.
|
||||
|
||||
```python
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
```
|
||||
|
||||
Every `AgentState` used by a host that enables multi-turn transactions **MUST** declare both fields. LangGraph uses the state schema for checkpoint persistence, so a field created dynamically by the runtime alone is not a safe durable contract.
|
||||
|
||||
Minimal example:
|
||||
|
||||
```python
|
||||
from typing import Any, TypedDict
|
||||
|
||||
class AgentState(TypedDict, total=False):
|
||||
# ...normal fields...
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
missing_parameters: list[str]
|
||||
confirmation_required: bool
|
||||
confirmation_received: bool
|
||||
```
|
||||
|
||||
## 3. Field responsibilities
|
||||
|
||||
| Field | Responsibility | Rule |
|
||||
|---|---|---|
|
||||
| `active_transaction` | Canonical in-flight transaction | Must survive checkpoint/resume while active. |
|
||||
| `last_transaction` | Snapshot of the latest terminal transaction | Used for audit/evidence; does not automatically reactivate a transaction. |
|
||||
| `transaction_status` | Current logical status | E.g. `COLLECTING_PARAMETERS`, `AWAITING_CONFIRMATION`, `COMPLETED`, `CANCELLED`, `OUT_OF_SCOPE`. |
|
||||
| `missing_parameters` | Parameters still required | Must reflect canonical transaction state, not only the current message. |
|
||||
| `selected_tool_call` | Auxiliary/backward-compatible state | Must not replace `active_transaction` as canonical state. |
|
||||
| `pending_tool_call` | Auxiliary/backward-compatible state | May support compatibility but is not the primary latch. |
|
||||
| `next_state` | Workflow routing guidance | Keeps the correct node/agent during collection/confirmation. |
|
||||
| `transaction_pre_validation` | Pre-validation evidence | Stores validation before confirmation/execution. |
|
||||
| `transaction_evidence` | Execution evidence | Stores results and the transaction execution trail. |
|
||||
|
||||
## 4. Recommended lifecycle
|
||||
|
||||
```text
|
||||
IDLE
|
||||
↓ transactional intent
|
||||
COLLECTING_PARAMETERS
|
||||
↓ complete parameters
|
||||
PRE_VALIDATION (when configured)
|
||||
↓ eligible
|
||||
AWAITING_CONFIRMATION
|
||||
↓ positive confirmation
|
||||
EXECUTING
|
||||
↓
|
||||
COMPLETED
|
||||
```
|
||||
|
||||
Alternative terminal outcomes include `CANCELLED`, `OUT_OF_SCOPE`, and `FAILED`.
|
||||
|
||||
## 5. Incremental parameter merge
|
||||
|
||||
A later answer must complement the existing transaction instead of rebuilding it from the latest text only.
|
||||
|
||||
```python
|
||||
existing = dict((state.get("active_transaction") or {}).get("arguments") or {})
|
||||
new_values = {"amount": "71.99"}
|
||||
arguments = {**existing, **new_values}
|
||||
```
|
||||
|
||||
Previously collected arguments must remain available on subsequent turns.
|
||||
|
||||
## 6. Routing precedence during an active transaction
|
||||
|
||||
When `active_transaction` is in `COLLECTING_PARAMETERS`, the message must first be evaluated as a possible answer to pending parameters.
|
||||
|
||||
Normative precedence:
|
||||
|
||||
1. clearly fills a pending parameter → continue transaction;
|
||||
2. explicit cancel/abandon → cancel transaction;
|
||||
3. unambiguous new intent → interrupt and route;
|
||||
4. generic keyword in the same domain/agent → **do not** interrupt;
|
||||
5. ambiguous message → keep transaction and clarify.
|
||||
|
||||
| Current state | Message | Correct result |
|
||||
|---|---|---|
|
||||
| `retail_order_cancel`, missing `order_id` | `PED-1001` | Continue cancellation and fill `order_id`. |
|
||||
| `retail_order_cancel`, missing `order_id` | `the order is PED-1001` | Continue cancellation; `order` must not switch to tracking. |
|
||||
| contestation, missing `amount` | `R$ 71.99` | Continue contestation and fill amount. |
|
||||
| pending cancellation | `forget it, show my bill` | Explicit interruption is allowed. |
|
||||
| pending cancellation | `track my order` | Unambiguous shift to tracking is allowed. |
|
||||
|
||||
## 7. Checkpoint and resume
|
||||
|
||||
Before normal routing, restore the checkpoint with the same conversation identity (`tenant_id`, `agent_id`, `session_id`/`conversation_key` according to the host contract).
|
||||
|
||||
An active transaction must be resumed before generic keyword routing or LLM continuity. `COLLECTING_PARAMETERS` without `active_transaction` should be treated as inconsistent state and diagnosed rather than silently restarting the tool.
|
||||
|
||||
## 8. Framework vs. agent responsibility
|
||||
|
||||
Framework owns latch persistence, argument merge, collection/confirmation states, resume precedence, deterministic confirmation, idempotency/evidence, and checkpoint/resume.
|
||||
|
||||
The agent owns domain tools, required parameters, domain messages, domain eligibility/pre-validation, and customer-facing final responses. It must not create a parallel transaction engine.
|
||||
|
||||
## 9. New host/template checklist
|
||||
|
||||
- [ ] `AgentState` declares `active_transaction`.
|
||||
- [ ] `AgentState` declares `last_transaction`.
|
||||
- [ ] `transaction_status` and `missing_parameters` are declared when used.
|
||||
- [ ] Checkpoint provider is compatible with the state schema.
|
||||
- [ ] The same conversation identity is reused across turns.
|
||||
- [ ] New parameters are merged with previously collected arguments.
|
||||
- [ ] Pending parameter answers take precedence over generic keyword routing.
|
||||
- [ ] Explicit intent shifts remain possible.
|
||||
- [ ] Transactional agent responses propagate `transaction_state_patch(state)` where required by the template.
|
||||
- [ ] Multi-turn tests cover collection, confirmation, interruption, and resume.
|
||||
|
||||
## 10. Minimum regression tests
|
||||
|
||||
Test order cancellation with a pending `order_id`, contestation with a subject collected on the first turn and amount on the second, explicit interruption to a different intent, and checkpoint/resume using the same conversation identity.
|
||||
|
||||
## 11. Anti-patterns
|
||||
|
||||
- rebuilding the transaction from only the latest message;
|
||||
- using `selected_tool_call` as the only latch source;
|
||||
- removing `active_transaction` because it appears redundant;
|
||||
- allowing a generic keyword such as `order` to interrupt `order_id` collection;
|
||||
- keeping parameters only in node-local variables;
|
||||
- duplicating transaction confirmation in the agent prompt;
|
||||
- clearing the latch before a terminal state.
|
||||
|
||||
## 12. Project references
|
||||
|
||||
- `specs/SPEC-002-Agent-Runtime.md`
|
||||
- `specs/SPEC-010-Agent-Development.md`
|
||||
- `templates/agent_template_backend/app/state.py`
|
||||
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||
- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py`
|
||||
- `Tuning-Performance/Deterministic_Transactional_Workflow/`
|
||||
- `Tuning-Performance/Transaction_Pre_Validation/`
|
||||
- `Tuning-Performance/Transaction_Evidence/`
|
||||
@@ -1,100 +1,80 @@
|
||||
# Optional file. If this file is absent, the backend keeps using .env exactly as before.
|
||||
# If present, each inference point can override provider/model/params.
|
||||
# Put this files in the same .env file folder
|
||||
profiles:
|
||||
default:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
max_tokens: 2048
|
||||
|
||||
# Workflow/routing
|
||||
supervisor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
|
||||
|
||||
# Lightweight semantic continuity classifier. Choose the smallest/fastest
|
||||
# model approved for the environment. The framework references this profile
|
||||
# through ROUTE_STICKINESS_LLM_PROFILE.
|
||||
route_continuity:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
router:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
|
||||
# Safety / evaluation
|
||||
guardrail:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 600
|
||||
|
||||
grl:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
|
||||
judge:
|
||||
provider: oci_openai
|
||||
model: xopenai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 800
|
||||
|
||||
# RAG
|
||||
rag_rewriter:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 300
|
||||
|
||||
rag_compressor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 1200
|
||||
|
||||
rag_generation:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1800
|
||||
|
||||
# Memory / operations
|
||||
summary_memory:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1200
|
||||
|
||||
noc:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
|
||||
# Agent-specific overrides
|
||||
billing_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
|
||||
product_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
|
||||
backoffice_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
from .config_loader import load_intents, load_router_defaults, load_state_policies
|
||||
from .continuity import SemanticRouteContinuity
|
||||
from .models import IntentDefinition, RouteDecision, RouterStatePolicy
|
||||
from agent_framework.runtime.transaction_parameters import extract_transaction_parameters, parse_transaction_confirmation
|
||||
|
||||
logger = logging.getLogger("agent_framework.routing")
|
||||
|
||||
@@ -78,6 +79,12 @@ class EnterpriseRouter:
|
||||
# pendente antes de executar a nova intent.
|
||||
state_decision = self._route_by_state(current_state)
|
||||
if state_decision:
|
||||
consumed = await self._transaction_parameter_precedence(
|
||||
state, text=str(text), state_decision=state_decision
|
||||
)
|
||||
if consumed is not None:
|
||||
await self._emit(consumed, state)
|
||||
return consumed
|
||||
interruption = await self._transaction_state_interruption_candidate(
|
||||
state, text=str(text), state_decision=state_decision
|
||||
)
|
||||
@@ -109,6 +116,17 @@ class EnterpriseRouter:
|
||||
method="state",
|
||||
next_state=tx_status,
|
||||
)
|
||||
consumed = await self._transaction_parameter_precedence(
|
||||
state, text=str(text), state_decision=synthetic
|
||||
)
|
||||
if consumed is not None:
|
||||
consumed.metadata = {
|
||||
**(consumed.metadata or {}),
|
||||
"transaction_state_recovered": True,
|
||||
}
|
||||
await self._emit(consumed, state)
|
||||
return consumed
|
||||
|
||||
interruption = await self._transaction_state_interruption_candidate(
|
||||
state, text=str(text), state_decision=synthetic
|
||||
)
|
||||
@@ -186,6 +204,63 @@ class EnterpriseRouter:
|
||||
return decision
|
||||
|
||||
|
||||
async def _transaction_parameter_precedence(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
text: str,
|
||||
state_decision: RouteDecision,
|
||||
) -> RouteDecision | None:
|
||||
"""Consume a turn as transaction parameters before evaluating intent shift.
|
||||
|
||||
Only COLLECTING_PARAMETERS participates. The LLM extracts values for the
|
||||
currently missing parameters; if at least one value is found, the state
|
||||
route wins deterministically and intent-shift classification is skipped.
|
||||
"""
|
||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||
if tx_status == "AWAITING_CONFIRMATION":
|
||||
confirmation = parse_transaction_confirmation(text)
|
||||
if confirmation is None:
|
||||
return None
|
||||
state_decision.metadata = {
|
||||
**(state_decision.metadata or {}),
|
||||
"transaction_turn_consumed": True,
|
||||
"transaction_confirmation_decision": confirmation,
|
||||
"transaction_confirmation_source": "deterministic",
|
||||
}
|
||||
return state_decision
|
||||
if tx_status != "COLLECTING_PARAMETERS":
|
||||
return None
|
||||
missing = [str(name) for name in (state.get("missing_parameters") or []) if str(name).strip()]
|
||||
if not missing:
|
||||
return None
|
||||
active = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {}
|
||||
tool_name = str(active.get("tool_name") or ((state.get("selected_tool_call") or {}).get("tool_name") if isinstance(state.get("selected_tool_call"), dict) else "") or "").strip()
|
||||
if not tool_name:
|
||||
return None
|
||||
known = dict(active.get("arguments") or {})
|
||||
schema = active.get("parameter_schema") if isinstance(active.get("parameter_schema"), dict) else {}
|
||||
description = str(active.get("tool_description") or "")
|
||||
values = await extract_transaction_parameters(
|
||||
self.llm,
|
||||
text=text,
|
||||
tool_name=tool_name,
|
||||
missing_parameters=missing,
|
||||
known_arguments=known,
|
||||
parameter_schema=schema,
|
||||
tool_description=description,
|
||||
)
|
||||
if not values:
|
||||
return None
|
||||
state_decision.metadata = {
|
||||
**(state_decision.metadata or {}),
|
||||
"transaction_turn_consumed": True,
|
||||
"transaction_parameter_values": values,
|
||||
"transaction_parameter_source": "llm",
|
||||
"transaction_parameter_missing_before": missing,
|
||||
}
|
||||
return state_decision
|
||||
|
||||
async def _transaction_state_interruption_candidate(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
@@ -222,6 +297,7 @@ class EnterpriseRouter:
|
||||
"interruption_source": "configured_routing",
|
||||
}
|
||||
return candidate
|
||||
else:
|
||||
return None
|
||||
|
||||
if not (self.enable_llm_router and self.llm is not None):
|
||||
|
||||
Binary file not shown.
@@ -10,6 +10,7 @@ from typing import Any, Iterable, Mapping
|
||||
|
||||
|
||||
from agent_framework.memory.summary_memory import MemoryContext, render_recent_messages
|
||||
from agent_framework.runtime.transaction_parameters import extract_transaction_parameters, parse_transaction_confirmation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -571,6 +572,7 @@ class AgentRuntimeMixin:
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
overwrite_from_message: bool = False,
|
||||
exclude_fields: Iterable[str] = (),
|
||||
) -> dict[str, Any]:
|
||||
"""Executa regras ``extract`` declaradas para a tool escolhida.
|
||||
|
||||
@@ -586,11 +588,14 @@ class AgentRuntimeMixin:
|
||||
return dict(arguments or {})
|
||||
|
||||
resolved = dict(arguments or {})
|
||||
excluded = {str(name) for name in (exclude_fields or ())}
|
||||
runtime = self.get_runtime_context(state)
|
||||
message = runtime.sanitized_input or runtime.original_text or runtime.user_text
|
||||
llm = getattr(self, "llm", None)
|
||||
|
||||
for field_name, rule in rules.items():
|
||||
if str(field_name) in excluded:
|
||||
continue
|
||||
from_message = str(rule.get("from") or "message").lower() == "message"
|
||||
if not from_message:
|
||||
continue
|
||||
@@ -1233,46 +1238,59 @@ class AgentRuntimeMixin:
|
||||
|
||||
@staticmethod
|
||||
def _confirmation_decision(text: str) -> str | None:
|
||||
normalized = " ".join((text or "").strip().lower().split())
|
||||
normalized = re.sub(r"[.!?]+$", "", normalized).strip()
|
||||
if normalized in {"sim", "confirmo", "sim, confirmo", "pode fazer", "pode prosseguir", "sim, desejo", "sim, desejo trocar", "sim, confirmo a devolução", "sim, confirmo a troca"}:
|
||||
return "confirm"
|
||||
if normalized in {"não", "nao", "cancelar", "cancele", "não confirmo", "nao confirmo"}:
|
||||
return "reject"
|
||||
return None
|
||||
return parse_transaction_confirmation(text)
|
||||
|
||||
@staticmethod
|
||||
def _extract_action_arguments(text: str) -> dict[str, Any]:
|
||||
"""Extrai apenas entidades explicitamente informadas na mensagem.
|
||||
def _transaction_parameter_schema(self, tool_name: str, policy: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Return generic schema metadata for transactional required parameters."""
|
||||
cfg = self._tool_config(tool_name)
|
||||
raw_schema = dict(getattr(cfg, "args_schema", {}) or {}) if cfg is not None else {}
|
||||
required = [str(name) for name in ((policy or {}).get("requires") or getattr(cfg, "requires", []) or [])]
|
||||
if not required:
|
||||
return raw_schema
|
||||
return {name: raw_schema.get(name, "string") for name in required}
|
||||
|
||||
Não usa a mensagem inteira como ``reason``: frases como "quero devolver
|
||||
uma compra" expressam a ação, mas não necessariamente o motivo. Defaults
|
||||
declarados no mapper continuam sendo aplicados por ``build_tool_arguments``.
|
||||
def _transaction_tool_description(self, tool_name: str) -> str:
|
||||
cfg = self._tool_config(tool_name)
|
||||
return str(getattr(cfg, "description", "") or "") if cfg is not None else ""
|
||||
|
||||
async def _extract_transaction_parameters(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
tool_name: str,
|
||||
missing_parameters: list[str],
|
||||
known_arguments: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Use the dedicated LLM extractor for pending transaction parameters.
|
||||
|
||||
A route decision may already contain the extraction performed by the
|
||||
router solely to enforce parameter-before-intent-shift precedence. Reuse
|
||||
it to avoid a second LLM call in the same turn.
|
||||
"""
|
||||
raw = text or ""
|
||||
args: dict[str, Any] = {}
|
||||
match = re.search(
|
||||
r"(?:pedido|ordem)\s*(?:n[ºo°.]?\s*)?(?:é\s*(?:o\s*)?|[:#=-]\s*)?([A-Za-z0-9_-]+)",
|
||||
raw,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if match:
|
||||
args["order_id"] = match.group(1)
|
||||
route_meta = ((state.get("route_decision") or {}).get("metadata") or {}) if isinstance(state.get("route_decision"), dict) else {}
|
||||
cached = route_meta.get("transaction_parameter_values")
|
||||
if isinstance(cached, dict):
|
||||
allowed = set(str(x) for x in missing_parameters)
|
||||
reused = {str(k): v for k, v in cached.items() if str(k) in allowed and v not in _EMPTY_VALUES}
|
||||
if reused:
|
||||
return reused
|
||||
|
||||
reason_match = re.search(
|
||||
r"(?:porque|pois|motivo\s*[:=-]?|por\s+(?:arrependimento|defeito|erro|atraso)|me\s+arrependi(?:\s+da\s+compra)?|arrependimento)\s*(.*)",
|
||||
raw,
|
||||
flags=re.IGNORECASE,
|
||||
active = self._active_transaction(state) or {}
|
||||
schema = active.get("parameter_schema") if isinstance(active.get("parameter_schema"), dict) else None
|
||||
if not schema:
|
||||
policy = self._resolve_tool_execution_policy(tool_name, known_arguments or {})
|
||||
schema = self._transaction_parameter_schema(tool_name, policy)
|
||||
description = str(active.get("tool_description") or self._transaction_tool_description(tool_name) or "")
|
||||
text = state.get("sanitized_input") or state.get("user_text") or ""
|
||||
return await extract_transaction_parameters(
|
||||
getattr(self, "llm", None),
|
||||
text=str(text),
|
||||
tool_name=tool_name,
|
||||
missing_parameters=list(missing_parameters or []),
|
||||
known_arguments=known_arguments or {},
|
||||
parameter_schema=schema,
|
||||
tool_description=description,
|
||||
)
|
||||
if reason_match:
|
||||
reason = reason_match.group(1).strip(" .,:;-")
|
||||
if not reason:
|
||||
matched_phrase = reason_match.group(0).strip(" .,:;-")
|
||||
if re.search(r"me\s+arrependi|arrependimento", matched_phrase, flags=re.IGNORECASE):
|
||||
reason = "Arrependimento da compra"
|
||||
if reason:
|
||||
args["reason"] = reason
|
||||
return args
|
||||
|
||||
def _transactional_action_match(self, text: str, tools: list[str] | None = None) -> str | None:
|
||||
"""Detecta solicitação transacional usando metadados de tools.yaml.
|
||||
@@ -1515,12 +1533,19 @@ class AgentRuntimeMixin:
|
||||
) -> dict[str, Any]:
|
||||
current = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {}
|
||||
txid = transaction_id or current.get("transaction_id") or str(uuid.uuid4())
|
||||
if str(current.get("tool_name") or "") != str(tool_name):
|
||||
state["transaction_pre_validation"] = None
|
||||
cfg = self._tool_config(tool_name)
|
||||
policy = self._resolve_tool_execution_policy(tool_name, arguments or {})
|
||||
tx = {
|
||||
"transaction_id": txid,
|
||||
"tool_name": tool_name,
|
||||
"arguments": dict(arguments or {}),
|
||||
"status": status,
|
||||
"started_from_intent": current.get("started_from_intent") or state.get("intent"),
|
||||
"requires": list(policy.get("requires") or getattr(cfg, "requires", []) or []),
|
||||
"parameter_schema": self._transaction_parameter_schema(tool_name, policy),
|
||||
"tool_description": self._transaction_tool_description(tool_name),
|
||||
}
|
||||
state["active_transaction"] = tx
|
||||
return tx
|
||||
@@ -2057,6 +2082,7 @@ class AgentRuntimeMixin:
|
||||
if active_before_interruption and interruption == "intent_shift":
|
||||
interrupted_tool = active_before_interruption.get("tool_name")
|
||||
self._finish_active_transaction(state, "CANCELLED")
|
||||
state["transaction_pre_validation"] = None
|
||||
state["tool_policy_result"] = {
|
||||
"action": "cancelled_by_intent_shift",
|
||||
"tool_name": interrupted_tool,
|
||||
@@ -2080,28 +2106,40 @@ class AgentRuntimeMixin:
|
||||
tool_name = selected.get("tool_name")
|
||||
if tool_name:
|
||||
previous_args = dict(selected.get("arguments") or {})
|
||||
new_args = self.build_tool_arguments(
|
||||
policy = self._resolve_tool_execution_policy(tool_name, previous_args)
|
||||
missing_before = self._missing_required_arguments(policy, previous_args)
|
||||
|
||||
# Parâmetros TRANSACIONAIS são interpretados exclusivamente pelo
|
||||
# extrator LLM genérico. Não existem regexes/nome de entidade
|
||||
# hardcoded no framework. O extrator recebe apenas os parâmetros
|
||||
# ainda pendentes da policy e pode consumir um ou vários no turno.
|
||||
extracted = await self._extract_transaction_parameters(
|
||||
state,
|
||||
tool_name=tool_name,
|
||||
intent=state.get("intent"),
|
||||
aliases=aliases,
|
||||
extra_args=self._extract_action_arguments(text),
|
||||
missing_parameters=missing_before,
|
||||
known_arguments=previous_args,
|
||||
)
|
||||
# Durante coleta incremental, valores de contexto podem ainda conter
|
||||
# parâmetros de uma operação anterior. O que já foi coletado para a
|
||||
# transação pendente prevalece; o turno atual só preenche lacunas.
|
||||
non_empty_new = {k: v for k, v in new_args.items() if v not in (None, "", [], {})}
|
||||
arguments = {**non_empty_new, **previous_args}
|
||||
arguments = {**previous_args, **extracted}
|
||||
|
||||
# Campos de envelope pertencem ao turno corrente e devem permanecer
|
||||
# atualizados, mesmo quando os parâmetros de negócio ficam congelados.
|
||||
for per_turn_key in ("query", "operator_instructions", "interaction_key"):
|
||||
if non_empty_new.get(per_turn_key) not in (None, "", [], {}):
|
||||
arguments[per_turn_key] = non_empty_new[per_turn_key]
|
||||
|
||||
# Reutiliza o contrato declarativo para preencher somente os campos
|
||||
# ainda faltantes; campos previamente coletados não são sobrescritos.
|
||||
arguments = await self._extract_mcp_parameters(tool_name, arguments, state)
|
||||
# Argumentos estruturados já presentes no contexto são aceitos de
|
||||
# forma genérica (não são parsing textual). Para required fields,
|
||||
# só completam lacunas que a fala atual/LLM não preencheu; valores
|
||||
# previamente coletados nunca são sobrescritos.
|
||||
contextual = self.build_tool_arguments(
|
||||
state, tool_name=tool_name, intent=state.get("intent"), aliases=aliases
|
||||
)
|
||||
required_set = set(str(name) for name in (policy.get("requires") or []))
|
||||
for key, value in contextual.items():
|
||||
if value in _EMPTY_VALUES:
|
||||
continue
|
||||
if key in required_set:
|
||||
if arguments.get(key) in _EMPTY_VALUES:
|
||||
arguments[key] = value
|
||||
else:
|
||||
arguments[key] = value
|
||||
arguments = await self._extract_mcp_parameters(
|
||||
tool_name, arguments, state, exclude_fields=policy.get("requires") or []
|
||||
)
|
||||
policy = self._resolve_tool_execution_policy(tool_name, arguments)
|
||||
missing = self._missing_required_arguments(policy, arguments)
|
||||
if missing:
|
||||
@@ -2250,23 +2288,46 @@ class AgentRuntimeMixin:
|
||||
if not selected_action:
|
||||
return results
|
||||
|
||||
explicit_action_args = self._extract_action_arguments(text)
|
||||
action_args = self.build_tool_arguments(
|
||||
state,
|
||||
tool_name=selected_action,
|
||||
intent=state.get("intent"),
|
||||
aliases=aliases,
|
||||
extra_args=explicit_action_args,
|
||||
)
|
||||
# Nova transação: parâmetros declarados ``from: message`` não podem ser
|
||||
# herdados de context.tool_arguments de uma operação anterior.
|
||||
# Campos que o contrato MCP declara como vindos da mensagem corrente não
|
||||
# podem herdar valores textuais de uma transação anterior. Isto é apenas
|
||||
# uma regra de freshness do envelope MCP; a extração de policy.requires
|
||||
# continua exclusivamente no TransactionParameterExtractor LLM abaixo.
|
||||
action_args = self._drop_stale_message_extracted_arguments(
|
||||
selected_action, action_args, explicit_fields=explicit_action_args.keys()
|
||||
selected_action, action_args, explicit_fields=()
|
||||
)
|
||||
# A mensagem atual é a fonte de verdade para esses campos no primeiro
|
||||
# turno transacional.
|
||||
policy = self._resolve_tool_execution_policy(selected_action, action_args)
|
||||
required = [str(name) for name in (policy.get("requires") or [])]
|
||||
|
||||
# Valores já estruturados no contexto podem satisfazer requirements sem
|
||||
# parsing textual. Para qualquer required field ainda ausente, a fala do
|
||||
# usuário é interpretada exclusivamente pelo extrator LLM transacional.
|
||||
missing_initial = self._missing_required_arguments(policy, action_args)
|
||||
# No primeiro turno, a fala atual pode fornecer/corrigir qualquer required
|
||||
# field, inclusive um valor que exista no contexto estruturado mas pertença
|
||||
# a uma transação anterior. O extrator continua restrito ao contrato
|
||||
# ``requires`` e só sobrescreve quando a LLM realmente extrai um valor.
|
||||
extracted_initial = await self._extract_transaction_parameters(
|
||||
state,
|
||||
tool_name=selected_action,
|
||||
missing_parameters=required,
|
||||
known_arguments={k: v for k, v in action_args.items() if k not in set(required)},
|
||||
)
|
||||
action_args.update(extracted_initial)
|
||||
|
||||
# O mapper MCP continua responsável somente por parâmetros auxiliares que
|
||||
# não pertencem ao contrato transacional.
|
||||
action_args = await self._extract_mcp_parameters(
|
||||
selected_action, action_args, state, overwrite_from_message=True
|
||||
selected_action,
|
||||
action_args,
|
||||
state,
|
||||
overwrite_from_message=True,
|
||||
exclude_fields=required,
|
||||
)
|
||||
policy = self._resolve_tool_execution_policy(selected_action, action_args)
|
||||
selected = {"tool_name": selected_action, "arguments": action_args}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
def confirmation_decision(text: str) -> str | None:
|
||||
"""Classifica respostas explícitas ao estado AWAITING_CONFIRMATION.
|
||||
|
||||
Esta função é compartilhada pelo router (precedência antes de intent_shift)
|
||||
e pelo runtime (execução/cancelamento efetivo), garantindo que ambos
|
||||
reconheçam exatamente o mesmo conjunto de respostas.
|
||||
"""
|
||||
normalized = " ".join((text or "").strip().lower().split())
|
||||
normalized = re.sub(r"[.!?]+$", "", normalized).strip()
|
||||
if normalized in {
|
||||
"sim",
|
||||
"confirmo",
|
||||
"sim, confirmo",
|
||||
"pode fazer",
|
||||
"pode prosseguir",
|
||||
"sim, desejo",
|
||||
"sim, desejo trocar",
|
||||
"sim, confirmo a devolução",
|
||||
"sim, confirmo a troca",
|
||||
}:
|
||||
return "confirm"
|
||||
if normalized in {"não", "nao", "cancelar", "cancele", "não confirmo", "nao confirmo"}:
|
||||
return "reject"
|
||||
return None
|
||||
|
||||
|
||||
def extract_action_arguments(text: str) -> dict[str, Any]:
|
||||
"""Extrai entidades explicitamente informadas em ações transacionais.
|
||||
|
||||
É usada tanto pelo runtime quanto pelo probe de precedência do router. Não
|
||||
transforma a mensagem inteira em motivo: só captura valores explicitamente
|
||||
identificáveis no turno atual.
|
||||
"""
|
||||
raw = text or ""
|
||||
args: dict[str, Any] = {}
|
||||
match = re.search(
|
||||
r"(?:pedido|ordem)\s*(?:n[ºo°.]?\s*)?(?:é\s*(?:o\s*)?|[:#=-]\s*)?([A-Za-z0-9_-]+)",
|
||||
raw,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if match:
|
||||
args["order_id"] = match.group(1)
|
||||
|
||||
reason_match = re.search(
|
||||
r"(?:porque|pois|motivo\s*[:=-]?|por\s+(?:arrependimento|defeito|erro|atraso)|me\s+arrependi(?:\s+da\s+compra)?|arrependimento)\s*(.*)",
|
||||
raw,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if reason_match:
|
||||
reason = reason_match.group(1).strip(" .,:;-")
|
||||
if not reason:
|
||||
matched_phrase = reason_match.group(0).strip(" .,:;-")
|
||||
if re.search(r"me\s+arrependi|arrependimento", matched_phrase, flags=re.IGNORECASE):
|
||||
reason = "Arrependimento da compra"
|
||||
if reason:
|
||||
args["reason"] = reason
|
||||
return args
|
||||
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Mapping
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_EMPTY_VALUES = (None, "", {}, [])
|
||||
|
||||
|
||||
def _response_text(response: Any) -> str:
|
||||
if response is None:
|
||||
return ""
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
if isinstance(response, dict):
|
||||
return str(response.get("content") or response.get("text") or response.get("answer") or "")
|
||||
return str(getattr(response, "content", None) or getattr(response, "text", None) or response)
|
||||
|
||||
|
||||
def _coerce(value: Any, declared_type: Any) -> Any:
|
||||
if value in _EMPTY_VALUES:
|
||||
return None
|
||||
type_name = str(declared_type or "string").strip().lower()
|
||||
try:
|
||||
if type_name in {"integer", "int"}:
|
||||
return int(value)
|
||||
if type_name in {"number", "float", "double"}:
|
||||
return float(value)
|
||||
if type_name in {"boolean", "bool"}:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
normalized = str(value).strip().lower()
|
||||
if normalized in {"true", "1", "yes", "sim"}:
|
||||
return True
|
||||
if normalized in {"false", "0", "no", "não", "nao"}:
|
||||
return False
|
||||
return None
|
||||
if type_name in {"array", "list"}:
|
||||
return value if isinstance(value, list) else [value]
|
||||
if type_name in {"object", "dict", "map"}:
|
||||
return value if isinstance(value, dict) else None
|
||||
return str(value).strip()
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_transaction_confirmation(text: str) -> str | None:
|
||||
"""Recognize an explicit confirmation/rejection before intent-shift routing.
|
||||
|
||||
This is intentionally small and domain-neutral. Parameter interpretation is
|
||||
LLM-only; confirmation remains a deterministic control token so an explicit
|
||||
yes/no cannot be reclassified as a new intent.
|
||||
"""
|
||||
normalized = " ".join(str(text or "").strip().lower().split())
|
||||
normalized = re.sub(r"[.!?]+$", "", normalized).strip()
|
||||
if normalized in {
|
||||
"sim", "confirmo", "sim, confirmo", "pode fazer", "pode prosseguir",
|
||||
"sim, desejo", "sim, desejo trocar", "sim, confirmo a devolução",
|
||||
"sim, confirmo a troca",
|
||||
}:
|
||||
return "confirm"
|
||||
if normalized in {"não", "nao", "cancelar", "cancele", "não confirmo", "nao confirmo"}:
|
||||
return "reject"
|
||||
return None
|
||||
|
||||
|
||||
async def extract_transaction_parameters(
|
||||
llm: Any,
|
||||
*,
|
||||
text: str,
|
||||
tool_name: str,
|
||||
missing_parameters: list[str],
|
||||
known_arguments: Mapping[str, Any] | None = None,
|
||||
parameter_schema: Mapping[str, Any] | None = None,
|
||||
tool_description: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Extract values for pending transactional parameters using the LLM only.
|
||||
|
||||
This component intentionally contains no domain/entity regexes and no
|
||||
knowledge of parameter names such as ``order_id`` or ``reason``. The
|
||||
transaction runtime supplies the pending parameter names and optional schema;
|
||||
the LLM only interprets the current user turn. State/control-flow decisions
|
||||
remain deterministic outside this function.
|
||||
"""
|
||||
pending = [str(name) for name in (missing_parameters or []) if str(name).strip()]
|
||||
message = str(text or "").strip()
|
||||
if not pending or not message or llm is None:
|
||||
return {}
|
||||
|
||||
schema = dict(parameter_schema or {})
|
||||
known = {
|
||||
str(key): value
|
||||
for key, value in dict(known_arguments or {}).items()
|
||||
if value not in _EMPTY_VALUES and str(key) not in pending
|
||||
}
|
||||
field_spec = {
|
||||
name: {
|
||||
"type": schema.get(name, "string") if not isinstance(schema.get(name), dict) else schema.get(name, {}).get("type", "string"),
|
||||
"description": None if not isinstance(schema.get(name), dict) else schema.get(name, {}).get("description"),
|
||||
}
|
||||
for name in pending
|
||||
}
|
||||
output_shape = {name: None for name in pending}
|
||||
prompt = (
|
||||
"Você extrai parâmetros PENDENTES de uma transação ativa. "
|
||||
"Sua única tarefa é interpretar a mensagem atual e devolver valores para os parâmetros pendentes. "
|
||||
"Não decida roteamento, intenção, confirmação ou execução da transação.\n\n"
|
||||
"REGRAS OBRIGATÓRIAS:\n"
|
||||
"1. Extraia SOMENTE parâmetros listados em pending_parameters.\n"
|
||||
"2. Não invente valores e não transforme uma nova solicitação/intenção do usuário em valor de parâmetro.\n"
|
||||
"3. Se nenhum parâmetro pendente foi realmente informado, devolva null para todos.\n"
|
||||
"4. Se houver apenas um parâmetro pendente, uma resposta contendo apenas um valor pode ser associada a ele quando isso for semanticamente inequívoco.\n"
|
||||
"5. Se houver vários parâmetros pendentes, extraia todos os que estiverem presentes no mesmo turno.\n"
|
||||
"6. O nome do parâmetro não precisa aparecer literalmente na fala; use a semântica, o nome da transação e o schema para associar valores.\n"
|
||||
"7. Em caso de dúvida, prefira null.\n"
|
||||
"8. Responda SOMENTE JSON válido, sem markdown, sem explicação e sem chaves extras.\n\n"
|
||||
f"transaction_tool: {tool_name}\n"
|
||||
f"transaction_description: {tool_description or ''}\n"
|
||||
f"pending_parameters: {json.dumps(pending, ensure_ascii=False)}\n"
|
||||
f"parameter_schema: {json.dumps(field_spec, ensure_ascii=False, default=str)}\n"
|
||||
f"known_arguments: {json.dumps(known, ensure_ascii=False, default=str)}\n"
|
||||
f"user_message: {message}\n"
|
||||
f"Formato obrigatório: {json.dumps(output_shape, ensure_ascii=False)}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = await llm.ainvoke(
|
||||
[{"role": "user", "content": prompt}],
|
||||
profile_name="transaction_parameter_extraction",
|
||||
component_name="transaction_parameter_extraction",
|
||||
generation_name="llm.transaction_parameter_extraction",
|
||||
temperature=0.0,
|
||||
max_tokens=max(120, min(500, 80 + 60 * len(pending))),
|
||||
)
|
||||
except TypeError:
|
||||
# Compatibilidade com doubles/testes e providers mínimos que aceitam
|
||||
# apenas messages.
|
||||
response = await llm.ainvoke([{"role": "user", "content": prompt}])
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"transaction.parameter.llm_extract_failed tool=%s pending=%s error=%s",
|
||||
tool_name,
|
||||
pending,
|
||||
exc,
|
||||
)
|
||||
return {}
|
||||
|
||||
raw = _response_text(response).strip()
|
||||
if raw.startswith("```"):
|
||||
raw = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.IGNORECASE | re.DOTALL).strip()
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except (TypeError, ValueError, json.JSONDecodeError):
|
||||
logger.warning(
|
||||
"transaction.parameter.llm_invalid_json tool=%s pending=%s raw=%r",
|
||||
tool_name,
|
||||
pending,
|
||||
raw[:240],
|
||||
)
|
||||
return {}
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
|
||||
extracted: dict[str, Any] = {}
|
||||
for name in pending:
|
||||
value = payload.get(name)
|
||||
declared = field_spec.get(name, {}).get("type", "string")
|
||||
coerced = _coerce(value, declared)
|
||||
if coerced not in _EMPTY_VALUES:
|
||||
extracted[name] = coerced
|
||||
|
||||
logger.info(
|
||||
"transaction.parameter.llm_extracted tool=%s pending=%s consumed=%s",
|
||||
tool_name,
|
||||
pending,
|
||||
sorted(extracted),
|
||||
)
|
||||
return extracted
|
||||
@@ -208,6 +208,21 @@ rag:
|
||||
| `RUNTIME_TIMEOUT` | Timeout geral | resposta controlada |
|
||||
|
||||
|
||||
|
||||
## Contrato Durável de Estado Transacional
|
||||
|
||||
Hosts que utilizam `AgentRuntime` com transações multi-turno DEVEM declarar no `AgentState` os campos `active_transaction` e `last_transaction`. O primeiro é a fonte canônica da transação em andamento e deve sobreviver a checkpoint/resume; o segundo mantém o snapshot da última transação terminal.
|
||||
|
||||
```python
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
```
|
||||
|
||||
`selected_tool_call` e `pending_tool_call` são campos auxiliares/compatibilidade e não substituem o latch canônico. Durante `COLLECTING_PARAMETERS`, a retomada da transação e o consumo de parâmetros pendentes têm precedência sobre keyword routing genérico. Uma mudança de intenção só deve interromper a transação quando for inequívoca ou explicitamente solicitada pelo usuário.
|
||||
|
||||
O contrato completo, ciclo de vida, precedência de roteamento, checklist e testes regressivos estão em [`docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md`](../docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md).
|
||||
|
||||
|
||||
## Requisitos Não Funcionais
|
||||
|
||||
| Categoria | Requisito |
|
||||
@@ -233,6 +248,8 @@ rag:
|
||||
- [ ] Output guardrails executam antes da resposta final.
|
||||
- [ ] Judges geram JudgeResult.
|
||||
- [ ] Memória e checkpoint são persistidos conforme provider.
|
||||
- [ ] Hosts transacionais declaram `active_transaction` e `last_transaction` no `AgentState`.
|
||||
- [ ] Durante `COLLECTING_PARAMETERS`, respostas a parâmetros pendentes têm precedência sobre keyword routing genérico.
|
||||
- [ ] Erros geram NOC e resposta controlada.
|
||||
|
||||
|
||||
|
||||
@@ -215,6 +215,21 @@ dataset:
|
||||
groundedness: 0.70
|
||||
```
|
||||
|
||||
|
||||
## Contrato obrigatório para agentes transacionais
|
||||
|
||||
Ao criar um agente que usa tools transacionais do framework, o desenvolvedor não deve criar um motor paralelo de coleta/confirmação. Deve reutilizar `AgentRuntime` e garantir que o `AgentState` do host mantenha o latch durável:
|
||||
|
||||
```python
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
```
|
||||
|
||||
Durante uma transação ativa, parâmetros já coletados são preservados e novos valores são mesclados incrementalmente. Em `COLLECTING_PARAMETERS`, uma resposta que satisfaz um parâmetro pendente tem precedência sobre keywords genéricas. Mudanças de intenção explícitas e inequívocas continuam permitidas.
|
||||
|
||||
Antes de publicar um novo template/host, execute os cenários multi-turno descritos no [`Transaction State Developer Guide`](../docs/TRANSACTION_STATE_DEVELOPER_GUIDE.md).
|
||||
|
||||
|
||||
## Testes
|
||||
|
||||
| Teste | Escopo |
|
||||
@@ -273,6 +288,7 @@ dataset:
|
||||
## Critérios de Aceite
|
||||
|
||||
- [ ] Novo agente é criado sem alterar core do framework.
|
||||
- [ ] Se houver transações multi-turno, `AgentState` declara `active_transaction` e `last_transaction`.
|
||||
- [ ] Configuração ocorre por YAML e `.env`.
|
||||
- [ ] Agente usa BusinessContext.
|
||||
- [ ] Agente acessa MCP por router/gateway.
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
transaction_pre_validation: dict[str, Any]
|
||||
transaction_evidence: list[dict[str, Any]]
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
available_mcp_tools: list[str]
|
||||
selected_tool_call: dict[str, Any]
|
||||
pending_tool_call: dict[str, Any]
|
||||
active_transaction: dict[str, Any]
|
||||
last_transaction: dict[str, Any]
|
||||
transaction_status: str
|
||||
transaction_pre_validation: dict[str, Any]
|
||||
confirmation_required: bool
|
||||
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
293
tests/test_transaction_parameter_llm_precedence.py
Normal file
293
tests/test_transaction_parameter_llm_precedence.py
Normal file
@@ -0,0 +1,293 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework.routing.enterprise_router import EnterpriseRouter
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
|
||||
class _SemanticLLM:
|
||||
"""Test double: parameter extraction + intent-shift classification."""
|
||||
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
|
||||
profile = kwargs.get("profile_name")
|
||||
if profile == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
marker = "user_message: "
|
||||
user = prompt.split(marker, 1)[1].split("\nFormato obrigatório:", 1)[0].strip() if marker in prompt else ""
|
||||
pending_raw = prompt.split("pending_parameters: ", 1)[1].split("\n", 1)[0]
|
||||
pending = json.loads(pending_raw)
|
||||
values = {name: None for name in pending}
|
||||
low = user.lower()
|
||||
if "ped-1001" in low and "order_id" in values:
|
||||
values["order_id"] = "PED-1001"
|
||||
if "desisti" in low and "reason" in values:
|
||||
values["reason"] = "desisti da compra"
|
||||
if low.strip() == "71,99" and "valor" in values:
|
||||
values["valor"] = 71.99
|
||||
if low.strip() == "tim music" and "subject" in values:
|
||||
values["subject"] = "TIM Music"
|
||||
return json.dumps(values, ensure_ascii=False)
|
||||
|
||||
# Router LLM fallback: treat fatura as a real intent shift.
|
||||
if "fatura" in prompt.lower():
|
||||
return json.dumps({
|
||||
"decision": "SHIFT",
|
||||
"intent": "billing_invoice_explanation",
|
||||
"agent": "billing_agent",
|
||||
"confidence": 0.98,
|
||||
"reason": "nova intenção de fatura",
|
||||
})
|
||||
return json.dumps({
|
||||
"decision": "CONTINUE",
|
||||
"intent": None,
|
||||
"agent": None,
|
||||
"confidence": 0.95,
|
||||
"reason": "continua transação",
|
||||
})
|
||||
|
||||
|
||||
class _Router:
|
||||
def __init__(self):
|
||||
self.registry = SimpleNamespace(
|
||||
tools={},
|
||||
get_tool=self.get_tool,
|
||||
)
|
||||
|
||||
def get_tool(self, name):
|
||||
data = {
|
||||
"solicitar_devolucao": SimpleNamespace(
|
||||
name="solicitar_devolucao",
|
||||
description="Abre uma solicitação de devolução de pedido.",
|
||||
selection_keywords=["devolver pedido", "devolução", "devolver"],
|
||||
args_schema={"order_id": "string", "reason": "string"},
|
||||
requires=["order_id", "reason"],
|
||||
confirmation_required=True,
|
||||
tool_type="action",
|
||||
),
|
||||
"cancelar_pedido": SimpleNamespace(
|
||||
name="cancelar_pedido",
|
||||
description="Cancela um pedido.",
|
||||
selection_keywords=["cancelar pedido", "cancelar compra"],
|
||||
args_schema={"order_id": "string"},
|
||||
requires=["order_id"],
|
||||
confirmation_required=True,
|
||||
tool_type="action",
|
||||
),
|
||||
}
|
||||
return data.get(name)
|
||||
|
||||
def resolve_execution_policy(self, tool_name, arguments=None):
|
||||
cfg = self.get_tool(tool_name)
|
||||
if not cfg:
|
||||
return {"operation_type": "read_only", "require_confirmation": False, "requires": []}
|
||||
return {
|
||||
"operation_type": "transactional",
|
||||
"require_confirmation": True,
|
||||
"requires": list(cfg.requires),
|
||||
"policy_source": "test",
|
||||
}
|
||||
|
||||
def parameter_extract_rules(self, tool_name):
|
||||
# Deliberately has MCP mappings for the same fields: transactional fields
|
||||
# must be excluded from this mechanism by the runtime.
|
||||
return {
|
||||
"order_id": {"from": "message", "strategy": "regex", "pattern": r"pedido\\s+(\\w+)"},
|
||||
"reason": {"from": "message", "strategy": "regex", "pattern": r"motivo\\s+(.+)"},
|
||||
}
|
||||
|
||||
def validate_execution_policy(self, tool_name, arguments=None):
|
||||
return True, None, self.resolve_execution_policy(tool_name, arguments)
|
||||
|
||||
|
||||
class _Runtime(AgentRuntimeMixin):
|
||||
def __init__(self):
|
||||
self.tool_router = _Router()
|
||||
self.llm = _SemanticLLM()
|
||||
self.calls = []
|
||||
|
||||
async def _call_mcp_tool(self, tool_name, arguments, state):
|
||||
self.calls.append((tool_name, dict(arguments)))
|
||||
return {"ok": True, "tool_name": tool_name, "result": {"status": "OK"}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transaction_extractor_handles_multiple_parameters_without_hardcoded_regex():
|
||||
runtime = _Runtime()
|
||||
state = {
|
||||
"user_text": "quero devolver pedido PED-1001 porque desisti da compra",
|
||||
"sanitized_input": "quero devolver pedido PED-1001 porque desisti da compra",
|
||||
"mcp_tools": ["solicitar_devolucao"],
|
||||
"route": "support_agent",
|
||||
"intent": "retail_support_exchange_return",
|
||||
}
|
||||
result = await runtime.execute_tools_for_intent(state)
|
||||
assert result[-1]["awaiting_confirmation"] is True
|
||||
assert state["transaction_status"] == "AWAITING_CONFIRMATION"
|
||||
args = state["pending_tool_call"]["arguments"]
|
||||
assert args["order_id"] == "PED-1001"
|
||||
assert args["reason"] == "desisti da compra"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collecting_one_parameter_consumes_turn_before_intent_shift(tmp_path):
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(
|
||||
"""
|
||||
router:
|
||||
fallback_agent: support_agent
|
||||
confidence_threshold: 0.70
|
||||
state_policies:
|
||||
- state: COLLECTING_SUPPORT_PARAMETERS
|
||||
agent: support_agent
|
||||
intents:
|
||||
- name: retail_order_tracking
|
||||
agent: orders_agent
|
||||
priority: 20
|
||||
keywords: [pedido]
|
||||
- name: retail_support_exchange_return
|
||||
agent: support_agent
|
||||
priority: 30
|
||||
keywords: [devolver pedido]
|
||||
- name: billing_invoice_explanation
|
||||
agent: billing_agent
|
||||
priority: 40
|
||||
keywords: [fatura]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
ROUTING_CONFIG_PATH=str(routing),
|
||||
ENABLE_LLM_ROUTER=True,
|
||||
ENABLE_ROUTE_STICKINESS=False,
|
||||
)
|
||||
router = EnterpriseRouter(settings, llm=_SemanticLLM())
|
||||
state = {
|
||||
"user_text": "o numero do pedido é PED-1001",
|
||||
"sanitized_input": "o numero do pedido é PED-1001",
|
||||
"next_state": "COLLECTING_SUPPORT_PARAMETERS",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"missing_parameters": ["order_id", "reason"],
|
||||
"active_agent": "support_agent",
|
||||
"intent": "state:COLLECTING_SUPPORT_PARAMETERS",
|
||||
"active_transaction": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {},
|
||||
"status": "COLLECTING_PARAMETERS",
|
||||
"started_from_intent": "retail_support_exchange_return",
|
||||
"parameter_schema": {"order_id": "string", "reason": "string"},
|
||||
"tool_description": "Abre uma solicitação de devolução de pedido.",
|
||||
},
|
||||
}
|
||||
decision = await router.route(state)
|
||||
assert decision.agent == "support_agent"
|
||||
assert decision.intent == "state:COLLECTING_SUPPORT_PARAMETERS"
|
||||
assert decision.metadata["transaction_turn_consumed"] is True
|
||||
assert decision.metadata["transaction_parameter_values"] == {"order_id": "PED-1001"}
|
||||
assert "transaction_interruption" not in decision.metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_parameter_found_allows_intent_shift(tmp_path):
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(
|
||||
"""
|
||||
router:
|
||||
fallback_agent: support_agent
|
||||
confidence_threshold: 0.70
|
||||
state_policies:
|
||||
- state: COLLECTING_SUPPORT_PARAMETERS
|
||||
agent: support_agent
|
||||
intents:
|
||||
- name: retail_support_exchange_return
|
||||
agent: support_agent
|
||||
priority: 20
|
||||
keywords: [devolver pedido]
|
||||
- name: billing_invoice_explanation
|
||||
agent: billing_agent
|
||||
priority: 40
|
||||
keywords: [fatura]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
ROUTING_CONFIG_PATH=str(routing),
|
||||
ENABLE_LLM_ROUTER=True,
|
||||
ENABLE_ROUTE_STICKINESS=False,
|
||||
)
|
||||
router = EnterpriseRouter(settings, llm=_SemanticLLM())
|
||||
state = {
|
||||
"user_text": "esquece isso, quero ver minha fatura",
|
||||
"sanitized_input": "esquece isso, quero ver minha fatura",
|
||||
"next_state": "COLLECTING_SUPPORT_PARAMETERS",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"missing_parameters": ["order_id", "reason"],
|
||||
"active_agent": "support_agent",
|
||||
"intent": "state:COLLECTING_SUPPORT_PARAMETERS",
|
||||
"active_transaction": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {},
|
||||
"status": "COLLECTING_PARAMETERS",
|
||||
"started_from_intent": "retail_support_exchange_return",
|
||||
"parameter_schema": {"order_id": "string", "reason": "string"},
|
||||
},
|
||||
}
|
||||
decision = await router.route(state)
|
||||
assert decision.intent == "billing_invoice_explanation"
|
||||
assert decision.agent == "billing_agent"
|
||||
assert decision.metadata["transaction_interruption"] == "intent_shift"
|
||||
|
||||
|
||||
def test_hardcoded_action_argument_extractor_removed():
|
||||
from pathlib import Path
|
||||
source = Path("libs/agent_framework/src/agent_framework/runtime/agent_runtime.py").read_text(encoding="utf-8")
|
||||
assert "def _extract_action_arguments" not in source
|
||||
assert "pedido|ordem" not in source
|
||||
assert "reason_match" not in source
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_is_consumed_before_intent_shift(tmp_path):
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(
|
||||
"""
|
||||
router:
|
||||
fallback_agent: support_agent
|
||||
confidence_threshold: 0.70
|
||||
state_policies:
|
||||
- state: WAITING_SUPPORT_CONFIRMATION
|
||||
agent: support_agent
|
||||
intents:
|
||||
- name: generic_yes_intent
|
||||
agent: other_agent
|
||||
priority: 50
|
||||
keywords: [sim]
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings = SimpleNamespace(
|
||||
ROUTING_CONFIG_PATH=str(routing),
|
||||
ENABLE_LLM_ROUTER=True,
|
||||
ENABLE_ROUTE_STICKINESS=False,
|
||||
)
|
||||
router = EnterpriseRouter(settings, llm=_SemanticLLM())
|
||||
state = {
|
||||
"user_text": "sim",
|
||||
"sanitized_input": "sim",
|
||||
"next_state": "WAITING_SUPPORT_CONFIRMATION",
|
||||
"transaction_status": "AWAITING_CONFIRMATION",
|
||||
"active_agent": "support_agent",
|
||||
"active_transaction": {
|
||||
"tool_name": "solicitar_devolucao",
|
||||
"arguments": {"order_id": "PED-1001", "reason": "desisti"},
|
||||
"status": "AWAITING_CONFIRMATION",
|
||||
"started_from_intent": "retail_support_exchange_return",
|
||||
},
|
||||
}
|
||||
decision = await router.route(state)
|
||||
assert decision.agent == "support_agent"
|
||||
assert decision.metadata["transaction_turn_consumed"] is True
|
||||
assert decision.metadata["transaction_confirmation_decision"] == "confirm"
|
||||
assert "transaction_interruption" not in decision.metadata
|
||||
@@ -34,6 +34,20 @@ intents:
|
||||
"""
|
||||
|
||||
|
||||
class _ParameterLLM:
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
import json
|
||||
prompt = messages[-1]["content"]
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction":
|
||||
pending = json.loads(prompt.split("pending_parameters: ", 1)[1].split("\n", 1)[0])
|
||||
user = prompt.split("user_message: ", 1)[1].split("\nFormato obrigatório:", 1)[0].strip()
|
||||
out = {name: None for name in pending}
|
||||
if len(pending) == 1 and user not in {"quero rastrear pedido", "quero ver minha fatura"}:
|
||||
out[pending[0]] = user
|
||||
return json.dumps(out, ensure_ascii=False)
|
||||
return '{}'
|
||||
|
||||
|
||||
def _router(tmp_path, *, stickiness=True):
|
||||
routing = tmp_path / "routing.yaml"
|
||||
routing.write_text(ROUTING_YAML, encoding="utf-8")
|
||||
@@ -42,7 +56,7 @@ def _router(tmp_path, *, stickiness=True):
|
||||
ENABLE_LLM_ROUTER=False,
|
||||
ENABLE_ROUTE_STICKINESS=stickiness,
|
||||
)
|
||||
return EnterpriseRouter(settings)
|
||||
return EnterpriseRouter(settings, llm=_ParameterLLM())
|
||||
|
||||
|
||||
def _active_tx(status="COLLECTING_PARAMETERS", arguments=None):
|
||||
@@ -55,7 +69,7 @@ def _active_tx(status="COLLECTING_PARAMETERS", arguments=None):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("message", ["PED-1001", "12345", "R$ 71,99", "10/09/2026"])
|
||||
@pytest.mark.parametrize("message", ["PED-1001", "o pedido é o PED-1001", "12345", "R$ 71,99", "10/09/2026"])
|
||||
async def test_matrix_collecting_parameter_answers_keep_transaction(message, tmp_path):
|
||||
router = _router(tmp_path)
|
||||
state = {
|
||||
@@ -75,6 +89,25 @@ async def test_matrix_collecting_parameter_answers_keep_transaction(message, tmp
|
||||
assert (decision.metadata or {}).get("transaction_interruption") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matrix_specific_same_agent_intent_shift_still_preempts_collection(tmp_path):
|
||||
router = _router(tmp_path)
|
||||
state = {
|
||||
"user_text": "quero rastrear pedido",
|
||||
"sanitized_input": "quero rastrear pedido",
|
||||
"next_state": "COLLECTING_ORDER_PARAMETERS",
|
||||
"transaction_status": "COLLECTING_PARAMETERS",
|
||||
"missing_parameters": ["order_id"],
|
||||
"active_transaction": _active_tx(arguments={}),
|
||||
"active_agent": "orders_agent",
|
||||
"intent": "state:COLLECTING_ORDER_PARAMETERS",
|
||||
"route_decision": {"agent": "orders_agent", "intent": "retail_order_cancel"},
|
||||
}
|
||||
decision = await router.route(state)
|
||||
assert decision.intent == "retail_order_tracking"
|
||||
assert decision.metadata["transaction_interruption"] == "intent_shift"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matrix_missing_next_state_recovers_active_transaction_before_stickiness(tmp_path):
|
||||
router = _router(tmp_path)
|
||||
|
||||
@@ -30,20 +30,45 @@ import pytest
|
||||
from agent_framework.runtime.agent_runtime import AgentRuntimeMixin
|
||||
|
||||
|
||||
class _TransactionTestLLM:
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
import json
|
||||
prompt = messages[-1]["content"]
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||
pending = json.loads(prompt.split("pending_parameters: ", 1)[1].split("\n", 1)[0])
|
||||
user = prompt.split("user_message: ", 1)[1].split("\nFormato obrigatório:", 1)[0].strip()
|
||||
out = {name: None for name in pending}
|
||||
low = user.lower()
|
||||
if "order_id" in out:
|
||||
import re
|
||||
m = re.search(r"\b(?:ped[- ]?)?(\d+)\b", low, re.I)
|
||||
if m:
|
||||
out["order_id"] = ("PED-" + m.group(1)) if "ped" in m.group(0).lower() else m.group(1)
|
||||
if "reason" in out and ("arrepend" in low or "desisti" in low):
|
||||
out["reason"] = "Arrependimento da compra" if "arrepend" in low else "desisti da compra"
|
||||
return {"content": json.dumps(out, ensure_ascii=False)}
|
||||
return {"content": "{}"}
|
||||
|
||||
|
||||
class _PolicyRouter:
|
||||
def __init__(self):
|
||||
from types import SimpleNamespace
|
||||
self.registry = SimpleNamespace(
|
||||
tools={"consultar_pedido": object(), "solicitar_devolucao": object()},
|
||||
get_tool=lambda name: {
|
||||
"consultar_pedido": SimpleNamespace(selection_keywords=["consultar pedido", "pedido"]),
|
||||
"solicitar_devolucao": SimpleNamespace(selection_keywords=["devolver pedido", "devolver", "devolução", "arrependimento"]),
|
||||
"consultar_pedido": SimpleNamespace(selection_keywords=["consultar pedido", "pedido"], args_schema={}, requires=[]),
|
||||
"solicitar_devolucao": SimpleNamespace(
|
||||
selection_keywords=["devolver pedido", "devolver", "devolução", "arrependimento"],
|
||||
args_schema={"order_id": "string", "reason": "string"},
|
||||
requires=["order_id", "reason"],
|
||||
description="Solicita devolução de pedido",
|
||||
),
|
||||
}.get(name),
|
||||
)
|
||||
|
||||
def resolve_execution_policy(self, tool_name, arguments=None):
|
||||
if tool_name == "solicitar_devolucao":
|
||||
return {"operation_type": "transactional", "require_confirmation": True, "policy_source": "test"}
|
||||
return {"operation_type": "transactional", "require_confirmation": True, "requires": ["order_id", "reason"], "policy_source": "test"}
|
||||
return {"operation_type": "read_only", "require_confirmation": False, "policy_source": "test"}
|
||||
|
||||
def validate_execution_policy(self, tool_name, arguments=None):
|
||||
@@ -56,6 +81,7 @@ class _PolicyRouter:
|
||||
class _Runtime(AgentRuntimeMixin):
|
||||
def __init__(self):
|
||||
self.tool_router = _PolicyRouter()
|
||||
self.llm = _TransactionTestLLM()
|
||||
self.calls = []
|
||||
|
||||
async def _call_mcp_tool(self, tool_name, arguments, state):
|
||||
@@ -163,14 +189,20 @@ async def test_collecting_parameters_does_not_replace_collected_subject_with_sta
|
||||
|
||||
class _InitialContestLLM:
|
||||
async def ainvoke(self, messages, **kwargs):
|
||||
prompt = messages[0]["content"]
|
||||
if "Campo: subject" in prompt:
|
||||
return {"content": '{"subject": "TIM CTRL Redes Sociais 8.0"}'}
|
||||
if "Campo: valor" in prompt:
|
||||
return {"content": '{"valor": null}'}
|
||||
import json
|
||||
prompt = messages[-1]["content"]
|
||||
if kwargs.get("profile_name") == "transaction_parameter_extraction":
|
||||
pending = json.loads(prompt.split("pending_parameters: ", 1)[1].split("\n", 1)[0])
|
||||
out = {name: None for name in pending}
|
||||
if "subject" in out:
|
||||
out["subject"] = "TIM CTRL Redes Sociais 8.0"
|
||||
return {"content": json.dumps(out, ensure_ascii=False)}
|
||||
if kwargs.get("profile_name") == "mcp_parameter_extraction":
|
||||
if "Campo: motivo" in prompt:
|
||||
return {"content": '{"motivo": "não contratei"}'}
|
||||
return {"content": '{}'}
|
||||
if "Campo: valor" in prompt:
|
||||
return {"content": '{"valor": null}'}
|
||||
return {"content": "{}"}
|
||||
|
||||
|
||||
class _InitialContestRouter(_ContestPolicyRouter):
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user