Compare commits

..

52 Commits

Author SHA1 Message Date
0d2b0f1084 adding ABANDON in multi-intent/intent-shift 2026-09-17 00:06:00 -03:00
0d0c95f9f8 adjustment: extra attributes for OTEL 2026-09-16 20:36:55 -03:00
583d801d4e manual agent_template_backend 2026-09-16 08:57:20 -03:00
4712bcc0dc adjustments: manual v4 2026-09-13 08:52:57 -03:00
e16d19083d adjustments: manual v3 2026-09-12 15:33:55 -03:00
dd207b4078 adjustments: manual v2 2026-09-12 08:04:25 -03:00
2f9b053098 adjustments: manual 2026-09-12 07:11:07 -03:00
7a22b7af11 new feature: frontend with separated responses 2026-09-11 11:59:46 -03:00
e882afde25 new feature: frontend with separated responses 2026-09-11 10:45:49 -03:00
f00c2f721a new feature: frontend with separated responses 2026-09-11 09:33:50 -03:00
f62c7993bb new feature: Multi-Intent 2026-09-11 00:34:56 -03:00
3f884d028a new feature: Multi-Intent 2026-09-10 22:55:50 -03:00
05410adf09 new feature: Multi-Intent 2026-09-10 16:12:04 -03:00
ffe47e8d49 ajustes no manual 2026-09-10 09:48:25 -03:00
ae5887ce50 adjustment: load-tester 2026-09-09 13:51:05 -03:00
82636ec15b bugfix: extractor cases 8 and 16 2026-09-08 10:13:03 -03:00
1711575abe bugfix reconciliation extractor 2026-09-07 22:57:36 -03:00
2c3aa58b59 bugfix reconciliation extractor 2026-09-07 20:16:57 -03:00
d84d833694 nova funcionalidade: reconciliacao temporal 2026-09-02 13:04:56 -03:00
fe8c18093f Ajustes no template para definicao de campos para extracao 2026-09-01 20:58:51 -03:00
T3782834
f4638e287a ajuste no contas 2026-09-01 11:23:22 -03:00
T3782834
e24963abe1 ajuste no guardraild COE 2026-08-31 21:11:51 -03:00
10a6f357dd Conferencia de parametros coletados na transacao 2026-08-31 17:27:35 -03:00
c8c83981c4 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 21:04:31 -03:00
83bf961185 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 11:22:10 -03:00
81f24d7357 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 10:03:42 -03:00
1fd18531c0 Documentation organization 2026-08-27 21:11:01 -03:00
df2a1cbe1b bugfix: oci_openai provider 2026-08-27 20:30:27 -03:00
42ab000c7b Documentation organization 2026-08-27 10:28:23 -03:00
472d44074c Documentation organization 2026-08-27 09:41:46 -03:00
T3782834
faf5ca55ba new feature: Integration with kbdb Autonomous 2026-08-26 10:15:51 -03:00
ac18d68eaf new feature: External guardrails/judges 2026-08-24 11:55:07 -03:00
63d0fb51c4 new feature: External guardrails/judges 2026-08-24 11:26:08 -03:00
T3782834
a472daa1e4 adjustments: transaction parameter extraction 2026-08-21 23:06:54 -03:00
T3782834
d93efd8972 adjustments: transaction parameter extraction 2026-08-21 22:44:36 -03:00
727997aa41 Adding transaction state 2026-08-21 17:59:06 -03:00
2496e831a1 Adding transaction state 2026-08-21 16:47:13 -03:00
b5d2a12953 bugfixes: router stickness vs transaction workflow parameters 2026-08-21 14:34:30 -03:00
bdc44b15d6 bugfix: enviar subject para contexto do guardrail 2026-08-21 11:01:48 -03:00
05077a91bc bugfix: aofertas 2026-08-21 10:47:19 -03:00
19c4024693 new feature: reasoning_content for compatible models in ainvoke_response() 2026-08-21 07:40:21 -03:00
22e8f56146 bugfixes: router stickness vs transaction workflow parameters 2026-08-20 23:29:56 -03:00
0a72afb94e bugfixes: router stickness vs transaction workflow parameters 2026-08-20 22:58:25 -03:00
57df723a24 new feature: mcp pre-validation 2026-08-20 15:13:20 -03:00
6e43cc0158 adjustments: route stickness usefull tokens only 2026-08-20 13:14:57 -03:00
d271031198 adjustments: route stickness usefull tokens only 2026-08-20 11:44:24 -03:00
34d33350ae new feature: transaction evidence 2026-08-20 09:52:22 -03:00
ad778f6889 bugfix: route stickness precedences (transaction in the same intent) 2026-08-20 09:18:58 -03:00
762f6fb024 bugfixes: transaction parameter collector, generic formatting messages, guardrails, prompts. Testing contas 2026-08-20 00:23:50 -03:00
f9c66b4792 Melhorias no guardrails de FRASEOLOGY. Melhorias no Transaction Workflow 2026-08-19 16:08:03 -03:00
560e79d21b New features: Domain_Requested_LLM_Composition, Domain_Requested_RAG, Offline_Workflow_Regression, Pause_Resume_Workflow, Voice_Interruption_Replay, Workflow_Error_Recovery, Durable Idempotency, Workflow_Pause_Resume, Dynamic_Transaction_States, Post_Finalization_Replay, Retrieval_Tool_Guardrails 2026-08-19 09:25:38 -03:00
23d32bbcfc Testes de Carga: /deploy/oke/load-test 2026-08-17 18:59:14 -03:00
494 changed files with 18121 additions and 47921 deletions

View File

@@ -234,8 +234,3 @@ KBDB_MAX_CROSS_REF_HOPS=1
KBDB_DOCUMENT_TYPE=customer_safe KBDB_DOCUMENT_TYPE=customer_safe
KBDB_METADATA_JSON= KBDB_METADATA_JSON=
KBDB_MIN_SCORE= KBDB_MIN_SCORE=
# Framework vocabulary and standard responses: pt-BR | en
AGENT_LOCALE=pt-BR
# Optional directory containing complete pt-BR.json and en.json catalogs.
# AGENT_LOCALES_DIR=./config/locales

View File

@@ -1,56 +0,0 @@
# Copy these settings into the .env loaded by the running backend/gateway.
# Existing feature flags (ENABLE_LLM_ROUTER, ENABLE_ROUTE_STICKINESS, etc.) still apply.
# Global default; each individual setting below takes precedence.
DECISION_PROVIDER=current
DECISION_FALLBACK_TO_CURRENT=true
JEV_ENDPOINT=https://api.typesafe.ai/v1/systemone
AI_GATEWAY_API_KEY=
JEV_MODEL=jev-latest
JEV_TIMEOUT_SECONDS=15
JEV_MIN_CONFIDENCE=0.7
JEV_DECISION_DIAGNOSTICS=true
# All options: current | jev. No existing behavior changes by default.
DECISION_ROUTE_CONTINUITY_PROVIDER=current
DECISION_ROUTER_PROVIDER=current
DECISION_WORKFLOW_EXPECTED_INPUT_PROVIDER=current
DECISION_WORKFLOW_REENTRY_PROVIDER=current
DECISION_TRANSACTION_CONFIRMATION_PROVIDER=current
DECISION_TRANSACTION_PARAMETER_RELEVANCE_PROVIDER=current
DECISION_TRANSACTION_INTENT_SHIFT_PROVIDER=current
DECISION_MULTI_INTENT_PROVIDER=current
DECISION_TRANSACTION_PARAMETER_RECONCILIATION_PROVIDER=current
DECISION_GLOBAL_SUPERVISOR_PROVIDER=current
DECISION_PROCESSING_INTERRUPTION_PROVIDER=current
# Generic LLM guardrail and output supervisor (existing profile names).
DECISION_GUARDRAIL_GUARDRAIL_PROVIDER=current
DECISION_GUARDRAIL_GRL_PROVIDER=current
# Calibrated guardrails, independently by task.
DECISION_GUARDRAIL_PINJ_PROVIDER=current
DECISION_GUARDRAIL_COER_PROVIDER=current
DECISION_GUARDRAIL_TOX_PROVIDER=current
DECISION_GUARDRAIL_REVPREC_PROVIDER=current
DECISION_GUARDRAIL_AOFERTA_PROVIDER=current
DECISION_GUARDRAIL_OOS_PROVIDER=current
DECISION_GUARDRAIL_FRASEOLOGIA_PROVIDER=current
DECISION_GUARDRAIL_RAGSEC_PROVIDER=current
DECISION_GUARDRAIL_DLEX_IN_PROVIDER=current
DECISION_GUARDRAIL_DLEX_OUT_PROVIDER=current
# Calibrated judges, by task; generic judges, by configured name.
DECISION_JUDGE_CSI_PROVIDER=current
DECISION_JUDGE_VCTN_PROVIDER=current
DECISION_JUDGE_ALUC_PROVIDER=current
DECISION_JUDGE_RQLT_PROVIDER=current
DECISION_JUDGE_LLM_JUDGE_PROVIDER=current
# Optional threshold per point, independent from the provider choice.
# DECISION_ROUTE_CONTINUITY_MIN_CONFIDENCE=0.90
# DECISION_TRANSACTION_CONFIRMATION_MIN_CONFIDENCE=0.95
# Named calibrated judge overrides its task group (optional).
# DECISION_JUDGE_GROUNDEDNESS_PROVIDER=jev
# DECISION_JUDGE_RESPONSE_QUALITY_PROVIDER=current
# Optional individual guardrail recheck override.
# DECISION_GUARDRAIL_DLEX_OUT_PROTOCOL_AUTHORIZATION_RECHECK_PROVIDER=jev

67
.idea/workspace.xml generated
View File

@@ -4,9 +4,7 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders"> <list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -47,35 +45,34 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;, "ModuleVcsDetector.initialDetectionPerformed": "true",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;, "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.typescript.service.memoryLimit.init": "true",
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;, "SHARE_PROJECT_CONFIGURATION_FILES": "true",
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "Shell Script.agent_frontend.executor": "Run",
&quot;Shell Script.agent_frontend.executor&quot;: &quot;Run&quot;, "Shell Script.agent_template_backend.executor": "Run",
&quot;Shell Script.agent_template_backend.executor&quot;: &quot;Run&quot;, "Shell Script.mcp_gateway.executor": "Run",
&quot;Shell Script.mcp_gateway.executor&quot;: &quot;Run&quot;, "Shell Script.mcp_server.executor": "Run",
&quot;Shell Script.mcp_server.executor&quot;: &quot;Run&quot;, "git-widget-placeholder": "master",
&quot;git-widget-placeholder&quot;: &quot;main&quot;, "kotlin-language-version-configured": "true",
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;, "last_opened_file_path": "C:/Asus_Projects/kbdb/agent_framework_oci/templates/agent_template_backend",
&quot;last_opened_file_path&quot;: &quot;D:/Dropbox/ORACLE/TIM/FY27/Commits/agent_platform_oci&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "nodejs_package_manager_path": "npm",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "project.structure.last.edited": "Project",
&quot;project.structure.last.edited&quot;: &quot;Project&quot;, "project.structure.proportion": "0.15",
&quot;project.structure.proportion&quot;: &quot;0.15&quot;, "project.structure.side.proportion": "0.2",
&quot;project.structure.side.proportion&quot;: &quot;0.2&quot;, "settings.editor.selected.configurable": "advanced.settings",
&quot;settings.editor.selected.configurable&quot;: &quot;advanced.settings&quot;, "vue.rearranger.settings.migration": "true"
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}</component> }]]></component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="C:\Asus_Projects\kbdb\agent_framework_oci\templates\agent_template_backend" /> <recent name="C:\Asus_Projects\kbdb\agent_framework_oci\templates\agent_template_backend" />
@@ -152,8 +149,8 @@
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-jdk-30f59d01ecdd-cffe25b9f5b3-intellij.indexing.shared.core-IU-253.28294.334" /> <option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.29188.36" />
<option value="bundled-js-predefined-d6986cc7102b-c7e53b3be11b-JavaScript-IU-253.28294.334" /> <option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-IU-251.29188.36" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -177,10 +174,6 @@
<workItem from="1785409583147" duration="1561000" /> <workItem from="1785409583147" duration="1561000" />
<workItem from="1785452072692" duration="784000" /> <workItem from="1785452072692" duration="784000" />
<workItem from="1785453511270" duration="1006000" /> <workItem from="1785453511270" duration="1006000" />
<workItem from="1792665278602" duration="734000" />
<workItem from="1793193595355" duration="436000" />
<workItem from="1793194079956" duration="5443000" />
<workItem from="1793271962068" duration="758000" />
</task> </task>
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders"> <task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

@@ -1,57 +0,0 @@
# Atualização dos testes após mudanças de contrato
Esta revisão mantém os quatro nomes de arquivo e a quantidade de testes. Oito
testes foram atualizados, sem `skip`, `xfail` ou alterações no runtime para
acomodar as expectativas antigas. Os nomes de sete funções foram ajustados para
descrever o comportamento atual. Os comentários nos testes registram o histórico.
## Histórico verificado no Git do pacote original
- `1711575`, 2026-09-07, `bugfix reconciliation extractor`: removeu
`field_evidence_views`, a construção de candidatos estruturais e partes da
reconciliação anterior.
- `82636ec`, 2026-09-08, `bugfix: extractor cases 8 and 16`: consolidou a coleta
de parâmetros no `single_extractor`, com contexto e pré-validação autoritativa.
- `36f9781`, 2026-09-18, `new feature: transaction interruption for same intent replacement`:
também adicionou sanitização e reavaliação ao DLEX_OUT e passou a usar
`sanitized_text` somente quando há um texto substituto.
## O que os testes passam a verificar
| Arquivo | Contrato coberto |
|---|---|
| `tests/test_transactional_tool_flow.py` | Após `NEEDS_PARAMETER`, o fluxo aguarda nova informação; a nova validação ocorre no turno seguinte e a operação não executa antes da confirmação. |
| `tests/test_transaction_parameter_llm_precedence.py` | O reconciliador recebe schema e contexto, aceita o campo resolvido e respeita `preserve` sem sobrescrever o valor conhecido. |
| `tests/test_transaction_explicit_abandonment_and_structural_candidate.py` | Campo indefinido continua pendente; extração contextual bem-sucedida passa pela validação antes da confirmação; `clear` não inventa um valor nem apaga os demais campos. |
| `tests/test_dlex_out_expected_protocol_authorization.py` | Protocolo autorizado não exige substituição do texto; credencial é removida e a resposta é reavaliada; outra violação mantém o bloqueio. |
Os simuladores deixaram de depender de frases específicas do prompt, como
“FOCO PRINCIPAL DA RECONCILIAÇÃO”. Eles devolvem saídas compatíveis com o contrato
da chamada. A identificação semântica por um modelo real não é comprovada por
esses testes unitários: os testes verificam como o framework utiliza suas saídas.
## Verificações realizadas
- 160 testes selecionados aprovados no pacote atualizado, incluindo os 41 testes
de localização e os quatro arquivos desta revisão.
- Os 66 testes dos quatro arquivos também foram executados usando a biblioteca
original do ZIP: todos aprovados. Assim, a atualização dos testes não depende
da alteração de idiomas para passar.
- Contagem mantida por arquivo: 31, 13, 17 e 5 testes, respectivamente.
- Nenhuma chamada a OCI, modelos remotos ou ferramentas reais de negócio.
- Esta revisão não afirma que a suíte completa do repositório foi executada.
## Aplicar ao repositório
O ZIP completo inclui a localização em `pt-BR` e `en` entregue anteriormente e
esta revisão dos testes. Copie o conteúdo da pasta `agent_platform_oci` para a
raiz do repositório, preservando a pasta `.git` que já existe no seu checkout.
Se a localização já estiver aplicada, o ZIP complementar de testes contém apenas
os quatro arquivos atualizados e esta documentação. Não é preciso usar ambos.
Sugestão de mensagem de commit para a revisão dos testes:
```text
test: align transaction and DLEX_OUT tests with current contracts
```

View File

@@ -77,12 +77,3 @@ No compose, o backend usa `config/mcp_servers.docker.yaml` para apontar para `te
## Operações read-only e transacionais ## Operações read-only e transacionais
Use `config/tool_policies.yaml` no backend para classificar somente as operações que precisam de tratamento adicional. A validação é aplicada no roteador central antes do MCP Gateway/Server. O arquivo é opcional e templates antigos continuam usando as políticas já presentes em `tools.yaml`. A configuração completa e o roteiro de migração estão em [README_TOOL_POLICIES.md](README_TOOL_POLICIES.md). Use `config/tool_policies.yaml` no backend para classificar somente as operações que precisam de tratamento adicional. A validação é aplicada no roteador central antes do MCP Gateway/Server. O arquivo é opcional e templates antigos continuam usando as políticas já presentes em `tools.yaml`. A configuração completa e o roteiro de migração estão em [README_TOOL_POLICIES.md](README_TOOL_POLICIES.md).
## Chamadas MCP multi-item
Para operações com listas de produtos, pedidos, ativos ou outros recursos, **não implemente loops de chamadas MCP dentro do agente**. O framework possui contrato genérico para parâmetros `array/list`, pre-validation, confirmação e resultado por item (`SUCCESS`, `PARTIAL_SUCCESS`, `FAILED`).
Guia completo: [MCP Multi-item — Guia do Desenvolvedor](../docs/MCP_MULTI_ITEM_DEVELOPER_GUIDE.md).
A tool primária deve retornar `results[]` com `success` ou `ok` booleano por item. Em workflows, use preferencialmente `output[tool_name].results`.

View File

@@ -1,84 +0,0 @@
# Seleção individual de decisões: modelo atual ou JEV
Implementação opt-in para o projeto do ZIP de 03/11/2026. O padrão continua sendo `current`; nenhum modelo foi ativado e nenhuma chave foi incluída.
## Configuração
Copie as variáveis de `.env.jev.example` para o `.env` utilizado pelo processo. Se usar contêiner/Kubernetes, passe as variáveis também para o ambiente do processo e guarde a chave no mecanismo de secrets do ambiente. Reinicie o serviço após alterar a configuração. Substitua os arquivos de código e refaça o build/redeploy caso o framework esteja instalado em pacote ou imagem; reiniciar sem atualizar o pacote instalado não aplica mudanças de código.
Exemplo: ativar somente continuidade e intenção, mantendo o restante atual:
```dotenv
DECISION_PROVIDER=current
DECISION_ROUTE_CONTINUITY_PROVIDER=jev
DECISION_ROUTER_PROVIDER=jev
AI_GATEWAY_API_KEY=<sua chave>
JEV_MODEL=typesafe-ai/jev
DECISION_FALLBACK_TO_CURRENT=true
JEV_MIN_CONFIDENCE=0.7
DECISION_ROUTE_CONTINUITY_MIN_CONFIDENCE=0.90
```
A variável individual prevalece sobre `DECISION_PROVIDER`. Uma variável individual ausente herda o padrão global. Como o exemplo define todas como `current`, mudar somente a variável global não altera os pontos explicitamente definidos. Os valores válidos são `current` e `jev`.
A seleção não habilita funcionalidades desativadas: `ENABLE_LLM_ROUTER`, `ENABLE_ROUTE_STICKINESS`, configuração de semantic classifiers e configuração dos guardrails/judges continuam controlando se cada ponto é executado. Para não tornar `current` incompatível, não foram alteradas as regras determinísticas nem os controles de execução.
## Pontos disponíveis
Cada sufixo abaixo corresponde a `DECISION_<SUFIXO>_PROVIDER`:
| Sufixo | Função |
|---|---|
| ROUTE_CONTINUITY | CONTINUE, ROUTE, HUMAN_HANDOFF, END_SESSION; inclui sondagem de controles globais que usa o mesmo classificador |
| ROUTER | Intenção semântica; agente, domínio e ferramentas derivados do catálogo local |
| WORKFLOW_EXPECTED_INPUT | Opção permitida para retomada de workflow; opção sem correspondência não retoma |
| WORKFLOW_REENTRY | REENTER ou RESUME |
| TRANSACTION_CONFIRMATION | Confirmação semântica, usando as opções configuradas; parser explícito permanece |
| TRANSACTION_PARAMETER_RELEVANCE | RELEVANT ou NOT_RELEVANT |
| TRANSACTION_INTENT_SHIFT | CONTINUE, REPLACE, SHIFT ou ABANDON e intenção de destino |
| MULTI_INTENT | Intenção de cada objetivo extraído, com preservação de ordem e trechos literais |
| TRANSACTION_PARAMETER_RECONCILIATION | resolved, preserve, clear ou unresolved por parâmetro |
| GLOBAL_SUPERVISOR | Backend permitido no registro do supervisor global |
| PROCESSING_INTERRUPTION | Regenerar resposta de voz ou repetir |
| GUARDRAIL_GUARDRAIL / GUARDRAIL_GRL | Guardrails LLM genéricos com os perfis padrão; perfis personalizados usam GUARDRAIL_<PERFIL> |
| GUARDRAIL_PINJ, COER, TOX, REVPREC, AOFERTA, OOS, FRASEOLOGIA, RAGSEC, DLEX_IN, DLEX_OUT | Cada tarefa calibrada tem sua própria variável GUARDRAIL_<TAREFA> |
| JUDGE_CSI, JUDGE_VCTN, JUDGE_ALUC, JUDGE_RQLT | Cada tarefa de judge calibrado, como padrão de grupo |
| JUDGE_<NOME> | Judge genérico, usando seu `name`; o padrão é JUDGE_LLM_JUDGE |
Um judge calibrado também aceita `DECISION_JUDGE_<NOME_CONFIGURADO>_PROVIDER`, que prevalece sobre o grupo da tarefa (por exemplo `DECISION_JUDGE_GROUNDEDNESS_PROVIDER` prevalece sobre `DECISION_JUDGE_ALUC_PROVIDER`). Rechecagens de guardrail aceitam o nome completo do componente, por exemplo `DECISION_GUARDRAIL_DLEX_OUT_PROTOCOL_AUTHORIZATION_RECHECK_PROVIDER`, prevalecendo sobre `DECISION_GUARDRAIL_DLEX_OUT_PROVIDER`.
Caracteres diferentes de letras/números são convertidos em `_`, com nome em maiúsculas. O mesmo ponto pode definir `DECISION_<SUFIXO>_MIN_CONFIDENCE`; esse valor é independente dos limites já existentes no componente. Os limites existentes continuam sendo aplicados depois da integração.
## O que é híbrido
- Multi-intenção: o modelo atual extrai os objetivos e seus trechos, mantendo a ordem. O JEV escolhe a intenção de cada objetivo. Não há geração de trechos pelo JEV; trechos não literais são rejeitados. Isso pode custar mais que uma chamada atual isolada, por ser uma etapa adicional.
- Reconciliação: o modelo atual extrai candidatos e proveniência. JEV decide o tratamento de cada campo. A decisão não inventa valores e não substitui validação de negócio.
- Geração final, extração livre, resumos, RAG rewrite/compression, TOXOUT e FALLBACK de texto seguem no provedor atual. Autorização, alçadas, idempotência, pré-validação de negócio e execução seguem nas implementações existentes.
- Guardrail LLM genérico no JEV avalia se o texto original pode passar sem alteração. Se precisar de sanitização, deve bloquear; o JEV não fornece `sanitized_text` ou instruções textuais de reescrita. Reescritas já separadas no fluxo continuam generativas.
- Judges JEV não inventam rótulos ou justificativas de prosa: o retorno contém os campos necessários à decisão e motivo técnico fixo. RQLT/CSI usam score 0–10; judge genérico converte para 0–1. Os limites precisam de calibração antes de produção.
## Transporte, falhas e observabilidade
O cliente usa `POST https://ai-gateway.vercel.sh/v1/decisions` com `typesafe-ai/jev`, API key em `Authorization`, timeout de 15 segundos por padrão e perguntas choice/predicate/score. Usa a biblioteca padrão do Python; não adiciona dependências de produção. A chamada bloqueante roda em thread para não bloquear o event loop.
Respostas incompletas, tipos incompatíveis, opções não declaradas, números inválidos, confiança abaixo do limite e erros de transporte acionam o provedor atual uma vez quando `DECISION_FALLBACK_TO_CURRENT=true`. Sem fallback, a exceção chega ao tratamento de falha já existente do componente; isso não redefine automaticamente cada política fail-open/fail-closed. Confiança ausente em choice/score é zero e aciona fallback no limite padrão. Para predicates usa-se a probabilidade do resultado selecionado. Para várias perguntas usa-se o mínimo das confianças.
O custo de uma chamada JEV seguida de fallback inclui ambas as etapas. Respostas recebidas são contabilizadas no repositório de uso e no evento `decision.jev` quando o provedor original oferece esses recursos, mesmo quando a validação rejeita a resposta. Logs registram ponto, modelo, duração, uso e falha/fallback, sem imprimir prompts, chave ou corpo de erro HTTP. Não há geração Langfuse de chat para chamadas JEV; a integração publica evento de decisão.
## Validação
Teste isolado, sem dependências do projeto ou API real:
```sh
python3 tests/test_jev_transport_standalone.py
```
Testes adicionais de integração em ambiente com dependências instaladas:
```sh
PYTHONPATH=libs/agent_framework/src python -m pytest tests/test_jev_decisions.py tests/test_expected_input_semantic_classifier.py tests/unit/test_semantic_route_stickiness.py
```
O teste isolado cobre contratos, seleção, precedência de configuração, fallback, erros, confiança, transporte e análise sintática dos módulos. Resultado local: 17 testes isolados passaram, incluindo execução dos corpos reais de continuidade, roteamento, reconciliação e judge calibrado com transporte simulado. A suíte completa não foi executada: as dependências do projeto não estavam disponíveis e a tentativa de instalação não conseguiu obtê-las. A suíte completa e a qualidade/latência do modelo real precisam ser verificadas no ambiente de execução. Não foi feita chamada real ao JEV.
Fontes de protocolo consultadas: https://vercel.com/docs/ai-gateway/sdks-and-apis/openai-decisions e https://vercel.com/ai-gateway/models/jev.

View File

@@ -1,264 +0,0 @@
The Universal Permissive License (UPL), Version 1.0
This page provides a copy of and also attempts to answer some frequently asked questions about the Universal Permissive License version 1.0. The UPL was first drafted by Oracle in the Spring of 2014, was revised several times after extensive consultation with the Java Community Process Executive Committee and through the Open Source Initiative review process, and was finally approved by the Open Source Initiative in February 2015.
To begin with, this is the final approved text of the Universal Permissive License v 1.0:
Copyright (c) [year] [copyright holders]
The Universal Permissive License (UPL), Version 1.0
Subject to the condition set forth below, permission is hereby granted to any
person obtaining a copy of this software, associated documentation and/or data
(collectively the "Software"), free of charge and under any and all copyright
rights in the Software, and any and all patent rights owned or freely
licensable by each licensor hereunder covering either (i) the unmodified
Software as contributed to or provided by such licensor, or (ii) the Larger
Works (as defined below), to deal in both
(a) the Software, and
(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
one is included with the Software (each a "Larger Work" to which the Software
is contributed by such licensors),
without restriction, including without limitation the rights to copy, create
derivative works of, display, perform, and distribute the Software and make,
use, sell, offer for sale, import, export, have made, and have sold the
Software and the Larger Work(s), and to sublicense the foregoing rights on
either these or other terms.
This license is subject to the following condition:
The above copyright notice and either this complete permission notice or at
a minimum a reference to the UPL must be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
When referring to the license, you may link to it at https://oss.oracle.com/licenses/upl or you may also find a copy on the Open Source Initiative web site at https://opensource.org/licenses/UPL.
Frequently Asked Questions
What is the UPL and why did you create it?
What are the most important features of the UPL?
Can you point to some concrete problems the UPL solves that you could not have addressed with the MIT, BSD or Apache licenses?
So why is the absence of an express patent license in the MIT or BSD licenses a problem - isn't the implied license enough?
Why didn't you include a defensive patent termination provision?
I heard this licenses our entire patent portfolio to Oracle, is that right?
How should I apply the UPL to my code?
What goes in the Larger Works file and what should it look like?
Can someone else change my Larger Works file and license my patents for other things?
What is the UPL and why did you create it?
The UPL is a highly permissive license, including both copyright and patent licenses, which permits use and relicensing under both copyleft and commercial terms, and also facilitates use as a contributor license agreement.
It was originally borne out of a discussion in the Java Community Process around what it would take to permit collaboration on JSR reference implementations in open source forges without the necessity of collaborators signing the JSPA or an Oracle Contributor Agreement. In order to accommodate the broadest possible use case, the license needed to include an express patent license and to be broadly agreed to be GPLv2, GPLv2+Classpath Exception, CDDL, and commercial license compatible, including the ability to cut and paste code between modules, and while it is almost universally agreed among those knowledgeable in FOSS licensing that license proliferation is something to be avoided without good cause, since a license meeting these relatively simple and obvious criteria did not exist, it was decided to draft one.
Drafting started with the MIT license, and after a lot of discussion with other open source lawyers, developers, members of the Java Community Process Executive Committee, and reviewers and board members of the Open Source Initiative, and several rounds of revision, finally arrived at the vetted license text you see here. The license was approved by the OSI as conforming with the Open Source Definition and non-duplicative of existing permissive licenses in February 2015.
Return to FAQ.
What are the most important features of the UPL?
The most important features and things to understand about the UPL are the following:
Clear patent protection. The UPL is a broad permissive license including both a copyright license and an express patent license, covering at least a version licensed by someone under the license (for example a distributor) and/or a version someone contributed to even if they never distribute the whole. (The reason the latter is needed is discussed below.) By virtue of the unambiguous patent license, the UPL is materially clearer with respect to the rights licensed and likely broader than either the MIT or BSD licenses.
Clear & simplified relicensing. The UPL expressly permits sublicensing under either the UPL or under other terms, which clearly allows someone to relicense code received under the UPL either on copyleft terms, on proprietary terms, or otherwise, thus permitting maximum flexibility in reuse.
Reduced overhead in source files. The UPL expressly permits use of the license without including a full copy of the text, which is useful for JavaScript or other cases where minimizing space consumed by licenses is desired - see below for our recommendation of how to apply the license or include proper attribution without the full text.
It can be used as a contributor agreement. Finally, the UPL may be used as a contributor license agreement licensing both the software itself and also contributor patents for use in one or more "Larger Works." The Larger Works licensed in this fashion are designated by the use of a separate file accompanying the license, akin to the NOTICE file that accompanies the Apache License, Version 2.0. The Larger Works file can be used to control for both contributions to other works (for example, we could specify MySQL in a Larger Works file for a work, which would then license contributor patents for MySQL as well as the contribution), to set patent license scope for specific versions (for example, we could specify the approved reference implementation of JSR-xxx including Maintenance Releases to ensure that all contributors to an RI are licensing both the final version of the RI and qualified updates under the JCP program), or both.
Return to FAQ.
Can you point to some concrete problems the UPL solves that you could not have addressed with the MIT, BSD or Apache licenses?
The most important problem the UPL solves is the inclusion of an express patent license in a license that is both broadly agreed to be uniformly copyleft compatible and also permits the software to be freely used in commercially licensed software (i.e., without concerns about reciprocal license obligations).
The MIT and BSD licenses do not include express patent licenses, and while many argue that there is an implied patent license, there is nontrivial debate about the existence & scope of any such implied license. This is addressed in some more detail below. The Apache license, on the other hand, is argued by some not to be GPLv2 compatible, which makes it tricky to use in connection with GPLv2 licensed code - the GPLv2 is still by far the most popular open source license, and unambiguous, undisputed GPL compatibility was a crucial qualifier for any license considered for use in the JCP. (Even the BSD license has been argued not to be GPL compatible - while this need not be addressed here, and it is not the conclusion of the FSF or most others, by virtue of expressly permitting relicensing on other terms, the UPL is definitively and fully GPLv2 compatible.)
Another problem solved with the UPL, which seems like a small thing but adds compliance and clarity, is that licensors and distributors are expressly permitted to reference the UPL rather than including a complete copy of the text in each item. What is the license solving for here? The MIT and BSD licenses expressly require that you include a complete copy of the license in each piece of source code. This means that if you have 50 different JavaScript files being downloaded from a page, each one should be giving you a complete copy of the license - you may be using as much bandwidth for licenses as you do for the actual code! And if you do not include a complete copy of the license, as some folks do not, merely referring to the license by URL or name, then you are not really applying the license in the intended manner and are in fact suggesting that downstream recipients not strictly follow those terms either.
Finally, the inclusion of the Larger Works concept facilitates use as a contributor license agreement (CLA), with contributors granting a patent license to the works to which they're contributing, and therefore creating a common & safe platform for collaboration where no one is going to assert infringement by the same project in which they're an active participant.
Return to FAQ.
So why is the absence of an express patent license in the MIT or BSD licenses a problem - isn't the implied license enough?
Some authorities think it is enough, but not everyone agrees on this point. To begin with, it has been suggested that in some jurisdictions there may be no implied license at all, or only a license to people to whom a licensor directly distributed the software.
But even if one were to take the position that a patent license covering any contribution is implied, and further that it covers the entirety of a work you distribute, in our world of cloud offerings, many contributors may never distribute the entirety of the work at all. This may reduce the likelihood that a court would find them to have granted an implied license for the whole of the work.
The UPL attempts to solve these problems by having each party that either contributes to the project or distributes it under the UPL expressly license all patents that they own or can freely license that cover the work - thus, e.g., licensing the whole even from contributors who never distribute the whole.
Return to FAQ.
Why isn't there a defensive patent termination provision?
A defensive patent termination provision may sound like a great idea at first, especially for a pure open source project, but does not really work for contributions to software that is also licensed on commercial terms.
Why? A company that takes on commercial obligations with respect to a piece of code needs to retain the ability to keep supporting and shipping that code. A defensive patent termination provision allows someone who makes a contribution and then breaches the project license to the project to then potentially effectively block infringement suits against them, even when they have willfully and flagrantly disregarded community norms and IP rights in the project. Such a provision is therefore in reality as much of a sword as it is a shield.
Say, for example, that a fictional company WidgetCo were to make a contribution to fictional company GadgetCo's product GadgetX under a license with a patent termination provision. Now, WidgetCo decides to violate GadgetCo's terms and distribute GadgetX without a license. GadgetCo's options for stopping the violation would ordinarily include both copyright and also patent infringement claims for any patents covering GadgetX. If, on the other hand, WidgetCo has made patent covered contributions under an agreement with a so-called defensive termination clause, GadgetCo's ability to stop WidgetCo from violating the license may be significantly compromised because it can no longer bring patent claims without risking losing its own license to the patents on the contributions, which therefore might require it to stop shipping GadgetX immediately and stop supporting versions that customers are relying on.
As a commercial distributor of software, GadgetCo's customers rely on it to have the rights it needs to be able to continue to deliver and support the software, and defensive patent termination rights in contributions are inconsistent with those obligations. Defensive patent termination only helps someone who may be subject to infringement litigation by someone who is using the code - it won't stop patent trolls, who aren't normally using the code to begin with. So who does it stop, in reality? Someone who is using the code but has IP rights that someone else using the code is infringing. Now, it may be reasonable to tell someone who has no relationship to a community that they cannot simultaneously expect to be able to use the code and also sue for its use - thus making such provisions at least not entirely crazy for a pure FOSS project that no one is signing up to develop or support on a commercial basis, and which is rarely if ever going to need to enforce its rights anyway. But for anyone who both actually expects to need to enforce its license and also wants to take contributions, this makes no sense - you need to be able to enforce your rights in the project without needing to stop shipping or using it, and accepting contributions under terms with a patent termination provision does not satisfy this basic need.
Return to FAQ.
I heard this licenses our entire patent portfolio to Oracle, is that right?
No, it is not, this is simply untrue except in odd and contrived circumstances that would apply to other FOSS licenses as well. If your entire patent portfolio happens to read on a UPL licensed project that you have distributed on those terms or contributed to, or its designated Larger Works, then yes, it would all be licensed for use in that Software and those specified Larger Works, not just to Oracle but to all licensees under the UPL, just as it would all be licensed for a project under the Apache Software License or Eclipse Public License if your entire portfolio read on a project under the ASL or EPL. Patents that do not read on the project as you contributed to or distributed it or the specified Larger Works are not licensed, there is no magic here.
The patent scope is not really all that complicated. It covers your contributions, the project you contributed to in the state it existed when you contributed, and anything specified in the Larger Works file (which may be another application, it may be other or later versions, it may be a piece of hardware, or any combination of those things - or, just as importantly, it may be none of those things, since many projects may have no Larger Works file at all, in which case the patent license is little different than the express patent license in the Apache license, only without the so-called defensive termination provision).
Return to FAQ.
How should I apply the UPL to my code?
It is best to insert a copyright notice and a copy of the license at the top of each source file, and if there is a package of numerous files, to also insert a LICENSE file including a copy of the UPL in the root of the distribution. Here is an example:
/*
** MyWidget version 1.0.
**
** Copyright (c) 2015 WidgetCo, Inc.
**
** The Universal Permissive License (UPL), Version 1.0
**
** Subject to the condition set forth below, permission is hereby granted to any
** person obtaining a copy of this software, associated documentation and/or data
** (collectively the "Software"), free of charge and under any and all copyright
** rights in the Software, and any and all patent rights owned or freely
** licensable by each licensor hereunder covering either (i) the unmodified
** Software as contributed to or provided by such licensor, or (ii) the Larger
** Works (as defined below), to deal in both
**
** (a) the Software, and
** (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
** one is included with the Software (each a "Larger Work" to which the Software
** is contributed by such licensors),
**
** without restriction, including without limitation the rights to copy, create
** derivative works of, display, perform, and distribute the Software and make,
** use, sell, offer for sale, import, export, have made, and have sold the
** Software and the Larger Work(s), and to sublicense the foregoing rights on
** either these or other terms.
**
** This license is subject to the following condition:
** The above copyright notice and either this complete permission notice or at
** a minimum a reference to the UPL must be included in all copies or
** substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
** SOFTWARE.
*/
If using the UPL for JavaScript or other circumstances in which space is at a premium, you may use a reference to the license rather than a full copy (though a full copy is always preferred if there's room and you won't be transmitting a bunch of copies to the same user). If you want to use just a short reference, you should still insert a copyright notice, and along with it, a statement that the file is licensed under the UPL, and may link to it at https://oss.oracle.com/licenses/upl or on the Open Source Initiative web site at https://opensource.org/licenses/UPL.
Here is an example header with a short reference for use in source code:
/*
** MyWidget version 1.0.
**
** Copyright (c) 2015 WidgetCo, Inc.
** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
*/
Return to FAQ.
What goes in the Larger Works file and what should it look like?
To begin with, we should note that the Larger Works concept will be irrelevant to many people and to be clear, there is no need to license any Larger Works at all in order to use the UPL on your code - this is still advantageous versus other permissive licenses because licensees can count on a patent license of unambiguous scope from each contributor, can freely relicense the code as necessary for their needs, and can abbreviate the license notice if appropriate.
The purpose of the Larger Works addition to the license is to allow you to use the UPL as a contributor license agreement, commonly referred to as a CLA, licensing patents for a specified larger work or set of works with which contributions may be used. If you desire to use the UPL as a CLA, in addition to including the UPL and/or references to it in your source, you should insert a lrgrwrks.txt file in the root of the distribution specifying the licensed Larger Works. You will name the licensed project/product there, and if appropriate, specify what versions are covered. In theory you could use a single word naming a product, which would be effective, but some slightly prettier examples are as follows.
Here is what we are hoping to use in connection with JCP Reference Implementations:
/* Larger Works File for JSR-xxx
**
** Licensed Larger Works: The Reference Implementation for JSR-xxx Including Maintenance Releases
** as defined in the JCP Procedures documentation.
*/
So what is the intent here? We are not expanding the scope of the licensed project beyond the RI, however, we want to make sure that the RI is fully covered such that an early contributor who stops adding new code still licenses the RI as a whole, including Maintenance Releases down the line.
A somewhat broader example is as follows:
/* Larger Works File for MyWidget
**
** Larger Works: MyApp
*/
Here, someone has set up the Larger Works file to act as a CLA for the application MyApp. The licensors of MyWidget are also licensing their patents for use in MyApp, and because there is no version restriction, this is presumably true for future versions of MyApp as well as the current version. If they wanted to license only the current version, they could have said:
/* Larger Works File for MyWidget
**
** Larger Works: MyApp 1.0
*/
Or if Company A wanted to use the UPL with a Larger Works file as a CLA for all of their products, they could use one of the following:
/* Larger Works File for Contributions to Company A Projects
**
** Larger Works: All Software Authored or Distributed by Company A
*/
or
/* Larger Works File for Contributions to Company A Projects
**
** Larger Works: All Company A Products and Projects
*/
If the Apache Software Foundation wanted to use the UPL as a CLA, they could use the following:
/* Larger Works File for Contributions to ASF Projects
**
** Larger Works: All Apache Software Foundation Projects
*/
or even more broadly, to include dependencies that are not ASF projects:
/* Larger Works File for Contributions to ASF projects
**
** Larger Works: All Apache Software Foundation projects and their dependencies
*/
or they could make it project specific, for single or multiple Projects:
/* Larger Works File for Contributions to Apache Ant & Maven
**
** Larger Works: Apache Ant and Apache Maven and their subprojects
*/
or project and version specific:
/* Larger Works File for Contributions to Cassandra
**
** Larger Works: Apache Cassandra 2.0.15 and updates other than major version updates
*/
or subject area specific:
/* Larger Works File for Contributions to Build Tools
**
** Larger Works: All Apache Software Foundation projects pertaining to software builds and related processes, including without
** limitation Ant, Ant Libraries, Ivy, IvyDE, EasyAnt, Maven and other software for controlling build processes tools
*/
As you can see, the intent of the Larger Works file is to permit nearly infinite flexibility in what is being licensed beyond the Software itself, as every company and FOSS project has different needs. Note that while a comment format has been used for these examples, since this is not executable code, this isn't strictly necessary, it was done as a matter of habit and in case someone imports one of the files into some executable code.
Return to FAQ.
Can someone else change my Larger Works file and license my patents for other things?
To begin with, a party who alters the Larger Works file cannot alter the scope of the license someone else granted, any more than they could alter the license text of the UPL, the GPL, or any other license and assert that the original licensors are bound by their changes. So no one can offer a broader license to your patents than the one you declared. The only power they have is to sublicense downstream recipients on different terms under whatever IP rights they have.
And on the flip side, what happens if someone simply removes the Larger Works file from a code base and then redistributes without it? Then they are sublicensing the code to a downstream recipient on other terms, without any Larger Works being licensed under their own patents, though the license from the original licensors would still survive on those terms as to people who receive the Software from other sources.
Similarly, if someone alters the Larger Works file and then distributes the code to new recipients themselves under the UPL, then that new distributor is granting a license of a different scope than the one granted by the original authors, which they are permitted to do, just as they would be permitted to grant their own license to the code under the GPL or a commercial license. However, the polite thing to do in this case would be to address the altered scope of their license in the Larger Works file while leaving intact the Larger Works designation from prior licensors. Let's say, for example, we start off with a Larger Works file that looks like this, covering all of Java:
/* Larger Works File for Contributions to Project X
**
** Larger Works: All Oracle Java implementations including without limitation Oracle Java EE, Java SE, Java ME, and OpenJDK
*/
This would grant a license to all of Java from each contributor on those terms. Now let's say Company A only wants to license a particular JSR, not all of Java. They would ideally add separate license and Larger Works files that travel with their contributions calling out only that JSR, most effectively by placing them in a separate directly clearly demarcating what is under that license. (Separating these things out is always a trick when you try to commingle source in the same file, but if you keep files separate, this is not generally difficult.)
In the alternative, if the contributions were not easily separable from the larger project in another directory, and for some reason they could not add a separate directory just for the license either, and therefore absolutely needed a way to combine their own, distinct Larger Works designation with that from others, they could in theory also add their own declaration to the existing Larger Works file without purporting to change the license from others as follows:
/* Larger Works File for Contributions to Project X
**
** Larger Works: All Oracle Java implementations including without limitation Oracle Java EE, Java SE, Java ME, and OpenJDK
**
** Exception: Company A licenses only the following Larger Works: The Reference Implementation for JSR-xxx including
** Maintenance Releases as defined in the JCP Procedures documentation
*/
This would therefore clearly inform a recipient that only the RI was licensed under Company A's patents, but leave intact the reference to the broader license from other contributors.
There are certainly other possibilities for accommodating multiple Larger Works scopes as well, about which your counsel can advise you.
Return to FAQ.
Return to top.

View File

@@ -1,133 +0,0 @@
# Termos e frases configuráveis — pt-BR e en
Esta distribuição inclui dois catálogos completos, em JSON UTF-8:
- `libs/agent_framework/src/agent_framework/locales/pt-BR.json`
- `libs/agent_framework/src/agent_framework/locales/en.json`
Cada arquivo reúne termos reconhecidos, mensagens padronizadas, instruções de
recuperação de guardrails e padrões de extração. JSON permite editar o catálogo
sem uma dependência adicional no carregador. Os dois arquivos usam as mesmas chaves.
## Selecionar o idioma
No `.env` do backend:
```dotenv
AGENT_LOCALE=en
```
Para português, use `AGENT_LOCALE=pt-BR` ou omita a variável. Também são aceitos
`pt`, `pt_BR`, `en-US` e `en-GB`. Uma opção inexistente gera erro; não há troca
silenciosa para português quando o idioma solicitado não está disponível.
O idioma é o padrão do backend. Os prompts e as configurações de negócio dos
agentes continuam sendo definidos pelo projeto; use prompts e descrições no
idioma desejado. Esta alteração não traduz automaticamente conteúdos de RAG,
respostas dos servidores MCP, mensagens declaradas por um workflow, arquivos de
exemplo, regras de negócio ou textos personalizados do aplicativo/frontend.
Prompts internos de classificação podem continuar em português: as decisões são
estruturadas. As instruções de recuperação que impunham português foram separadas
nos catálogos, incluindo sua variante em inglês.
## Personalizar sem alterar a biblioteca
Copie os catálogos para um diretório da aplicação e configure:
```dotenv
AGENT_LOCALE=en
AGENT_LOCALES_DIR=./config/locales
```
Nesse exemplo, o arquivo usado será `./config/locales/en.json`. O catálogo é
completo, e não um arquivo de alterações parciais. Preserve as chaves e os campos
como `{action}`, `{target}`, `{options}` e `{subject}` ao traduzir as frases.
Não é necessário manter a mesma quantidade de termos nas listas.
Reinicie o backend após editar os arquivos: o carregador mantém os catálogos em
cache. Na inicialização, ele verifica a estrutura, os campos das mensagens, a
sintaxe dos padrões e conflitos entre confirmação e recusa.
As principais seções são:
| Seção | Conteúdo |
|---|---|
| `terms.confirmation` | Confirmações e recusas explícitas de transações |
| `terms.workflow` | Respostas de compatibilidade para checkpoints antigos |
| `terms.multi_intent` | Conjunções, verbos e prefixos de confirmação |
| `terms.global_router` | Termos de continuidade e encerramento |
| `terms.groundedness` | Indícios de especificidade e incerteza |
| `terms.mock` | Termos dos classificadores locais usados em testes/desenvolvimento |
| `messages` | Frases de confirmação, nova tentativa, esclarecimento e fallback |
| `prompts.fallback` | Instruções de reescrita e regeneração por guardrail |
| `prompts.judge_rewrite` | Instruções de recuperação dos judges |
| `memory` | Padrões para extração de memória de longo prazo |
| `protocol` | Dígitos, letras e nomes usados na vocalização de protocolos |
| `safety` | Padrões determinísticos de segurança e escopo |
| `legacy_extraction` | Padrões do extrator legado de pedido/motivo |
## Decisões internas e contratos de workflow
Os códigos internos permanecem estáveis: `confirm`, `reject`, `SIM`, `NAO`,
`OUTRO` e nomes de estados não devem ser traduzidos em arquivos existentes.
O catálogo trata as palavras reconhecidas e os textos apresentados.
Um workflow que declara `expected_input`, `reprompt` ou mensagens próprias
continua tendo precedência. Para aceitar `YES` e `NO` em um contrato enumerado,
configure essas opções no workflow e ajuste suas transições, ou mantenha os
códigos atuais e use seu classificador semântico para mapear as respostas.
O dicionário global não substitui silenciosamente o contrato de um fluxo.
As listas de confirmação transacional usam correspondência da frase completa,
ignorando maiúsculas, espaços repetidos e pontuação final simples. Não inclua
frases ambíguas como “entendi”/“understood” nessas listas: compreender uma
explicação não equivale a autorizar uma operação. O fallback semântico existente
continua disponível para respostas que não constam do catálogo.
Os padrões de segurança mantêm cobertura em português e inglês nos dois arquivos.
Nomes de concorrentes presentes em `safety` preservam a configuração original de
telecom; adapte essas regras quando mudar o domínio da aplicação.
## Vários idiomas no mesmo processo
Para aplicações que selecionam o idioma por agente ou atendimento, envolva a
execução completa (roteamento, runtime e guardrails) no contexto correspondente:
```python
from agent_framework.localization import use_locale
with use_locale("en"):
# Execute aqui o roteamento, o agente e seus guardrails.
result = await handle_turn(...)
```
`handle_turn` representa o ponto de entrada da sua aplicação; não é uma nova API
do framework. O contexto usa `ContextVar`, é isolado entre tarefas assíncronas e
restaura o idioma anterior ao sair. Não altere `os.environ` a cada atendimento.
O idioma da conversa não é detectado automaticamente.
## Validação desta entrega
- 41 testes novos de localização aprovados.
- 160 testes selecionados aprovados após a atualização de oito testes antigos.
- As oito expectativas antigas estavam desalinhadas com mudanças de contrato
anteriores à localização. Histórico, escopo e validação estão em
`ATUALIZACAO_TESTES.md`. Os 66 testes dos quatro arquivos atualizados também
passaram com a biblioteca original do ZIP.
- As verificações incluem execução/cancelamento real do runtime com ferramentas
simuladas, confirmação determinística no roteador sem LLM, múltiplas intenções,
retomada de workflows, memória, protocolos, segurança, concorrência e catálogo
personalizado.
- Não foram feitas chamadas a modelos remotos, OCI ou ferramentas reais de negócio.
Os testes novos estão em `tests/test_locale_catalogs.py`. Para executá-los em um
ambiente com as dependências do projeto e pytest/pytest-asyncio:
```bash
PYTHONPATH=libs/agent_framework/src pytest -q tests/test_locale_catalogs.py
```
O pacote de entrega conserva os arquivos do projeto e remove metadados do macOS,
cache de bytecode, builds antigos e diretórios de Git/IDE. As fontes corrigidas
ficam em `libs/agent_framework/src`. As variantes de demonstração em
`Tuning-Performance` mantêm suas configurações de domínio originais.

View File

@@ -1,29 +0,0 @@
# Multi-item result preservation fix
## Objective
Preserve the terminal outcome of each item executed by the primary transactional tool. A failure in a later auxiliary/composite step must not erase successful item outcomes or turn an already-completed multi-item operation into a total failure.
## Framework-only change
No agent/template business logic is required for this fix. The runtime now recognizes an explicit structural contract on the requested tool: a `results` array with two or more entries carrying boolean `success` or `ok` fields.
For workflow-backed tools the authoritative source is preferentially:
`workflow.output[tool_name].results`
The framework derives one of:
- `SUCCESS`
- `PARTIAL_SUCCESS`
- `FAILED`
and publishes a generic `metadata.multi_item` summary with item counts.
When one or more primary items completed successfully but the outer wrapper is `ok=false` because a later auxiliary/post-processing step failed, the normalized result preserves the original failure as `original_ok=false` / `secondary_error` while exposing the requested operation as operationally successful (`ok=true`).
The LLM context also receives an explicit generic instruction that successful per-item terminal evidence cannot be overwritten by a secondary error and that mixed outcomes must be reported individually.
## Regression tests
Added `tests/test_multi_item_result_normalization.py` covering:
- 2 success + 1 failure -> `PARTIAL_SUCCESS`;
- all primary items successful + later secondary failure -> primary `SUCCESS` preserved;
- single-item behavior unchanged;
- unrelated nested tool results cannot override failure of the requested primary tool.

View File

@@ -1,17 +0,0 @@
# Observability severity patch
- Central `severity_policy.py` classifies explicit MCP failures, guardrail blocks, judge rejections, RAG failures and memory/cache errors.
- `Telemetry.event()` accepts `level` and `status_message` and preserves WARNING/ERROR events as individual observations in compact mode.
- `Telemetry.span()` handle supports `set_status(level, message)`; failed MCP results mark their span ERROR without raising exceptions.
- Long-term-memory read/write failures emit telemetry while preserving existing fallback behavior.
- Guardrail blocked and negative judge evaluation emit WARNING when these helper methods are invoked.
- Normal cache misses and empty RAG results are not automatically failures.
## Scope / limitations
- Existing call sites must invoke `GuardrailTelemetry.blocked` for guardrail blocking events; this patch does not rewrite every pipeline call site.
- Redis cache currently has no injected Telemetry instance, so its existing logger errors are not yet Langfuse observations. A separate dependency-injection change is required to cover that path safely.
- RAG database connection errors that are caught by external KBDB integrations without emitting a telemetry event cannot be classified automatically.
- No live Langfuse ingestion test was possible in this environment. Verify both compact and verbose modes against your server.
## Validation
`PYTHONPATH=libs/agent_framework/src python -m pytest -q tests/test_observability_severity_policy.py`

7806
README.md

File diff suppressed because it is too large Load Diff

11810
README_en.md Normal file

File diff suppressed because it is too large Load Diff

11900
README_pt.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
# Reporting security vulnerabilities
Oracle values the independent security research community and believes that
responsible disclosure of security vulnerabilities helps us ensure the security
and privacy of all our users.
Please do NOT raise a GitHub Issue to report a security vulnerability. If you
believe you have found a security vulnerability, please submit a report to
[secalert_us@oracle.com][1] preferably with a proof of concept. Please review
some additional information on [how to report security vulnerabilities to Oracle][2].
We encourage people who contact Oracle Security to use email encryption using
[our encryption key][3].
We ask that you do not use other channels or contact the project maintainers
directly.
Non-vulnerability related security issues including ideas for new or improved
security features are welcome on GitHub Issues.
## Security updates, alerts and bulletins
Security updates will be released on a regular cadence. Many of our projects
will typically release security fixes in conjunction with the
Oracle Critical Patch Update program. Additional
information, including past advisories, is available on our [security alerts][4]
page.
## Security-related information
We will provide security related information such as a threat model, considerations
for secure use, or any known security issues in our documentation. Please note
that labs and sample code are intended to demonstrate a concept and may not be
sufficiently hardened for production use.
[1]: mailto:secalert_us@oracle.com
[2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html
[3]: https://www.oracle.com/security-alerts/encryptionkey.html
[4]: https://www.oracle.com/security-alerts/

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -9,18 +9,11 @@ profiles:
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -1,196 +0,0 @@
# Implementação prática — MCP Multi-Item
Este documento mostra como adicionar uma operação multi-item sem criar lógica paralela no agente e sem alterar desnecessariamente contratos já existentes.
## 1. Modele uma única operação lógica
```text
quero cancelar Produto A, Produto B e Produto C
```
A operação lógica continua sendo uma só. Não crie tools/intents por item nem loops no Agent.
## 2. Escolha o contrato natural da operação
### Opção A — parâmetro escalar
Use quando a interface já recebe um `subject`, `id`, `query` ou expressão que pode representar uma ou várias entidades:
```yaml
tools:
cancelar_vas_avulso:
args_schema:
subject:
type: string
```
Entrada:
```text
subject = "Produto A, Produto B, Produto C"
```
### Opção B — coleção explícita
Use quando a API/tool é naturalmente batch:
```yaml
tools:
cancelar_produtos:
args_schema:
items:
type: array
```
**Não transforme uma tool escalar em array apenas para obter suporte multi-item.**
## 3. Use a policy existente
Exemplo escalar:
```yaml
tool_policies:
cancelar_vas_avulso:
operation_type: transactional
require_confirmation: true
requires:
- subject
pre_validation:
enabled: true
tool: validar_vas_subject
fail_open: false
```
Nenhuma chave específica de multi-item é necessária.
## 4. Canonicalize/expanda no validator quando necessário
Entrada:
```json
{"subject": "produto a, produto b, produto c"}
```
Saída possível:
```json
{
"eligible": true,
"transaction_decision": {
"resolved_arguments": {
"subject": "Produto A, Produto B, Produto C",
"items": [
{"name": "Produto A", "id": "1001"},
{"name": "Produto B", "id": "1002"},
{"name": "Produto C", "id": "1003"}
]
},
"confirmation_message": "Você confirma o cancelamento de Produto A, Produto B e Produto C?"
}
}
```
O validator deve ser side-effect-free.
## 5. Não transforme uma falha individual em rejeição global
Caso:
```text
A = válido
B = inválido
C = válido
```
Com `fail_open: false`, isto bloquearia tudo:
```json
{"eligible": false, "reason": "B inválido"}
```
Quando A e C ainda podem ser executados, mantenha a transação globalmente elegível e preserve o estado individual dos itens.
## 6. Execute no MCP Server ou workflow
A tool/workflow pode receber `subject` e expandir internamente, ou receber `items[]` já canonicalizado.
### Backend com API unitária
```python
async def cancelar_produtos(items):
results = []
for item in items:
try:
response = await backend.cancel_one(item)
results.append({"name": item["name"], "success": True, "response": response})
except Exception as exc:
results.append({"name": item["name"], "success": False, "error": str(exc)})
return {"results": results}
```
O loop pertence ao **MCP Server/workflow**, não ao Agent.
## 7. Retorne `results[]` por item
```json
{
"results": [
{"name": "Produto A", "success": true},
{"name": "Produto B", "success": false, "error": "not_found"},
{"name": "Produto C", "success": true}
]
}
```
O framework aceita `success` ou `ok` booleano por item.
## 8. Resultado interpretado pelo framework
```text
items_count = 3
items_succeeded_count = 2
items_failed_count = 1
multi_item_status = PARTIAL_SUCCESS
```
## 9. Workflow com etapa auxiliar
O resultado autoritativo da operação principal deve estar em:
```text
workflow.output[tool_name].results
```
Assim uma falha auxiliar posterior não apaga itens já concluídos.
## 10. Checklist do desenvolvedor
- [ ] existe uma única tool lógica para a operação;
- [ ] `tools.yaml` declara o contrato natural, escalar ou coleção;
- [ ] não existe requisito de `type: array` para habilitar multi-item;
- [ ] `tool_policies.yaml` mantém os `requires` reais do domínio;
- [ ] se um escalar representa vários itens, validator/tool/workflow faz a expansão;
- [ ] com `fail_open: false`, falha individual não bloqueia automaticamente todos os itens acionáveis;
- [ ] fan-out, se necessário, pertence ao MCP Server/workflow;
- [ ] a tool primária devolve `results[]`;
- [ ] cada item contém `success` ou `ok` booleano;
- [ ] sucessos parciais não são apagados por uma falha global posterior;
- [ ] a apresentação comunica sucesso/falha por item;
- [ ] não foi criado um segundo motor multi-item no Agent.
## 11. Testes mínimos recomendados
```text
1 item / sucesso
N itens / todos sucesso
N itens / sucesso parcial
N itens / todos falha
entrada escalar expandida para N itens
entrada batch explícita
item não canonicalizado
confirmação única para N itens
falha auxiliar após sucesso primário
replay/idempotência da mesma operação
```
Para detalhes do contrato interno do runtime, consulte `docs/MCP_MULTI_ITEM_DEVELOPER_GUIDE.md`.

View File

@@ -1,205 +0,0 @@
# Multi-Item MCP
Capability de referência para operações MCP que precisam processar **mais de um item do mesmo tipo** em uma única intenção: produtos, pedidos, VAS, ativos, faturas, linhas, contratos ou recursos.
> Esta capability é **framework-native**. Ela não exige um motor multi-item dentro do agente e não adiciona novos atributos obrigatórios a `tools.yaml` ou `tool_policies.yaml`.
> **Importante:** multi-item não é definido por `type: array`. O parâmetro de entrada pode ser escalar (`subject`, `id`, `query`) ou uma coleção. O que torna a execução multi-item é a resolução/execução de vários itens e o retorno por item em `results[]`.
## Quando usar
Use quando o usuário puder solicitar, por exemplo:
```text
cancele Produto A, Produto B e Produto C
```
ou:
```text
consulte os pedidos 1001, 1002 e 1003
```
O desenvolvedor deve modelar **uma operação lógica**. O framework cuida de estado, confirmação e interpretação de resultados; validator/MCP Server/workflow cuidam da resolução e execução dos itens.
## Divisão de responsabilidades
```text
Agent
seleciona a operação
NÃO cria motor multi-item
↓
tools.yaml
declara o contrato natural da operação
pode ser escalar OU coleção
NÃO exige chave multi_item
↓
tool_policies.yaml
confirmação + requires + pre-validation
NÃO possui sintaxe especial de multi-item
↓
MCP validator
resolve/canonicaliza itens
pode expandir subject/id/query em items[]
pode preencher transaction_decision.resolved_arguments
↓
MCP tool / workflow
executa 1 ou N itens
devolve results[] com success/ok por item
↓
Agent Framework
mantém estado
controla confirmação
interpreta results[]
preserva evidência por item
calcula SUCCESS / PARTIAL_SUCCESS / FAILED
↓
LLM / Presentation
apresenta cada sucesso/falha
```
## Configuração mínima — padrão escalar
Este padrão é útil quando a interface natural já usa `subject` e o validator expande múltiplas entidades.
### `tools.yaml`
```yaml
tools:
cancelar_vas_avulso:
description: Cancela um ou mais VAS.
mcp_server: contas
enabled: true
tool_type: action
args_schema:
subject:
type: string
```
### `tool_policies.yaml`
```yaml
tool_policies:
cancelar_vas_avulso:
operation_type: transactional
require_confirmation: true
requires:
- subject
pre_validation:
enabled: true
tool: validar_vas_subject
fail_open: false
```
Nenhuma chave abaixo existe ou é necessária:
```yaml
multi_item: true
multi_item_strategy: partial
fan_out: framework
```
## Configuração alternativa — API batch
Quando a tool MCP recebe naturalmente uma coleção, `array/list` continua válido:
```yaml
args_schema:
items:
type: array
```
Isso é **uma opção de contrato**, não um pré-requisito para multi-item.
## Pre-validation e expansão de itens
Um `subject` escalar pode ser transformado em argumentos canônicos:
```json
{
"eligible": true,
"transaction_decision": {
"resolved_arguments": {
"subject": "Produto A, Produto B, Produto C",
"items": [
{"name": "Produto A", "id": "1001"},
{"name": "Produto B", "id": "1002"},
{"name": "Produto C", "id": "1003"}
]
},
"confirmation_message": "Você confirma o cancelamento de Produto A, Produto B e Produto C?"
}
}
```
### Validação parcial
`eligible` é global. Com `fail_open: false`, não retorne `eligible: false` apenas porque um item falhou se outros ainda são acionáveis.
Padrão recomendado:
1. `eligible: true` enquanto houver pelo menos um item acionável;
2. preservar resolução/eligibilidade individual;
3. executar a operação primária;
4. produzir o resultado terminal individual em `results[]`.
## Resultado MCP esperado
```json
{
"results": [
{"name": "Produto A", "success": true},
{"name": "Produto B", "success": false, "error": "not_found"},
{"name": "Produto C", "success": true}
]
}
```
O framework deriva automaticamente `SUCCESS`, `PARTIAL_SUCCESS` ou `FAILED`.
## Uma operação lógica, sem N chamadas no agente
São válidos:
```text
Agent -> cancelar(subject="A, B, C") -> validator/workflow -> results[]
```
ou:
```text
Agent -> cancelar(items=[A,B,C]) -> MCP/workflow -> results[]
```
Se o backend possui apenas API unitária, o fan-out fica no MCP Server/workflow. Evite loop MCP de negócio dentro do agente.
## Workflow-backed tools
Quando a operação é implementada por workflow, o caminho preferencial do resultado primário é:
```text
workflow.output[tool_name].results
```
Uma falha posterior não deve apagar sucesso terminal de itens já executados.
## Documentação completa
- `docs/MCP_MULTI_ITEM_DEVELOPER_GUIDE.md` — guia canônico em português;
- `docs/MCP_MULTI_ITEM_DEVELOPER_GUIDE_en.md` — guia canônico em inglês;
- `templates/agent_template_backend/docs/MCP_MULTI_ITEM.md` — referência junto ao template;
- `specs/SPEC-010-Agent-Development.md` — regra arquitetural.
Para o passo a passo prático, leia `IMPLEMENTACAO_MULTI_ITEM_MCP.md`.

View File

@@ -1,58 +0,0 @@
# Multi-Item MCP
Reference capability for MCP operations that process more than one item under a single logical user intent.
> Multi-item support is **not enabled by `type: array`**. The input may be scalar (`subject`, `id`, `query`) or a collection. Multi-item behavior comes from resolving/executing several items and returning per-item `results[]`.
```text
Agent selects the operation; no business fan-out
-> tools.yaml declares the natural input contract: scalar OR collection
-> tool_policies confirmation/requires/pre-validation; no special multi-item syntax
-> validator resolves/canonicalizes and may expand one value into items[]
-> MCP/workflow executes 1 or N items and returns results[]
-> framework preserves per-item evidence and derives SUCCESS/PARTIAL_SUCCESS/FAILED
-> presentation reports each success/failure
```
## Scalar pattern
```yaml
tool_policies:
cancelar_vas_avulso:
operation_type: transactional
require_confirmation: true
requires:
- subject
pre_validation:
enabled: true
tool: validar_vas_subject
fail_open: false
```
`subject` may represent one or many entities. The validator/workflow may expand it into canonical `items[]`.
## Explicit batch pattern
```yaml
args_schema:
items:
type: array
```
This is supported, but it is **an interface option, not a requirement**.
## Result contract
```json
{
"results": [
{"name": "A", "success": true},
{"name": "B", "success": false},
{"name": "C", "success": true}
]
}
```
The runtime derives `PARTIAL_SUCCESS` and preserves the successful items.
For full details, read `docs/MCP_MULTI_ITEM_DEVELOPER_GUIDE_en.md`.

View File

@@ -9,18 +9,11 @@ profiles:
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -8,4 +8,3 @@ Variantes e documentos de referência para comparar funcionalidades e impacto de
- `Deterministic_Transactional_Workflow`: transações multi-etapas executadas por workflow LangGraph determinístico após clarification e confirmação. - `Deterministic_Transactional_Workflow`: transações multi-etapas executadas por workflow LangGraph determinístico após clarification e confirmação.
- `Transaction_Evidence`: persistência e correlação de resultados transacionais como evidência operacional para turnos posteriores e groundedness. - `Transaction_Evidence`: persistência e correlação de resultados transacionais como evidência operacional para turnos posteriores e groundedness.
- `Transaction_Pre_Validation`: pré-validação MCP side-effect-free antes da confirmação transacional; regras de elegibilidade permanecem no domínio. - `Transaction_Pre_Validation`: pré-validação MCP side-effect-free antes da confirmação transacional; regras de elegibilidade permanecem no domínio.
- `Multi_Item_MCP`: operações MCP sobre listas de produtos/pedidos/serviços/ativos com confirmação única, canonicalização e resultado por item (`SUCCESS` / `PARTIAL_SUCCESS` / `FAILED`), sem criar motor multi-item no agente.

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1

View File

@@ -1,3 +0,0 @@
FROM nginx:stable-alpine
COPY index.html styles.css app.js a2a-client.js /usr/share/nginx/html/
EXPOSE 80

View File

@@ -1,133 +0,0 @@
# Frontend A2A para o Agent Framework OCI
Interface estática, sem build e sem dependências JavaScript de produção. Usa os
endpoints A2A que já existem no pacote: pode falar com o template de backend
ou com o Agent Gateway. O frontend nativo em `apps/agent_frontend` é preservado.
## Experimentar sem OCI
Na raiz do framework:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' uvicorn
python -m uvicorn examples.a2a_frontend_demo:app --host 127.0.0.1 --port 8000
```
Abra **http://localhost:8000** e clique em **Conectar ao serviço**. O exemplo
serve a tela e a API na mesma origem, sem precisar configurar CORS.
A demonstração usa o **adaptador A2A e o SDK reais**, com a lógica de negócio
simulada. Digite uma mensagem e acompanhe a tarefa. Comandos de demonstração:
- `/confirmar`: aguarda sua resposta e retoma a mesma Task.
- `/lento`: permite testar o botão de cancelamento.
- `/erro`: demonstra uma Task que termina em falha.
Se a porta 8000 estiver ocupada, altere a porta ao iniciar e informe o novo
endereço no campo **Endereço do serviço**, ou use `?service=http://localhost:NOVA_PORTA`.
## Usar com seu agente ou Gateway
Habilite A2A no serviço, conforme `docs/A2A_ASSINCRONO.md`, e sirva este diretório:
```sh
python -m http.server 5173 --directory apps/agent_frontend_a2a
```
Abra **http://localhost:5173**. Não abra `index.html` como arquivo local, pois a
interface usa módulos JavaScript e requisições HTTP.
| Uso | Endereço do serviço | Destino na interface | Seleção carregada do Agent Card |
| --- | --- | --- | --- |
| Template de agente | `http://localhost:8000` | Agente | Perfis de `agents.yaml` |
| Agent Gateway | `http://localhost:8010` | Gateway | Backends do catálogo |
Use **Seleção automática** para deixar a decisão com o serviço. No Gateway,
um backend selecionado é enviado como `message.metadata.backend_id`. Um perfil
opcional dentro dele pode ser informado em **Perfil do agente no backend**.
O adaptador A2A encaminha a seleção ao roteador existente; os contratos nativos
não são alterados.
O serviço deve permitir a origem da interface em `CORS_ORIGINS`. O valor padrão
do pacote já permite `http://localhost:5173`. Se mudar o host ou a porta,
configure a origem exata, por exemplo:
```dotenv
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
ENABLE_A2A=true
A2A_TASK_MODE=true
```
Para publicar com Docker:
```sh
docker build -t agent-frontend-a2a apps/agent_frontend_a2a
docker run --rm -p 5173:80 agent-frontend-a2a
```
O navegador precisa alcançar o endereço informado no formulário. Para uso
externo, informe a URL pública do backend/Gateway, não o nome interno do
container. Uma interface HTTPS deve acessar um serviço HTTPS.
## O que a interface oferece
- Descoberta do Agent Card e escolha de versão automática ou explícita (1.0/0.3).
- Envio de mensagem com retorno imediato de Task.
- Acompanhamento por POST SSE com `SubscribeToTask`/`tasks/resubscribe`.
- Recuperação por `GetTask`/`tasks/get` quando o fluxo termina ou cai.
- Consulta periódica como alternativa ao streaming.
- Cancelamento solicitado ao serviço, incluindo tarefas aguardando input.
- Continuação de tarefas em input-required/auth-required.
- Exibição de respostas diretas e dos artefatos de Tasks, com suporte aos
resultados estruturados de múltiplos agentes do framework.
- Contexto de negócio opcional (cliente e contrato/pedido).
- Identificadores da conversa/tarefa preservados em `sessionStorage` para
retomar após recarregar a mesma aba. O conteúdo do chat não é persistido.
- Nova conversa independente, sem reaproveitar uma Task concluída.
O frontend usa `fetch` para SSE, permitindo enviar os mesmos cabeçalhos de
autenticação nas consultas e no acompanhamento. Em **Contexto e autenticação**,
informe o nome e o valor completo do cabeçalho exigido pelo serviço, por
exemplo `Authorization` / `Bearer ...`, ou `x-api-key` / valor da chave.
Credenciais ficam somente na memória e precisam ser informadas novamente após
recarregar. Não são gravadas em `sessionStorage` nem colocadas na URL.
Nenhuma operação de envio é repetida automaticamente. Se a resposta do envio
não chegar, a tela informa a incerteza, pois o serviço pode ter aceitado a
mensagem. O botão **Retomar** consulta a mesma Task, quando seu ID é conhecido.
Trocar conexão, tenant, perfil ou backend requer reconectar e inicia outra
conversa quando a identidade da configuração mudou.
## Limites herdados do serviço
O frontend não transforma o armazenamento em memória em armazenamento durável.
Uma Task perdida ao reiniciar o serviço não pode ser recuperada pela tela.
O cancelamento local não desfaz efeitos externos já confirmados e não garante
cancelar uma operação já aceita por um agente remoto.
`auth-required` indica que a tarefa aguarda autorização, mas esta interface não
implementa login OAuth nem aprovação externa automática. Realize o fluxo exigido
pelo agente, atualize a credencial se necessário e retome a tarefa.
Não há upload de arquivos, push notifications, gRPC ou emissão de tokens do LLM.
A seleção de versão 0.3 é explícita quando o serviço publica um Agent Card 1.0
mas também aceita o formato 0.3, como ocorre no adaptador do framework.
## Testes
Sem bibliotecas JavaScript adicionais, com Node.js 22 ou superior:
```sh
cd apps/agent_frontend_a2a
node --test tests/client.test.js
```
Com a demonstração em execução:
```sh
A2A_DEMO_URL=http://localhost:8000 node --test tests/*.test.js
```
Os testes de integração exercitam o cliente JavaScript contra o SDK real do
framework: A2A 1.0 e 0.3, envio, streaming, consulta, confirmação, cancelamento
e falha. Os testes de interface também foram realizados em navegador local.

View File

@@ -1,167 +0,0 @@
export const uid = () => globalThis.crypto.randomUUID();
export const stateName = (state = '') => state.replace(/^TASK_STATE_/, '').toLowerCase().replaceAll('_', '-');
export const isStopped = state => ['completed', 'failed', 'canceled', 'rejected', 'input-required', 'auth-required'].includes(stateName(state));
export const needsInput = state => ['input-required', 'auth-required'].includes(stateName(state));
export class RpcError extends Error {
constructor(error) { super(error.message || 'O serviço retornou um erro.'); this.name = 'RpcError'; this.code = error.code; }
}
export function serviceURLs(value) {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('Informe uma URL HTTP ou HTTPS sem credenciais, parâmetros ou fragmentos.');
const base = url.href.replace(/\/$/, '').replace(/\/a2a$/, '');
return { base, rpc: `${base}/a2a`, card: `${base}/.well-known/agent-card.json` };
}
export function normalizeResult(result) {
if (!result || typeof result !== 'object') throw new Error('Resposta A2A inválida.');
for (const [key, type] of [['task','task'], ['message','message'], ['statusUpdate','status'], ['artifactUpdate','artifact']]) {
if (result[key]) return { type, value: result[key] };
}
const kinds = { task: 'task', message: 'message', 'status-update': 'status', 'artifact-update': 'artifact' };
if (kinds[result.kind]) return { type: kinds[result.kind], value: result };
throw new Error('O serviço não retornou uma mensagem, tarefa ou evento A2A reconhecido.');
}
export function selectVersion(card, preference = 'auto') {
if (preference !== 'auto') return preference;
const versions = (card.supportedInterfaces || []).filter(i => i.protocolBinding === 'JSONRPC').map(i => i.protocolVersion);
if (versions.some(v => /^1\./.test(v))) return '1.0';
if (/^0\.3/.test(card.protocolVersion || '') || versions.some(v => /^0\.3/.test(v))) return '0.3';
throw new Error('O Agent Card não anuncia A2A 1.0 ou 0.3 por JSON-RPC.');
}
// SSE over POST/fetch supports authentication headers and both A2A versions.
export async function* parseSSE(body) {
if (!body) throw new Error('O serviço não abriu o fluxo de eventos.');
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = '', data = [], size = 0;
try {
while (true) {
const { value, done } = await reader.read();
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
while (true) {
const end = buffer.search(/[\r\n]/);
if (end < 0 || (!done && buffer[end] === '\r' && end === buffer.length - 1)) break;
const line = buffer.slice(0, end);
buffer = buffer.slice(end + (buffer.slice(end, end + 2) === '\r\n' ? 2 : 1));
if (!line) {
if (data.length) yield data.join('\n');
data = []; size = 0;
} else if (line.startsWith('data:')) {
const entry = line.slice(5).replace(/^ /, '');
data.push(entry); size += entry.length;
}
if (size > 4 * 1024 * 1024) throw new Error('Evento recebido excedeu o limite de tamanho.');
}
if (buffer.length > 4 * 1024 * 1024) throw new Error('Evento recebido excedeu o limite de tamanho.');
if (done) break; // A partial frame is not an event; the caller can recover with GetTask.
}
} finally {
await reader.cancel().catch(() => {});
reader.releaseLock();
}
}
export class A2AClient {
constructor({ url, version = '1.0', headers = {}, fetchImpl = globalThis.fetch }) {
this.urls = serviceURLs(url); this.version = version; this.fetch = fetchImpl.bind(globalThis);
this.headers = { ...headers, 'A2A-Version': version };
}
method(v1, v03) { return this.version === '0.3' ? v03 : v1; }
async discover(signal) {
const response = await this.fetch(this.urls.card, { headers: this.headers, credentials: 'omit', signal });
if (!response.ok) throw new Error(`Não foi possível carregar o Agent Card (HTTP ${response.status}).`);
return response.json();
}
async rpc(method, params, { signal, timeoutMs = 15000 } = {}) {
const id = uid();
const timeout = AbortSignal.timeout(timeoutMs);
const response = await this.fetch(this.urls.rpc, {
method: 'POST', headers: { ...this.headers, 'Content-Type': 'application/json' }, credentials: 'omit',
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
});
if (!response.ok) throw new Error(`O serviço retornou HTTP ${response.status}. Confira o endereço e a autenticação.`);
const data = await response.json();
return this.validate(data, id);
}
validate(data, id) {
if (data.jsonrpc !== '2.0' || data.id !== id) throw new Error('Resposta JSON-RPC inválida.');
if (data.error) throw new RpcError(data.error);
if (!Object.hasOwn(data, 'result')) throw new Error('Resposta sem resultado A2A.');
return data.result;
}
async sendMessage(text, { contextId, taskId, tenantId, agentId, backendId, data = {}, signal } = {}) {
const legacy = this.version === '0.3';
const message = {
messageId: uid(), role: legacy ? 'user' : 'ROLE_USER', contextId,
parts: [{ text }, { data }], metadata: {},
};
if (taskId) message.taskId = taskId;
for (const [key, value] of Object.entries({ tenant_id: tenantId, agent_id: agentId, backend_id: backendId })) {
if (value) message.metadata[key] = value;
}
if (legacy) {
message.kind = 'message'; message.parts[0].kind = 'text'; message.parts[1].kind = 'data';
}
const result = await this.rpc(this.method('SendMessage', 'message/send'), {
message, configuration: legacy ? { blocking: false } : { returnImmediately: true },
}, { signal });
return normalizeResult(result);
}
async getTask(id, signal) { return this.rpc(this.method('GetTask', 'tasks/get'), { id }, { signal }); }
async cancelTask(id) { return this.rpc(this.method('CancelTask', 'tasks/cancel'), { id }); }
async *subscribe(id, signal) {
const requestId = uid();
const response = await this.fetch(this.urls.rpc, {
method: 'POST', credentials: 'omit', signal,
headers: { ...this.headers, 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({ jsonrpc: '2.0', id: requestId, method: this.method('SubscribeToTask', 'tasks/resubscribe'), params: { id } }),
});
if (!response.ok) throw new Error(`Não foi possível acompanhar os eventos (HTTP ${response.status}).`);
if (!(response.headers.get('content-type') || '').includes('text/event-stream')) {
this.validate(await response.json(), requestId);
throw new Error('O serviço não retornou um fluxo SSE.');
}
for await (const frame of parseSSE(response.body)) {
yield normalizeResult(this.validate(JSON.parse(frame), requestId));
}
}
}
export class TaskView {
constructor(task = {}) { this.task = structuredClone(task); this.task.artifacts ||= []; }
apply(event) {
const value = event.value;
if (event.type === 'task') {
if (this.task.id && value.id !== this.task.id) throw new Error('Evento de outra tarefa.');
this.task = structuredClone(value); this.task.artifacts ||= [];
}
else if (event.type === 'status') {
if (this.task.id && value.taskId !== this.task.id) throw new Error('Evento de outra tarefa.');
this.task.id = value.taskId; this.task.contextId = value.contextId; this.task.status = value.status;
} else if (event.type === 'artifact') {
if (this.task.id && value.taskId !== this.task.id) throw new Error('Evento de outra tarefa.');
const index = this.task.artifacts.findIndex(a => a.artifactId === value.artifact.artifactId);
if (index < 0) this.task.artifacts.push(structuredClone(value.artifact));
else if (value.append) this.task.artifacts[index].parts.push(...structuredClone(value.artifact.parts || []));
else this.task.artifacts[index] = structuredClone(value.artifact);
}
return this.task;
}
}
export function displayResponse(value, depth = 0) {
if (!value || depth > 5) return null;
const task = value.task || value;
const parts = task.artifacts?.at(-1)?.parts || task.status?.message?.parts || value.message?.parts || value.parts || [];
const native = parts.find(p => p.data && typeof p.data === 'object' && ('text' in p.data || 'speak' in p.data || 'metadata' in p.data))?.data;
if (native) {
const remote = native.metadata?.a2a_result;
return (remote && displayResponse(remote, depth + 1)) || native;
}
const text = parts.filter(p => typeof p.text === 'string').map(p => p.text).join('');
if (text) return { text };
const structured = parts.filter(p => 'data' in p).map(p => p.data);
return structured.length ? { text: JSON.stringify(structured.length === 1 ? structured[0] : structured, null, 2) } : null;
}

View File

@@ -1,225 +0,0 @@
import { A2AClient, TaskView, displayResponse, isStopped, needsInput, selectVersion, stateName, uid } from './a2a-client.js';
const $ = id => document.getElementById(id);
const value = id => $(id).value.trim();
const labels = { submitted:'Tarefa recebida', working:'Agente trabalhando', completed:'Concluída', failed:'Falha no processamento', canceled:'Tarefa cancelada', rejected:'Solicitação recusada', 'input-required':'Aguardando sua resposta', 'auth-required':'Aguardando autorização' };
const eventLabels = { 'flow.start':'Fluxo iniciado', 'session.upserted':'Sessão preparada', 'message.received':'Mensagem recebida', 'workflow.started':'Workflow iniciado', 'workflow.completed':'Workflow concluído', 'message.responded':'Resposta disponível', 'flow.end':'Fluxo finalizado', 'a2a.backend.status':'Status do backend atualizado' };
const state = { client:null, card:null, contextId:uid(), view:new TaskView(), connected:false, busy:false, epoch:0, controller:null, turn:null, events:0, lastState:'', scope:null };
const savedKey = 'framework-a2a-session-v1';
const connectionFields = ['serviceUrl','target','version','skill','tenant','tracking','customerKey','contractKey','agentOverride','authHeader','authValue'];
const messageGroups = new Map();
function notice(text = '') { $('notice').textContent = text; $('notice').hidden = !text; }
function setStatus(text, kind = '') { $('taskStatus').textContent = text; $('taskDot').className = `dot ${kind}`; }
function setBusy(busy) { state.busy = busy; document.body.setAttribute('aria-busy', String(busy)); controls(); }
function controls() {
const active = state.view.task.id && !isStopped(state.view.task.status?.state);
const allowSend = state.connected && !state.busy && !active;
$('message').disabled = !allowSend; $('sendButton').disabled = !allowSend;
$('cancelButton').disabled = !state.connected || !(active || needsInput(state.view.task.status?.state));
$('resumeButton').disabled = !state.connected || state.busy || !state.view.task.id;
$('newConversation').disabled = state.busy;
$('connectButton').disabled = state.busy;
for (const id of connectionFields) $(id).disabled = state.busy;
}
function log(text) {
const item = document.createElement('li');
const time = document.createElement('time'); time.textContent = new Date().toLocaleTimeString('pt-BR');
item.append(time, document.createTextNode(text)); $('activity').append(item);
while ($('activity').children.length > 80) $('activity').firstChild.remove();
$('eventCount').textContent = `${++state.events} eventos`;
}
function saveSession() {
// Only identifiers and connection settings: no credentials or message content.
try { sessionStorage.setItem(savedKey, JSON.stringify({
serviceUrl: state.client?.urls.base, version:state.client?.version, target:value('target'),
contextId:state.contextId, taskId:state.view.task.id, taskState:state.view.task.status?.state,
tenant:value('tenant'), skill:value('skill'), agentOverride:value('agentOverride'),
})); } catch { /* Storage can be unavailable in private browsing. */ }
}
function ids() {
$('taskId').textContent = state.view.task.id || '—'; $('taskShort').textContent = state.view.task.id ? `#${state.view.task.id.slice(0,8)}` : '';
$('contextId').textContent = state.contextId; $('contextShort').textContent = `Conversa ${state.contextId.slice(0,8)}`;
}
function stopWatcher() { ++state.epoch; state.controller?.abort(); state.controller = null; }
function newConversation() {
stopWatcher(); state.contextId = uid(); state.view = new TaskView(); state.turn = null; state.lastState = ''; messageGroups.clear();
$('chat').replaceChildren(); $('activity').replaceChildren(); state.events = 0; $('eventCount').textContent='0 eventos'; notice();
setBusy(false); ids(); setStatus(state.connected ? 'Pronto para conversar' : 'Pronto para conectar'); saveSession(); $('message').focus();
}
function userMessage(text) {
const div = document.createElement('div'); div.className='message user'; div.textContent=text;
$('welcome')?.remove(); $('chat').append(div); scrollChat();
}
function agentMessages(response) {
const metadata = response.metadata || {};
const candidates = response.supervisor_results || metadata.supervisor_results || response.agent_responses || metadata.agent_responses;
if (Array.isArray(candidates)) {
const parts = candidates.map(p => ({ agent:p.agent || p.agent_id || p.name || 'Agente', text:p.answer ?? p.text ?? p.message ?? p.response ?? p.content, primary:p.primary }));
const valid = parts.filter(p => p.text != null && String(p.text).trim());
if (valid.length) return valid.sort((a,b)=>Number(Boolean(a.primary))-Number(Boolean(b.primary)));
}
const text = response.text ?? response.speak ?? response.content ?? '';
return [{ agent:metadata.active_agent || metadata.agent_id || state.card?.name || 'Agente', text }];
}
function scrollChat() { $('chat').scrollTop = $('chat').scrollHeight; }
function renderResponse(response) {
if (!response) return;
$('welcome')?.remove(); state.turn ||= uid();
let group = messageGroups.get(state.turn);
if (!group) { group=document.createElement('section'); group.className='message agent'; group.setAttribute('aria-label','Resposta do agente'); messageGroups.set(state.turn,group); $('chat').append(group); }
const signature=JSON.stringify(response); if (group.dataset.signature === signature) return;
group.dataset.signature=signature; group.replaceChildren();
for (const part of agentMessages(response)) {
const section=document.createElement('section'); section.className='agent-section';
const header=document.createElement('div'); header.className='agent-header';
const avatar=document.createElement('span'); avatar.className='agent-avatar'; avatar.textContent='O'; avatar.setAttribute('aria-hidden','true');
const name=document.createElement('span'); name.textContent=String(part.agent).replaceAll('_',' ');
const body=document.createElement('div'); body.className='agent-body'; body.textContent=typeof part.text==='string' ? part.text : JSON.stringify(part.text,null,2);
header.append(avatar,name); section.append(header,body); group.append(section);
}
scrollChat();
}
function updateTask(event) {
const task=state.view.apply(event);
if (task.contextId) state.contextId=task.contextId;
const current=stateName(task.status?.state);
if (current && current!==state.lastState) { log(labels[current] || current); state.lastState=current; }
if (event.type==='status' && event.value.metadata?.framework_event) log(eventLabels[event.value.metadata.framework_event] || event.value.metadata.framework_event);
setStatus(labels[current] || 'Acompanhando tarefa', ['failed','rejected'].includes(current) ? 'error' : isStopped(current) ? '' : 'busy');
if (event.type==='artifact' || isStopped(current)) renderResponse(displayResponse(task));
ids(); saveSession(); controls();
}
function sleep(ms,signal) {
return new Promise((resolve,reject)=>{
if (signal.aborted) { reject(new DOMException('Interrompido','AbortError')); return; }
const abort=()=>{clearTimeout(timer);reject(new DOMException('Interrompido','AbortError'));};
const timer=setTimeout(()=>{signal.removeEventListener('abort',abort);resolve();},ms);
signal.addEventListener('abort',abort,{once:true});
});
}
async function watchTask() {
stopWatcher(); const epoch=state.epoch; const controller=new AbortController(); state.controller=controller;
const signal=controller.signal; const id=state.view.task.id; setBusy(true);
try {
if (value('tracking')==='stream' && state.card?.capabilities?.streaming && !isStopped(state.view.task.status?.state)) {
try {
log('Conectando ao fluxo da tarefa');
for await (const event of state.client.subscribe(id,signal)) {
if (epoch!==state.epoch) return; updateTask(event);
if (isStopped(state.view.task.status?.state)) break;
}
} catch (error) {
if (signal.aborted) return;
log('Fluxo interrompido; recuperando por consulta');
}
}
// Always obtain the authoritative final snapshot; also recovers a fast Task
// that completed before SubscribeToTask or a dropped/truncated SSE stream.
let failures=0;
while (true) {
try {
const task=await state.client.getTask(id,signal);
if (epoch!==state.epoch) return;
updateTask({type:'task',value:task}); failures=0;
if (isStopped(task.status?.state)) break;
} catch (error) {
if (signal.aborted) return;
if (++failures>=3 || error.code) throw error;
log('Conexão indisponível; tentando consultar novamente');
}
await sleep(failures ? failures*1000 : 900,signal);
}
} catch (error) {
if (!signal.aborted && epoch===state.epoch) {
notice(`Acompanhamento pausado: ${error.message} Use Retomar para consultar a mesma tarefa. Nenhuma mensagem foi reenviada.`);
setStatus('Acompanhamento pausado','error'); log('Acompanhamento pausado');
}
} finally {
if (epoch===state.epoch) { setBusy(false); $('message').focus(); }
}
}
function authHeaders() {
const secret=$('authValue').value;
if (!secret) return {};
const name=value('authHeader');
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) || /[\r\n]/.test(secret)) throw new Error('Cabeçalho de autenticação inválido.');
if (['content-type','accept','a2a-version','host','origin','cookie'].includes(name.toLowerCase())) throw new Error('Use Authorization ou o cabeçalho de autenticação configurado no serviço.');
return {[name]:secret};
}
$('connectionForm').addEventListener('submit',async event=>{
event.preventDefault(); notice(); setBusy(true); setStatus('Conectando…','busy');
try {
const candidate=new A2AClient({url:value('serviceUrl'),version:value('version')==='0.3'?'0.3':'1.0',headers:authHeaders()});
const card=await candidate.discover(AbortSignal.timeout(12000)); candidate.version=selectVersion(card,value('version')); candidate.headers['A2A-Version']=candidate.version;
const scope=JSON.stringify([candidate.urls.base,candidate.version,value('target'),value('tenant'),value('skill'),value('agentOverride')]);
if (state.scope && scope!==state.scope) newConversation();
state.scope=scope; state.client=candidate; state.card=card; state.connected=true;
const previous=value('skill'); $('skill').replaceChildren(new Option('Seleção automática',''));
for (const skill of card.skills || []) $('skill').append(new Option(skill.name || skill.id,skill.id));
$('skill').value=[...$('skill').options].some(o=>o.value===previous)?previous:'';
$('serviceName').textContent=card.name || 'Serviço A2A'; $('connectionDetail').textContent=`${(card.skills || []).length === 1 ? '1 opção disponível' : `${(card.skills || []).length} opções disponíveis`} · ${card.capabilities?.streaming ? 'Atualizações em tempo real' : 'Consulta de tarefas'}`;
$('connectionDot').className='dot'; $('versionBadge').textContent=`A2A ${candidate.version}`;
setStatus(state.view.task.id?'Conectado · tarefa disponível para consulta':'Pronto para conversar');
log('Conectado ao serviço'); ids(); saveSession();
} catch(error) {
state.connected=false; $('connectionDot').className='dot error'; $('connectionDetail').textContent='Não foi possível conectar.';
setStatus('Falha na conexão','error'); notice(`${error.message} Se o navegador bloquear a conexão, confira CORS_ORIGINS no serviço e use HTTP/HTTPS para servir esta página.`);
} finally { setBusy(false); }
});
$('messageForm').addEventListener('submit',async event=>{
event.preventDefault(); const text=value('message'); if(!text || !state.connected || $('sendButton').disabled) return;
notice(); const previousTask=state.view.task; const resumeId=needsInput(previousTask.status?.state)?previousTask.id:undefined;
state.turn=uid(); userMessage(text); $('message').value=''; $('message').style.height='auto'; setBusy(true); setStatus('Enviando mensagem…','busy');
const messageId=uid();
const business_context={customer_key:value('customerKey')||null,contract_key:value('contractKey')||null,interaction_key:messageId,session_key:state.contextId};
const data={message_id:messageId,business_context,context:{business_context}};
if(value('customerKey')) data.customer_key=value('customerKey');
if(value('contractKey')) data.contract_key=value('contractKey');
try {
const gateway=value('target')==='gateway';
const result=await state.client.sendMessage(text,{contextId:state.contextId,taskId:resumeId,tenantId:value('tenant')||'default',
agentId:gateway?value('agentOverride'):value('skill'),backendId:gateway?value('skill'):undefined,data});
if(result.type==='message') {
state.view=new TaskView(); if(result.value.contextId) state.contextId=result.value.contextId;
renderResponse(displayResponse(result.value)); setStatus('Resposta recebida'); ids(); saveSession(); setBusy(false);
} else if(result.type==='task') {
state.view=new TaskView(); state.lastState=''; updateTask(result); log(`Tarefa ${state.view.task.id.slice(0,8)} recebida`); await watchTask();
} else throw new Error('Resposta de envio inesperada.');
} catch(error) {
notice(`Não foi possível confirmar o envio: ${error.message} Ele pode ter sido aceito pelo serviço; não houve reenvio automático.`);
setStatus('Envio não confirmado','error'); setBusy(false);
}
});
$('resumeButton').addEventListener('click',()=>{notice();state.turn ||= uid();watchTask();});
$('cancelButton').addEventListener('click',async()=>{
const id=state.view.task.id; if(!id || !state.connected) return;
stopWatcher(); setBusy(true); setStatus('Solicitando cancelamento…','busy');
try { updateTask({type:'task',value:await state.client.cancelTask(id)});log('Cancelamento confirmado pelo serviço');notice(); }
catch(error) {
try {updateTask({type:'task',value:await state.client.getTask(id)});} catch { /* Keep the last known Task. */ }
notice(`O serviço não confirmou o cancelamento: ${error.message} Consulte a tarefa com Retomar.`);
} finally {setBusy(false);}
});
$('newConversation').addEventListener('click',newConversation);
$('message').addEventListener('keydown',event=>{if(event.key==='Enter'&&!event.shiftKey&&!event.isComposing){event.preventDefault();$('messageForm').requestSubmit();}});
$('message').addEventListener('input',()=>{ $('message').style.height='auto';$('message').style.height=`${Math.min($('message').scrollHeight,150)}px`; });
for(const button of document.querySelectorAll('[data-prompt]')) button.addEventListener('click',()=>{if(!$('message').disabled){$('message').value=button.dataset.prompt;$('message').focus();}else notice('Conecte ao serviço para iniciar a conversa.');});
$('target').addEventListener('change',()=>{ $('skillLabel').firstChild.textContent=value('target')==='gateway'?'Backend no Gateway':'Agente'; });
for(const id of ['serviceUrl','version','target','tenant','skill','agentOverride','authHeader','authValue']) $(id).addEventListener('change',()=>{
if(state.connected){state.connected=false;$('connectionDot').className='dot muted';setStatus('Configuração alterada · conecte novamente');controls();}
});
try {
const saved=JSON.parse(sessionStorage.getItem(savedKey)||'null');
if(saved?.serviceUrl){
$('serviceUrl').value=saved.serviceUrl;$('version').value=saved.version||'auto';$('target').value=saved.target||'backend';$('tenant').value=saved.tenant||'default';$('agentOverride').value=saved.agentOverride||'';
if(saved.skill)$('skill').append(new Option(saved.skill,saved.skill));$('skill').value=saved.skill||'';
state.contextId=saved.contextId||uid();state.view=new TaskView(saved.taskId?{id:saved.taskId,contextId:state.contextId,status:{state:saved.taskState}}:{});
state.scope=JSON.stringify([saved.serviceUrl,saved.version,saved.target||'backend',saved.tenant||'default',saved.skill||'',saved.agentOverride||'']);
if(saved.taskId)notice('Uma tarefa desta aba está disponível. Conecte ao mesmo serviço e clique em Retomar para consultar o resultado.');
}
} catch { /* An unavailable or invalid saved session starts a fresh conversation. */ }
const requestedService=new URLSearchParams(location.search).get('service');
if(requestedService)$('serviceUrl').value=requestedService;
else if(!state.scope && location.port==='8000')$('serviceUrl').value=location.origin;
$('target').dispatchEvent(new Event('change'));ids();controls();

View File

@@ -1,52 +0,0 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Oracle Agent Workspace · A2A</title><link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="workspace">
<aside class="sidebar">
<a class="brand" href="./"><img class="oracle-logo" src="oracle-logo.svg" alt="Oracle"><span class="brand-copy">Agent Workspace<small>FRAMEWORK OCI · A2A</small></span></a>
<div class="sidebar-heading"><span>CONEXÃO</span><span class="badge" id="versionBadge">A2A</span></div>
<form id="connectionForm" class="connection-form">
<label>Endereço do serviço<input id="serviceUrl" type="url" value="http://localhost:8000" required spellcheck="false"></label>
<div class="two-columns">
<label>Destino<select id="target" aria-label="Destino"><option value="backend">Agente</option><option value="gateway">Gateway</option></select></label>
<label>Versão<select id="version" aria-label="Versão"><option value="auto">Automática</option><option value="1.0">A2A 1.0</option><option value="0.3">A2A 0.3</option></select></label>
</div>
<button id="connectButton" class="primary" type="submit">Conectar ao serviço <span aria-hidden="true">↗</span></button>
</form>
<div class="service-card"><span id="connectionDot" class="dot"></span><div><strong id="serviceName">Nenhum serviço conectado</strong><p id="connectionDetail">Conecte para carregar os agentes disponíveis.</p></div></div>
<div class="sidebar-heading"><span>CONVERSA</span></div>
<label id="skillLabel">Agente<select id="skill" aria-label="Agente ou backend"><option value="">Seleção automática</option></select></label>
<label>Tenant<input id="tenant" value="default" spellcheck="false"></label>
<label>Acompanhamento<select id="tracking" aria-label="Acompanhamento"><option value="stream">Tempo real</option><option value="poll">Consulta periódica</option></select></label>
<details class="advanced"><summary>Contexto e autenticação</summary>
<label>Identificador do cliente<input id="customerKey" placeholder="customer_key" autocomplete="off"></label>
<label>Contrato ou pedido<input id="contractKey" placeholder="contract_key" autocomplete="off"></label>
<label>Perfil do agente no backend<input id="agentOverride" placeholder="Opcional; usado pelo Gateway" autocomplete="off"></label>
<label>Cabeçalho de autenticação<input id="authHeader" value="Authorization" autocomplete="off" spellcheck="false"></label>
<label>Valor do cabeçalho<input id="authValue" type="password" placeholder="Bearer … ou credencial configurada" autocomplete="off"></label>
<p class="hint">A credencial fica apenas na memória desta página.</p>
</details>
<div class="sidebar-footer"><span class="dot muted"></span> Protocolo A2A · JSON-RPC + SSE</div>
</aside>
<main class="main">
<header class="topbar"><div><span class="eyebrow">SEU ESPAÇO DE TRABALHO</span><h1>Converse com seus agentes</h1></div><button id="newConversation" class="secondary" type="button">+ Nova conversa</button></header>
<section class="task-strip" aria-label="Acompanhamento da tarefa">
<div class="status-pill"><span id="taskDot" class="dot muted"></span><span id="taskStatus" role="status" aria-live="polite">Pronto para conectar</span></div>
<span id="taskShort" class="task-id"></span>
<div class="task-actions"><button id="resumeButton" class="text-button" type="button" disabled>Retomar</button><button id="cancelButton" class="text-button danger" type="button" disabled>Cancelar tarefa</button></div>
</section>
<div id="notice" class="notice" role="alert" hidden></div>
<section id="chat" class="chat" role="log" aria-label="Conversa com o agente" aria-live="polite">
<div id="welcome" class="welcome"><span class="eyebrow">CONVERSAS QUE VIRAM AÇÕES</span><h2>O próximo passo<br>começa com uma mensagem.</h2><p>Conecte um agente ou o Gateway do framework.<br>Acompanhe cada tarefa até a resposta.</p><div class="suggestions"><button type="button" data-prompt="O que você pode fazer por mim?">Conhecer o agente <span>↗</span></button><button type="button" data-prompt="Quero consultar meu pedido.">Consultar um pedido <span>↗</span></button></div></div>
</section>
<div class="composer-area"><form id="messageForm" class="composer"><label class="sr-only" for="message">Mensagem para o agente</label><textarea id="message" rows="1" placeholder="Escreva uma mensagem para começar…" disabled></textarea><button id="sendButton" type="submit" class="send-button" aria-label="Enviar mensagem" disabled>↑</button></form><div class="composer-footer"><span>Enter envia · Shift + Enter quebra a linha</span><span id="contextShort">Conversa nova</span></div></div>
<details class="activity-panel"><summary>Atividade da tarefa <span id="eventCount">0 eventos</span></summary><ol id="activity"></ol><dl><dt>Contexto</dt><dd id="contextId">—</dd><dt>Tarefa</dt><dd id="taskId">—</dd></dl></details>
</main>
</div>
<script type="module" src="app.js"></script>
</body>
</html>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#EA1B22" fill-rule="evenodd" d="M.1 8c0 2.761 2.237 5 4.997 5h5.806A4.999 4.999 0 0015.9 8c0-2.761-2.237-5-4.997-5H5.097A4.999 4.999 0 00.1 8zm13.911 0a3.235 3.235 0 01-3.234 3.237h-5.55A3.235 3.235 0 011.991 8a3.235 3.235 0 013.234-3.236h5.551A3.235 3.235 0 0114.011 8z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 531 B

View File

@@ -1 +0,0 @@
{"name":"agent-framework-a2a-frontend","private":true,"type":"module","scripts":{"test":"node --test tests/*.test.js"}}

File diff suppressed because one or more lines are too long

View File

@@ -1,86 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { A2AClient, TaskView, displayResponse, parseSSE, serviceURLs, selectVersion, normalizeResult, isStopped, needsInput, RpcError } from '../a2a-client.js';
function bodyFor(text, size = 1) {
const bytes=new TextEncoder().encode(text);
return new ReadableStream({start(controller){for(let i=0;i<bytes.length;i+=size)controller.enqueue(bytes.slice(i,i+size));controller.close();}});
}
test('SSE handles fragmented UTF-8, CRLF, comments and multi-line data',async()=>{
const text=': ping\r\nid: 1\r\nevent: message\r\ndata: {"text":\r\ndata: "Olá 👋"}\r\n\r\ndata: second\n\n';
const frames=[];for await(const frame of parseSSE(bodyFor(text)))frames.push(frame);
assert.deepEqual(frames,['{"text":\n"Olá 👋"}','second']);
});
test('SSE discards an unfinished frame for recovery by GetTask',async()=>{
const frames=[];for await(const frame of parseSSE(bodyFor('data: complete\n\ndata: truncated')))frames.push(frame);
assert.deepEqual(frames,['complete']);
});
test('service URLs accept a base or /a2a and reject embedded credentials',()=>{
assert.equal(serviceURLs('http://localhost:8000/a2a/').rpc,'http://localhost:8000/a2a');
assert.equal(serviceURLs('https://host/prefix').card,'https://host/prefix/.well-known/agent-card.json');
for(const url of ['javascript:alert(1)','https://user:secret@host','https://host?token=secret'])assert.throws(()=>serviceURLs(url));
});
test('version selection uses JSONRPC interfaces and allows explicit legacy compatibility',()=>{
assert.equal(selectVersion({supportedInterfaces:[{protocolBinding:'JSONRPC',protocolVersion:'1.0'}]}),'1.0');
assert.equal(selectVersion({protocolVersion:'0.3.0'}),'0.3');
assert.equal(selectVersion({},'0.3'),'0.3');
assert.throws(()=>selectVersion({supportedInterfaces:[{protocolBinding:'GRPC',protocolVersion:'1.0'}]}));
});
test('both versions normalize task, message and streaming events',()=>{
assert.equal(normalizeResult({statusUpdate:{taskId:'t'}}).type,'status');
assert.equal(normalizeResult({kind:'artifact-update',taskId:'t'}).type,'artifact');
assert.equal(normalizeResult({message:{messageId:'m'}}).type,'message');
assert.throws(()=>normalizeResult({unexpected:true}));
});
test('TaskView updates status, replaces snapshots and appends artifact chunks',()=>{
const view=new TaskView({id:'t',artifacts:[]});
view.apply({type:'artifact',value:{taskId:'t',artifact:{artifactId:'a',parts:[{text:'Hel'}]}}});
view.apply({type:'artifact',value:{taskId:'t',append:true,artifact:{artifactId:'a',parts:[{text:'lo'}]}}});
assert.equal(displayResponse(view.task).text,'Hello');
view.apply({type:'status',value:{taskId:'t',contextId:'c',status:{state:'TASK_STATE_COMPLETED'}}});
assert.ok(isStopped(view.task.status.state));assert.ok(needsInput('input-required'));
assert.throws(()=>view.apply({type:'status',value:{taskId:'other',status:{state:'working'}}}));
});
test('framework text/data duplicates render once and native agent metadata survives',()=>{
const native={text:'answer',metadata:{agent_responses:[{agent:'billing',text:'answer'}]}};
const task={artifacts:[{parts:[{text:'answer'},{data:native}]}],status:{message:{parts:[{text:'answer'}]}}};
assert.deepEqual(displayResponse(task),native);
const wrapped={parts:[{data:{text:'answer',metadata:{a2a_result:{task}}}}]};
assert.deepEqual(displayResponse(wrapped),native);
});
for(const legacy of [false,true]){
test(`send/get/cancel use correct envelopes and credentials (${legacy?'0.3':'1.0'})`,async()=>{
const requests=[];
const fetchImpl=async function(url,options){
assert.equal(this,globalThis);
const body=JSON.parse(options.body);requests.push({url,options,body});
return Response.json({jsonrpc:'2.0',id:body.id,result:body.method.includes('send')||body.method==='SendMessage' ? (legacy?{kind:'task',id:'t',status:{state:'working'}}:{task:{id:'t',status:{state:'TASK_STATE_WORKING'}}}):{id:'t'}});
};
const client=new A2AClient({url:'https://service',version:legacy?'0.3':'1.0',headers:{Authorization:'Bearer test'},fetchImpl});
await client.sendMessage('hello',{contextId:'c',agentId:'a',backendId:'billing',tenantId:'tenant',data:{business_context:{customer_key:'customer'}}});
await client.getTask('t');await client.cancelTask('t');
assert.deepEqual(requests.map(r=>r.body.method),legacy?['message/send','tasks/get','tasks/cancel']:['SendMessage','GetTask','CancelTask']);
const params=requests[0].body.params;
assert.equal(params.message.role,legacy?'user':'ROLE_USER');assert.equal(params.message.metadata.backend_id,'billing');
assert.equal(params.message.metadata.agent_id,'a');assert.equal(params.message.contextId,'c');
assert.equal(legacy?params.configuration.blocking:params.configuration.returnImmediately,!legacy);
assert.equal(requests[0].options.headers.Authorization,'Bearer test');
assert.equal(requests[0].options.credentials,'omit');
});
}
test('JSON-RPC errors and mismatched response IDs never become successful responses',async()=>{
const bad=new A2AClient({url:'https://service',fetchImpl:async()=>Response.json({jsonrpc:'2.0',id:'wrong',result:{}})});
await assert.rejects(bad.getTask('t'),/JSON-RPC/);
const failure=new A2AClient({url:'https://service',fetchImpl:async(url,o)=>Response.json({jsonrpc:'2.0',id:JSON.parse(o.body).id,error:{code:-32001,message:'Task missing'}})});
await assert.rejects(failure.getTask('t'),RpcError);
});
test('POST SSE supports auth headers, validates envelopes and streams canonical events',async()=>{
const client=new A2AClient({url:'https://service',headers:{'x-api-key':'test'},fetchImpl:async(url,o)=>{
assert.equal(o.method,'POST');assert.equal(o.headers['x-api-key'],'test');
const body=JSON.parse(o.body);assert.equal(body.method,'SubscribeToTask');
return new Response(bodyFor(`data: ${JSON.stringify({jsonrpc:'2.0',id:body.id,result:{statusUpdate:{taskId:'t',status:{state:'TASK_STATE_COMPLETED'}}}})}\n\n`),{headers:{'content-type':'text/event-stream'}});
}});
const events=[];for await(const event of client.subscribe('t'))events.push(event);
assert.equal(events[0].type,'status');assert.equal(events[0].value.taskId,'t');
});

View File

@@ -1,32 +0,0 @@
// Run against examples/a2a_frontend_demo.py with A2A_DEMO_URL=http://127.0.0.1:8765.
import test from 'node:test';
import assert from 'node:assert/strict';
import { A2AClient, TaskView, displayResponse, isStopped, stateName } from '../a2a-client.js';
const url=process.env.A2A_DEMO_URL;
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function complete(client,task){
const deadline=Date.now()+6000;
while(!isStopped(task.status?.state)&&Date.now()<deadline){await delay(50);task=await client.getTask(task.id);}
assert.ok(isStopped(task.status?.state));return task;
}
for(const version of ['1.0','0.3']){
test(`SDK integration: submit, subscribe, poll and resume (${version})`,{skip:!url},async()=>{
const client=new A2AClient({url,version});const card=await client.discover();assert.ok(card.capabilities.streaming);
const first=await client.sendMessage('hello',{contextId:crypto.randomUUID()});assert.equal(first.type,'task');assert.ok(!isStopped(first.value.status.state));
const view=new TaskView(first.value);let count=0;
for await(const event of client.subscribe(first.value.id)){view.apply(event);count++;}
assert.ok(count>0);const final=await client.getTask(first.value.id);
assert.equal(stateName(final.status.state),'completed');assert.match(displayResponse(final).text,/hello/);
const prompt=await client.sendMessage('/confirmar',{contextId:crypto.randomUUID()});
const waiting=await complete(client,prompt.value);assert.equal(stateName(waiting.status.state),'input-required');
const resumed=await client.sendMessage('sim',{contextId:waiting.contextId,taskId:waiting.id});
const finished=await complete(client,resumed.value);assert.equal(finished.id,waiting.id);assert.equal(stateName(finished.status.state),'completed');
});
}
test('SDK integration: cancellation and failed tasks',{skip:!url},async()=>{
const client=new A2AClient({url});
const slow=await client.sendMessage('/lento',{contextId:crypto.randomUUID()});
await delay(50);const canceled=await client.cancelTask(slow.value.id);assert.equal(stateName(canceled.status.state),'canceled');
const failed=await client.sendMessage('/erro',{contextId:crypto.randomUUID()});
assert.equal(stateName((await complete(client,failed.value)).status.state),'failed');
});

View File

@@ -1,7 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
COPY libs/agent_framework /agent_framework
COPY apps/agent_gateway /app
RUN pip install --no-cache-dir -e '/agent_framework[a2a]' -r requirements.txt
ENV ENABLE_A2A=true
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"]

View File

@@ -71,13 +71,3 @@ O Gateway emite eventos de observabilidade:
- `IC.GLOBAL_GATEWAY_COMPLETED` - `IC.GLOBAL_GATEWAY_COMPLETED`
- `NOC.005` em falhas - `NOC.005` em falhas
- `NOC.006` em conclusão HTTP - `NOC.006` em conclusão HTTP
## A2A opcional
A fachada A2A 1.0, a compatibilidade 0.3 e o catálogo misto de backends são
explicados em [Integração A2A](../../docs/A2A_IMPLEMENTACAO.md).
A ativação é opcional (`ENABLE_A2A=false` por padrão).
A fachada também oferece [Tasks A2A assíncronas e streaming](../../docs/A2A_ASSINCRONO.md),
com envio imediato, consulta, reconexão e cancelamento. O template de backend
pode publicar o mesmo protocolo diretamente.

View File

@@ -16,7 +16,7 @@ from agent_framework.global_supervisor import (
GlobalSupervisorRouter, GlobalSupervisorRouter,
InMemoryGlobalSessionStore, InMemoryGlobalSessionStore,
) )
from app.supervisor_llm import build_supervisor_llm from agent_framework.llm.providers import create_llm
from agent_framework.observability.observer import AgentObserver from agent_framework.observability.observer import AgentObserver
from agent_framework.security import install_authentication from agent_framework.security import install_authentication
@@ -38,7 +38,7 @@ app.add_middleware(
registry = BackendRegistry.from_yaml(settings.BACKENDS_CONFIG_PATH) registry = BackendRegistry.from_yaml(settings.BACKENDS_CONFIG_PATH)
analytics = create_analytics_publisher(settings) analytics = create_analytics_publisher(settings)
observer = AgentObserver(analytics=analytics) observer = AgentObserver(analytics=analytics)
llm = build_supervisor_llm(settings) if settings.GLOBAL_ROUTING_MODE in {"supervisor", "hybrid"} else None llm = create_llm(settings)
session_store = InMemoryGlobalSessionStore(ttl_seconds=settings.GLOBAL_SESSION_TTL_SECONDS) session_store = InMemoryGlobalSessionStore(ttl_seconds=settings.GLOBAL_SESSION_TTL_SECONDS)
router = GlobalSupervisorRouter( router = GlobalSupervisorRouter(
registry=registry, registry=registry,
@@ -219,10 +219,6 @@ import asyncio
@app.get("/gateway/events/{session_id:path}") @app.get("/gateway/events/{session_id:path}")
async def gateway_events(session_id: str): async def gateway_events(session_id: str):
existing = await session_store.get(session_id)
if existing and existing.active_backend and registry.get(existing.active_backend).protocol == "a2a":
raise HTTPException(status_code=409, detail="A2A backend: use message/send; native event proxy is unavailable")
async def stream(): async def stream():
yield ( yield (
"event: connected\n" "event: connected\n"
@@ -254,9 +250,6 @@ async def gateway_events(session_id: str):
return return
backend = registry.get(backend_id) backend = registry.get(backend_id)
if backend.protocol == "a2a":
yield 'event: error\ndata: {"message":"Native event proxy is unavailable for A2A backends"}\n\n'
return
backend_base_url = ( backend_base_url = (
getattr(backend, "base_url", None) getattr(backend, "base_url", None)
@@ -333,16 +326,3 @@ async def gateway_events(session_id: str):
"X-Accel-Buffering": "no", "X-Accel-Buffering": "no",
}, },
) )
# Opt-in: existing deployments do not import or require the A2A SDK.
if settings.ENABLE_A2A:
from agent_framework.a2a.server import install_a2a
async def _a2a_gateway_handler(data: dict):
return await gateway_message(GatewayRequest(**data))
install_a2a(
app, _a2a_gateway_handler, registry, settings.A2A_PUBLIC_URL,
settings.A2A_SECURITY_SCHEMES or None, settings.A2A_SECURITY or None,
task_mode=settings.A2A_TASK_MODE, timeout_seconds=settings.A2A_TASK_TIMEOUT_SECONDS,
)

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from functools import lru_cache from functools import lru_cache
from typing import Literal from typing import Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -17,13 +16,6 @@ class GatewaySettings(BaseSettings):
API_PORT: int = 8010 API_PORT: int = 8010
CORS_ORIGINS: str = "http://localhost:5173" CORS_ORIGINS: str = "http://localhost:5173"
ENABLE_A2A: bool = False
A2A_TASK_MODE: bool = True
A2A_TASK_TIMEOUT_SECONDS: float = Field(default=120.0, gt=0)
A2A_PUBLIC_URL: str = "http://localhost:8010"
A2A_SECURITY_SCHEMES: dict = Field(default_factory=dict)
A2A_SECURITY: list[dict[str, list[str]]] = Field(default_factory=list)
BACKENDS_CONFIG_PATH: str = "./config/backends.yaml" BACKENDS_CONFIG_PATH: str = "./config/backends.yaml"
GLOBAL_ROUTING_MODE: Literal["router", "supervisor", "hybrid"] = "hybrid" GLOBAL_ROUTING_MODE: Literal["router", "supervisor", "hybrid"] = "hybrid"
GLOBAL_KEEP_ACTIVE_BACKEND: bool = True GLOBAL_KEEP_ACTIVE_BACKEND: bool = True
@@ -46,13 +38,6 @@ class GatewaySettings(BaseSettings):
LANGFUSE_HOST: str = "https://cloud.langfuse.com" LANGFUSE_HOST: str = "https://cloud.langfuse.com"
MODEL_PRICES_JSON: str | None = None MODEL_PRICES_JSON: str | None = None
USD_BRL_RATE: str = "5.0" USD_BRL_RATE: str = "5.0"
OCI_COMPARTMENT_ID: str | None = None
OCI_GENAI_PROJECT_OCID: str | None = None
OCI_AUTH_MODE: str = "config_file"
OCI_CONFIG_FILE: str = "~/.oci/config"
OCI_CONFIG_PROFILE: str = "DEFAULT"
OCI_REGION: str | None = None
# Analytics/Observer do próprio gateway. # Analytics/Observer do próprio gateway.
ENABLE_ANALYTICS: bool = False ENABLE_ANALYTICS: bool = False

View File

@@ -1,102 +0,0 @@
"""Gateway-only bootstrap for the Global Supervisor LLM.
Resolve the supervisor entry in llm_profiles.yaml *before* create_llm().
This matters because create_llm() selects OCISDKProvider immediately when
LLM_PROVIDER=oci_sdk; OCISDKProvider does not dynamically switch providers.
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
import yaml
from agent_framework.llm.providers import create_llm
logger = logging.getLogger("agent_gateway.supervisor_llm")
def _profile_file(settings) -> Path:
explicit = (
os.getenv("GLOBAL_SUPERVISOR_LLM_PROFILES_PATH")
or getattr(settings, "GLOBAL_SUPERVISOR_LLM_PROFILES_PATH", None)
or os.getenv("LLM_PROFILES_PATH")
or getattr(settings, "LLM_PROFILES_PATH", None)
)
if explicit:
path = Path(str(explicit)).expanduser().resolve()
if not path.is_file():
raise RuntimeError(f"Supervisor LLM profile YAML not found: {path}")
return path
candidates = (
"config/llm_profiles.yaml",
"llm_profiles.yaml",
"apps/agent_gateway/config/llm_profiles.yaml",
"apps/agent_template_backend/config/llm_profiles.yaml",
"apps/agent_template_backend/llm_profiles.yaml",
)
for candidate in candidates:
path = Path(candidate)
if path.is_file():
return path.resolve()
raise RuntimeError(
"No llm_profiles.yaml found for Gateway supervisor. Set "
"GLOBAL_SUPERVISOR_LLM_PROFILES_PATH to the EXISTING YAML file path."
)
def build_supervisor_llm(settings):
"""Create LLM from the supervisor profile without mutating global settings."""
path = _profile_file(settings)
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
profile = (data.get("profiles") or {}).get("supervisor")
if not isinstance(profile, dict):
raise RuntimeError(f"Missing profiles.supervisor in {path}")
effective_provider = str(profile.get("provider") or settings.LLM_PROVIDER).strip()
allowed = {"oci_openai", "openai_compatible", "oci_sdk", "mock"}
if effective_provider not in allowed:
raise RuntimeError(f"Unsupported supervisor provider: {effective_provider}")
configured_url = (
profile.get("base_url")
or profile.get("service_endpoint")
or getattr(settings, "OCI_GENAI_BASE_URL", None)
)
url = str(configured_url or "").strip().rstrip("/")
if effective_provider == "oci_openai":
# OCI native URL is commonly used by the process-wide .env when the
# default provider is oci_sdk. Translate only the well-known OCI host.
if url.startswith("https://inference.generativeai.") and url.endswith(".oci.oraclecloud.com"):
url += "/openai/v1"
if not url.endswith("/openai/v1"):
raise RuntimeError(
f"Supervisor OpenAI-compatible base URL must end with /openai/v1, got {url!r}. "
"Set base_url under profiles.supervisor to the working endpoint."
)
key = profile.get("api_key") or getattr(settings, "OCI_GENAI_API_KEY", None) or os.getenv("OCI_GENAI_API_KEY")
if effective_provider in {"oci_openai", "openai_compatible"} and not key:
raise RuntimeError(
"Supervisor profile requires OCI_GENAI_API_KEY (or profiles.supervisor.api_key). "
"OCI SDK config_file signing is NOT an OpenAI-compatible bearer API key."
)
overrides = {
"LLM_PROVIDER": effective_provider,
"OCI_GENAI_MODEL": str(profile.get("model") or settings.OCI_GENAI_MODEL),
"OCI_GENAI_BASE_URL": url,
"LLM_TEMPERATURE": float(profile.get("temperature", settings.LLM_TEMPERATURE)),
"LLM_MAX_TOKENS": int(profile.get("max_tokens", settings.LLM_MAX_TOKENS)),
}
if key:
overrides["OCI_GENAI_API_KEY"] = str(key)
supervisor_settings = settings.model_copy(update=overrides)
logger.info(
"Supervisor LLM profile loaded path=%s provider=%s model=%s base_url=%s",
path, effective_provider, overrides["OCI_GENAI_MODEL"], url,
)
return create_llm(supervisor_settings)

View File

@@ -1,27 +0,0 @@
#default_backend: native
#backends:
# native:
# url: http://localhost:8000
# description: Agente nativo do framework
# keywords: [conta, fatura]
# # protocol omitido equivale a native
# external:
# protocol: a2a
# a2a_version: '1.0' # ou '0.3'
# url: https://external-agent.example
# a2a_rpc_path: /a2a # endpoint definido pelo operador a partir do Agent Card
# a2a_agent_card_path: /.well-known/agent-card.json
# description: Agente externo A2A
# keywords: [suporte]
# a2a_headers_env:
# Authorization: EXTERNAL_AGENT_AUTHORIZATION # valor completo: Bearer <token>
default_backend: agent_template
backends:
agent_template:
protocol: a2a
a2a_version: "1.0"
url: http://localhost:8000
a2a_rpc_path: /a2a
a2a_agent_card_path: /.well-known/agent-card.json

View File

@@ -1,93 +0,0 @@
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
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router:
provider: oci_openai
model: openai.gpt-4.1
temperature: 0
max_tokens: 768
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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
# Generated by prepare_env.py. Do not commit.
ADB_DSN=oradb23ai_high
ADB_PASSWORD=Moniquinha19721972
ADB_TABLE_PREFIX=AGENTFW
ADB_USER=admin
ADB_WALLET_LOCATION=/app/wallet
ADB_WALLET_PASSWORD=Moniquinha1972
AGENT_PUBSUB_TOPIC=teste_pubsub
ANALYTICS_PROVIDERS=pubsub
API_HOST=0.0.0.0
API_PORT=8000
APP_ENV=oke-load-test
APP_NAME=ai-agent-template
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
DEFAULT_CHANNEL=web
EMBEDDING_PROVIDER=oci
ENABLE_ANALYTICS=false
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
ENABLE_INPUT_GUARDRAILS=true
ENABLE_JUDGES=true
ENABLE_LANGFUSE=true
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
ENABLE_LLM_ROUTER=true
ENABLE_LONG_TERM_MEMORY=true
ENABLE_MCP_TOOLS=false
ENABLE_OCI_STREAMING=false
ENABLE_OTEL=false
ENABLE_OUTPUT_GUARDRAILS=true
ENABLE_OUTPUT_SUPERVISOR=true
ENABLE_PARALLEL_GUARDRAILS=true
ENABLE_REDIS_CACHE=false
ENABLE_ROUTE_STICKINESS=true
ENABLE_SUPERVISOR=true
ENABLE_TEXT_ADAPTER=true
ENABLE_VOICE_ADAPTER=true
ENABLE_WHATSAPP_ADAPTER=true
END_SESSION_MESSAGE='Atendimento encerrado. Obrigado pelo contato.'
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
GCP_PROJECT_ID=project-7f829b68-bf43-442c-ad3
GCP_PUBSUB_TIMEOUT_SECONDS=30
GCP_PUBSUB_TOPIC=teste_pubsub
GCP_PUBSUB_TOPIC_PATH=projects/project-7f829b68-bf43-442c-ad3/topics/teste_pubsub
GOOGLE_APPLICATION_CREDENTIALS=/mnt/d/Dropbox/ORACLE/TIM/FY27/Testes/sa-hoshikawa.json
GRAPH_STORE_PROVIDER=autonomous
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
GUARDRAILS_FAIL_FAST=true
HUMAN_HANDOFF_MESSAGE='Vou encaminhar seu atendimento para uma pessoa.'
IDENTITY_CONFIG_PATH=./config/identity.yaml
IMAGE_TAG=loadtest
JUDGES_CONFIG_PATH=./config/judges.yaml
K8S_NAMESPACE=agent-load-test
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES='AGA.,NOC., IC.'
LANGFUSE_HOST=http://langfuse-web.langfuse.svc.cluster.local:3000
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
LANGFUSE_IGNORE_HEALTHCHECKS=true
LANGFUSE_PUBLIC_KEY=pk-lf-2c82b991104f3f38f41878baecaaffd0
LANGFUSE_SECRET_KEY=sk-lf-defd21d2c6da3d4a9f301d993236958f9e56d94f9050a00f79067267b78a00a0
LANGFUSE_TRACE_MODE=compact
LLM_MAX_TOKENS=2048
LLM_PROVIDER=oci_sdk
LLM_TEMPERATURE=0.2
LLM_TIMEOUT_SECONDS=120
LOADTEST_AGENT_ID=telecom_contas
LOADTEST_CPU_LIMIT=2000m
LOADTEST_CPU_REQUEST=500m
LOADTEST_DURATION=10m
LOADTEST_MAX_REPLICAS=30
LOADTEST_MAX_VUS=1000
LOADTEST_MEMORY_LIMIT=2Gi
LOADTEST_MEMORY_REQUEST=768Mi
LOADTEST_MESSAGE='Explique em uma frase o que é uma fatura de telecom.'
LOADTEST_MIN_REPLICAS=4
LOADTEST_RPS=100
LOADTEST_SCENARIO=unique_sessions
LOADTEST_TARGET=http://agent-template-backend-lb.agent-load-test.svc.cluster.local:8000
LOADTEST_TENANT_ID=loadtest
LOADTEST_VUS=250
LOG_LEVEL=INFO
LONG_TERM_MEMORY_AUTO_EXTRACT=true
LONG_TERM_MEMORY_INJECT_CONTEXT=true
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
LONG_TERM_MEMORY_PROVIDER=sqlite
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
MCP_GATEWAY_ENABLED=false
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
MEMORY_CONTEXT_STRATEGY=summary
MEMORY_HISTORY_LIMIT=80
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
MEMORY_MAX_SUMMARY_CHARS=6000
MEMORY_RECENT_MESSAGES_LIMIT=8
MEMORY_REPOSITORY_PROVIDER=autonomous
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
MEMORY_SUMMARY_USE_LLM=true
MONGODB_DATABASE=agent_platform
MONGODB_URI='mongodb://user:password@autonomous-private-endpoint:27017/?tls=true'
OCIR_REPOSITORY_PREFIX=agent-framework-loadtest
OCI_AUTH_MODE=oke_workload_identity
OCI_CLI_PROFILE=LATINOAMERICA-SaoPaulo
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
OCI_CONFIG_FILE='~/.oci/config'
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_PROJECT_OCID=''
OCI_PROFILE=LATINOAMERICA-Chicago
OCI_REGION=sa-saopaulo-1
OCI_REGION_KEY=gru
OCIR_USERNAME=idi1o0a010nx/oracleidentitycloudservice/cristiano.hoshikawa@oracle.com
OCIR_AUTH_TOKEN="A5XBm>8gvA2oDy;it1lq"
OCIR_EMAIL=cristiano.hoshikawa@oracle.com
OCIR_PULL_SECRET_NAME=ocir-secret
OCI_STREAM_ENDPOINT=''
OCI_STREAM_OCID=''
OCI_STREAM_PARTITION_KEY=agent-events
OCI_TENANCY_NAMESPACE=idi1o0a010nx
OKE_CLUSTER_OCID=ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaabpismmmj6kwo4idomzso5fr32xlnbjcm74pfa7bfhc52plqe64oq
EXISTING_LB_OCID=ocid1.loadbalancer.oc1.sa-saopaulo-1.aaaaaaaaklbmmriyabvxradqkchy5cobelzxj37rn27p2rsfef5uez54jfqa
BACKEND_SET_NAME=agent-framework-loadtest
BACKEND_LISTENER_NAME=agent-framework-loadtest
BACKEND_LISTENER_PORT=8000
BACKEND_LISTENER_PROTOCOL=HTTP
BACKEND_HEALTH_PATH=/health
OTEL_EXPORTER_OTLP_ENDPOINT=''
OTEL_SERVICE_NAME=ai-agent-template
OUTPUT_SUPERVISOR_MAX_RETRIES=3
PROJECT_ID=project-7f829b68-bf43-442c-ad3
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
PUBSUB_SEQUENCE_ENABLED=true
PUBSUB_SEQUENCE_KEY_PREFIX=observer:sequence
PUBSUB_SEQUENCE_MEMORY_FALLBACK=false
PUBSUB_SEQUENCE_MONGODB_COLLECTION=observer_event_counters
PUBSUB_SEQUENCE_MONGODB_DATABASE=agent_platform
PUBSUB_SEQUENCE_MONGODB_URI='mongodb://user:password@autonomous-private-endpoint:27017/?tls=true'
PUBSUB_SEQUENCE_PROVIDER=mongodb
PUBSUB_SEQUENCE_TTL_SECONDS=86400
PUBSUB_TOPIC_PATH=projects/project-7f829b68-bf43-442c-ad3/topics/teste_pubsub
RAG_FILE_GLOBS='*.md,*.txt,*.yaml,*.yml,*.json'
RAG_TOP_K=5
REDIS_URL=redis://localhost:6379/0
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
ROUTE_STICKINESS_HISTORY_TURNS=2
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
ROUTE_STICKINESS_MAX_TOKENS=80
ROUTING_CONFIG_PATH=./config/routing.yaml
ROUTING_MODE=router
SESSION_REPOSITORY_PROVIDER=autonomous
SIMULATE_AGA_MISSING_TRANSACTION_ID=true
TOOLS_CONFIG_PATH=./config/tools.yaml
USAGE_REPOSITORY_PROVIDER=autonomous
VECTOR_STORE_PROVIDER=autonomous

View File

@@ -1,248 +0,0 @@
# Agentes do framework com A2A assíncrono
## O que foi implementado
O agente criado a partir de `templates/agent_template_backend` pode expor o
protocolo nativo e A2A no mesmo processo. O Agent Gateway também oferece a
mesma fachada A2A. As duas fachadas usam o SDK oficial e suas operações de
Tasks/SSE, com interoperabilidade A2A 1.0 e 0.3.
```text
Consumidor A2A ── Gateway A2A (Task)
├── agente nativo (/gateway/message)
└── agente framework A2A (Task remota)
Consumidor A2A ── agente framework A2A diretamente
```
O envio imediato devolve uma Task antes de terminar a operação de negócio.
A execução continua após a resposta HTTP e também após a desconexão do SSE.
O consumidor pode consultar o resultado ou se reconectar à tarefa em andamento.
Os endpoints nativos conservam seu comportamento; `/gateway/message` continua
aguardando o resultado, e o SSE nativo permanece separado do SSE A2A.
## Habilitar no agente construído com o framework
Na raiz do framework, instale a dependência opcional:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' -r templates/agent_template_backend/requirements.txt
```
No ambiente do backend:
```dotenv
ENABLE_A2A=true
A2A_TASK_MODE=true
A2A_PUBLIC_URL=http://localhost:8000
A2A_TASK_TIMEOUT_SECONDS=240
```
Inicie o backend com suas configurações normais. Por exemplo, após preparar o
`.env` e os arquivos de configuração do agente:
```sh
cd templates/agent_template_backend
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
O endereço público real deve ser usado em `A2A_PUBLIC_URL`. A configuração de
exemplo está em `.env.a2a.example`. Há também uma imagem opcional, construída
na raiz do framework:
```sh
docker build -f templates/agent_template_backend/Dockerfile.a2a -t framework-agent-a2a .
```
Os perfis de `agents.yaml` são publicados como skills no Agent Card:
`GET /.well-known/agent-card.json`. Use `message.metadata.agent_id` para escolher
um perfil; sem ele, o backend mantém sua seleção padrão.
## Registrar esse agente no Agent Gateway
```yaml
backends:
pedidos_a2a:
protocol: a2a
a2a_version: '1.0'
url: http://backend-pedidos:8000
a2a_rpc_path: /a2a
description: Consulta e acompanhamento de pedidos
keywords: [pedido, entrega, rastreamento]
default_agent_id: pedidos
```
No gateway, habilite `ENABLE_A2A=true`, `A2A_TASK_MODE=true` e configure seu próprio
`A2A_PUBLIC_URL`. Para uma execução mais longa, ajuste os limites, por exemplo:
`A2A_TASK_TIMEOUT_SECONDS=300` e `BACKEND_TIMEOUT_SECONDS=270`, com o timeout do
backend em 240 segundos. A duração efetiva é limitada pelo menor prazo aplicável.
O gateway devolve sua própria Task imediatamente e consulta a Task remota até
conclusão ou solicitação de input/autenticação. Eventos de status remoto são
publicados como `a2a.backend.status`. Os IDs local e remoto podem ser distintos.
O resultado nativo inclui `metadata.a2a_task_id` e `metadata.a2a_context_id` para
correlação; o consumidor externo continua acompanhando o ID emitido pelo gateway.
## Envio assíncrono A2A 1.0
Envie um `POST /a2a` com `Content-Type: application/json` e
`A2A-Version: 1.0`:
```json
{
"jsonrpc": "2.0",
"id": "request-1",
"method": "SendMessage",
"params": {
"configuration": {"returnImmediately": true},
"message": {
"messageId": "message-1",
"contextId": "conversation-123",
"role": "ROLE_USER",
"parts": [{"text": "Consulte meu pedido"}],
"metadata": {"agent_id": "pedidos", "tenant_id": "default"}
}
}
}
```
A resposta contém `result.task.id`, `contextId` e um estado inicial como
`TASK_STATE_SUBMITTED` ou `TASK_STATE_WORKING`. Guarde esse ID e consulte:
```json
{
"jsonrpc": "2.0",
"id": "request-2",
"method": "GetTask",
"params": {"id": "ID_DA_TAREFA"}
}
```
O resultado está em `result`. Quando `status.state` for `TASK_STATE_COMPLETED`,
o conteúdo final estará em `artifacts[].parts`. Há uma parte de texto e uma
parte `data` com a resposta original do framework.
Sem `returnImmediately: true`, o envio aguarda o estado final ou interrompido,
retornando uma Task no mesmo formato. Para manter o comportamento da primeira
entrega A2A, que retornava Message, configure `A2A_TASK_MODE=false`; nesse modo,
streaming fica desabilitado e não há Tasks locais.
## Streaming e reconexão
Para enviar e acompanhar, use `SendStreamingMessage` no lugar de `SendMessage`
com os mesmos parâmetros. A resposta tem `Content-Type: text/event-stream`.
Cada campo `data:` contém uma resposta JSON-RPC. Os eventos contêm:
1. A Task inicial.
2. Atualizações de status: submitted/working e eventos de progresso.
3. O artefato de resposta.
4. O estado completed, failed, canceled, input-required ou auth-required.
O backend do framework publica marcos como `flow.start`, `session.upserted`,
`workflow.started`, `workflow.completed` e `message.responded`, quando o fluxo
passa por essas etapas. Eles aparecem em
`result.statusUpdate.metadata.framework_event`, com os dados correspondentes
em `framework_payload`. Isso não implica transmissão de tokens do LLM.
Para se reconectar a uma Task em andamento:
```json
{
"jsonrpc": "2.0",
"id": "request-3",
"method": "SubscribeToTask",
"params": {"id": "ID_DA_TAREFA"}
}
```
A reconexão entrega o estado atual e os eventos seguintes; não há promessa de
replay de todos os eventos perdidos. Se a tarefa já terminou, consulte `GetTask`.
## Cancelamento, falhas e continuidade
`CancelTask`, com `params.id`, cancela a execução local em andamento. O SDK
interrompe a coroutine e registra `TASK_STATE_CANCELED`. Isso não desfaz efeitos
já confirmados em ferramentas ou sistemas externos. Quando o gateway está
consultando um agente remoto, parar a tarefa local não garante cancelar a
operação que já foi aceita pelo serviço remoto.
Exceções e expiração de `A2A_TASK_TIMEOUT_SECONDS` resultam em
`TASK_STATE_FAILED`, consultável depois, com mensagem genérica. Detalhes internos
permanecem nos logs do servidor.
Para uma interação que exige nova entrada, um handler pode retornar
`metadata.a2a_task_state: "input-required"` ou `"auth-required"`. O consumidor
envia outra mensagem com o mesmo `taskId` e `contextId`. A tarefa é retomada;
uma tarefa concluída não pode ser reexecutada. O adaptador de saída preserva a
continuidade de tarefas remotas nesses estados.
Os workflows nativos que não declaram esse estado concluem uma Task por turno;
a conversa continua com uma nova Task e o mesmo `contextId`, usando as sessões
e memórias já existentes no framework.
## Equivalentes A2A 0.3
Use `A2A-Version: 0.3`, `role: "user"` e os discriminadores `kind` de 0.3.
| A2A 1.0 | A2A 0.3 |
| --- | --- |
| `SendMessage` + `returnImmediately: true` | `message/send` + `blocking: false` |
| `SendStreamingMessage` | `message/stream` |
| `GetTask` | `tasks/get` |
| `SubscribeToTask` | `tasks/resubscribe` |
| `CancelTask` | `tasks/cancel` |
Em 0.3, `result` contém diretamente a Task, sem o envelope `task`. Os estados
usam `submitted`, `working`, `completed`, `failed`, etc.
## Armazenamento e autenticação
O padrão usa o TaskStore em memória do SDK. Tarefas e resultados são perdidos
quando o processo reinicia. Para múltiplos workers/réplicas, use afinidade de
requisições ao mesmo processo durante a execução ou configure armazenamento e
coordenação compartilhados. Um TaskStore persistente, sozinho, não torna a
execução retomável após falha. As funções `install_a2a` e `install_backend_a2a`
aceitam `task_store` para integração com um armazenamento do SDK.
O backend opcional reutiliza `AGENT_AUTH_*`; o gateway usa
`AGENT_GATEWAY_AUTH_*`. A identidade autenticada pelo middleware define o dono
das Tasks, incluindo consultas e cancelamento. Usuários anônimos compartilham
o escopo anônimo. `tenant_id` em metadata continua sendo dado do pedido, não
credencial nem isolamento de autorização. Clientes que compartilham uma mesma
credencial/principal compartilham o escopo de tarefas.
`A2A_SECURITY_SCHEMES` e `A2A_SECURITY` documentam os mecanismos no Agent Card,
como descrito no guia geral. SSE está habilitado no modo de Tasks;
notificações push e gRPC continuam fora desta implementação.
## Validação
A suíte inclui envio imediato enquanto o handler permanece bloqueado, polling,
streaming com progresso e resultado, cancelamento, falhas, timeout, retomada,
separação entre identidades autenticadas e gateway chamando um backend A2A real
em aplicação local. Há também testes HTTP em `127.0.0.1` que verificam a chegada
do primeiro evento antes da conclusão e reconexão após desconexão.
```sh
python -m pip install pytest pytest-asyncio uvicorn
PYTHONPATH=libs/agent_framework/src pytest \
tests/test_a2a_interoperability.py tests/test_a2a_async_tasks.py \
tests/test_a2a_http_streaming.py \
tests/unit/test_sse.py tests/unit/test_sse_replay_dedup.py \
tests/unit/test_authentication.py tests/unit/test_authentication_policies.py -q
```
Os testes não acessam OCI nem serviços de negócio externos. Em ambientes sem
permissão para abrir portas locais, execute o teste HTTP em um ambiente que
permita bind em loopback.
Especificação oficial: https://a2a-protocol.org/latest/specification/
SDK oficial: https://github.com/a2aproject/a2a-python
## Frontend pronto para A2A
A interface `apps/agent_frontend_a2a` consome este protocolo diretamente, com
Tasks, streaming, consulta e cancelamento. Consulte
[Frontend A2A](../apps/agent_frontend_a2a/README.md) para a demonstração sem OCI
e os comandos de execução com o backend ou Gateway. O frontend nativo continua
em `apps/agent_frontend`.

View File

@@ -1,180 +0,0 @@
# Framework Agents with Asynchronous A2A
## What was implemented
An agent created from `templates/agent_template_backend` can expose both the native protocol and A2A in the same process. The Agent Gateway also provides the same A2A interface. Both interfaces use the official SDK and its Tasks/SSE operations, with A2A 1.0 and 0.3 interoperability.
```text
A2A consumer ── A2A Gateway (Task)
├── native agent (/gateway/message)
└── framework A2A agent (remote Task)
A2A consumer ── framework A2A agent directly
```
Immediate submission returns a Task before the business operation finishes. Execution continues after the HTTP response and even after the SSE connection is closed. The consumer can retrieve the result or reconnect to an in-progress task. Native endpoints retain their behavior: `/gateway/message` still waits for the result, and native SSE remains separate from A2A SSE.
## Enable A2A in an agent built with the framework
From the framework root, install the optional dependency:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' -r templates/agent_template_backend/requirements.txt
```
In the backend environment:
```dotenv
ENABLE_A2A=true
A2A_TASK_MODE=true
A2A_PUBLIC_URL=http://localhost:8000
A2A_TASK_TIMEOUT_SECONDS=240
```
Start the backend with its usual settings. For example, after preparing the `.env` and agent configuration files:
```sh
cd templates/agent_template_backend
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
Use the actual public address in `A2A_PUBLIC_URL`. A sample configuration is provided in `.env.a2a.example`. An optional image can also be built from the framework root:
```sh
docker build -f templates/agent_template_backend/Dockerfile.a2a -t framework-agent-a2a .
```
Profiles defined in `agents.yaml` are published as skills in the Agent Card: `GET /.well-known/agent-card.json`. Use `message.metadata.agent_id` to select a profile; otherwise, the backend retains its default selection.
## Register the agent with the Agent Gateway
```yaml
backends:
pedidos_a2a:
protocol: a2a
a2a_version: '1.0'
url: http://backend-pedidos:8000
a2a_rpc_path: /a2a
description: Order inquiry and tracking
keywords: [pedido, entrega, rastreamento]
default_agent_id: pedidos
```
On the gateway, enable `ENABLE_A2A=true` and `A2A_TASK_MODE=true`, and configure its own `A2A_PUBLIC_URL`. For longer-running operations, adjust the limits; for example, use `A2A_TASK_TIMEOUT_SECONDS=300` and `BACKEND_TIMEOUT_SECONDS=270`, with the backend timeout set to 240 seconds. The effective duration is limited by the shortest applicable deadline.
The gateway immediately returns its own Task and polls the remote Task until completion or until input/authentication is required. Remote status events are published as `a2a.backend.status`. Local and remote IDs may differ. The native result includes `metadata.a2a_task_id` and `metadata.a2a_context_id` for correlation; external consumers continue tracking the ID issued by the gateway.
## Asynchronous A2A 1.0 submission
Send a `POST /a2a` request with `Content-Type: application/json` and `A2A-Version: 1.0`:
```json
{
"jsonrpc": "2.0",
"id": "request-1",
"method": "SendMessage",
"params": {
"configuration": {"returnImmediately": true},
"message": {
"messageId": "message-1",
"contextId": "conversation-123",
"role": "ROLE_USER",
"parts": [{"text": "Check my order"}],
"metadata": {"agent_id": "pedidos", "tenant_id": "default"}
}
}
}
```
The response contains `result.task.id`, `contextId`, and an initial state such as `TASK_STATE_SUBMITTED` or `TASK_STATE_WORKING`. Save that ID and query the task:
```json
{
"jsonrpc": "2.0",
"id": "request-2",
"method": "GetTask",
"params": {"id": "TASK_ID"}
}
```
The result is in `result`. When `status.state` is `TASK_STATE_COMPLETED`, the final content is in `artifacts[].parts`. It includes a text part and a `data` part containing the original framework response.
Without `returnImmediately: true`, submission waits for a final or interrupted state and returns a Task in the same format. To preserve the behavior of the initial A2A release, which returned a Message, set `A2A_TASK_MODE=false`. In that mode, streaming is disabled and there are no local Tasks.
## Streaming and reconnection
To submit and follow a task, use `SendStreamingMessage` instead of `SendMessage`, with the same parameters. The response uses `Content-Type: text/event-stream`. Each `data:` field contains a JSON-RPC response. Events include:
1. The initial Task.
2. Status updates (submitted/working) and progress events.
3. The response artifact.
4. A completed, failed, canceled, input-required, or auth-required state.
The framework backend publishes milestones such as `flow.start`, `session.upserted`, `workflow.started`, `workflow.completed`, and `message.responded` when the flow reaches those stages. They appear in `result.statusUpdate.metadata.framework_event`, with corresponding data in `framework_payload`. This does not imply LLM token streaming.
To reconnect to an in-progress Task:
```json
{
"jsonrpc": "2.0",
"id": "request-3",
"method": "SubscribeToTask",
"params": {"id": "TASK_ID"}
}
```
Reconnection delivers the current state and subsequent events; replay of all missed events is not guaranteed. If the task has already finished, use `GetTask`.
## Cancellation, failures, and continuity
`CancelTask`, with `params.id`, cancels the ongoing local execution. The SDK interrupts the coroutine and records `TASK_STATE_CANCELED`. This does not undo effects already committed in tools or external systems. When the gateway is polling a remote agent, stopping the local task does not guarantee cancellation of an operation already accepted by the remote service.
Exceptions and expiration of `A2A_TASK_TIMEOUT_SECONDS` result in `TASK_STATE_FAILED`, which can be queried later and carries a generic message. Internal details remain in server logs.
For an interaction requiring further input, a handler can return `metadata.a2a_task_state: "input-required"` or `"auth-required"`. The consumer sends another message with the same `taskId` and `contextId`. The task is resumed; a completed task cannot be re-executed. The output adapter preserves continuity for remote tasks in these states.
Native workflows that do not declare such a state complete one Task per turn. The conversation continues with a new Task and the same `contextId`, using the framework's existing sessions and memories.
## A2A 0.3 equivalents
Use `A2A-Version: 0.3`, `role: "user"`, and the 0.3 `kind` discriminators.
| A2A 1.0 | A2A 0.3 |
| --- | --- |
| `SendMessage` + `returnImmediately: true` | `message/send` + `blocking: false` |
| `SendStreamingMessage` | `message/stream` |
| `GetTask` | `tasks/get` |
| `SubscribeToTask` | `tasks/resubscribe` |
| `CancelTask` | `tasks/cancel` |
In 0.3, `result` directly contains the Task without the `task` envelope. States use `submitted`, `working`, `completed`, `failed`, etc.
## Storage and authentication
By default, the SDK uses an in-memory TaskStore. Tasks and results are lost when the process restarts. With multiple workers/replicas, use request affinity to the same process during execution or configure shared storage and coordination. A persistent TaskStore alone does not make execution resumable after a failure. The `install_a2a` and `install_backend_a2a` functions accept `task_store` for integration with SDK storage.
The optional backend reuses `AGENT_AUTH_*`; the gateway uses `AGENT_GATEWAY_AUTH_*`. The identity authenticated by middleware determines Task ownership, including retrieval and cancellation. Anonymous users share the anonymous scope. `tenant_id` in metadata remains request data, not a credential or authorization isolation mechanism. Clients sharing the same credential/principal also share a task scope.
`A2A_SECURITY_SCHEMES` and `A2A_SECURITY` document mechanisms in the Agent Card, as described in the general guide. SSE is enabled in Task mode; push notifications and gRPC are outside the scope of this implementation.
## Validation
The test suite covers immediate submission while the handler remains blocked, polling, streaming with progress and results, cancellation, failures, timeouts, resumption, isolation between authenticated identities, and a gateway calling a real A2A backend in a local application. HTTP tests on `127.0.0.1` also verify that the first event arrives before completion and that reconnection works after disconnection.
```sh
python -m pip install pytest pytest-asyncio uvicorn
PYTHONPATH=libs/agent_framework/src pytest \
tests/test_a2a_interoperability.py tests/test_a2a_async_tasks.py \
tests/test_a2a_http_streaming.py \
tests/unit/test_sse.py tests/unit/test_sse_replay_dedup.py \
tests/unit/test_authentication.py tests/unit/test_authentication_policies.py -q
```
The tests do not access OCI or external business services. In environments where binding to local ports is not permitted, run the HTTP test in an environment that allows binding to loopback.
Official specification: https://a2a-protocol.org/latest/specification/
Official SDK: https://github.com/a2aproject/a2a-python
## A2A-ready frontend
The `apps/agent_frontend_a2a` interface consumes this protocol directly, supporting Tasks, streaming, retrieval, and cancellation. See [A2A Frontend](../apps/agent_frontend_a2a/README.md) for the demonstration without OCI and startup commands using the backend or Gateway. The native frontend remains in `apps/agent_frontend`.

View File

@@ -1,145 +0,0 @@
# Integração A2A no Agent Framework OCI
## Decisão de arquitetura
O Agent Gateway existente passa a ter uma fachada A2A opcional. Não é necessário
manter um segundo roteador, duplicar o supervisor ou migrar os agentes atuais.
```text
Consumidor atual ── /gateway/message ─┐
├─ Agent Gateway / supervisor ─┬─ backend native
Consumidor A2A ──── /a2a ─────────────┘ └─ backend a2a
```
O backend nativo continua recebendo `{channel, payload, agent_id, tenant_id}`.
A ausência de `protocol` no catálogo mantém `native`. A2A fica desativado por
padrão; o SDK só é importado ao habilitar a fachada ou selecionar um backend A2A.
Não foi alterado o runtime dos agentes, os workflows, os canais ou os MCPs.
A entrada usa A2A **1.0 / JSON-RPC**, SDK oficial `a2a-sdk==1.2.2`, com adaptação
0.3 disponibilizada pelo próprio SDK. Agentes externos podem ser registrados
com `a2a_version: '1.0'` ou `'0.3'`; não há detecção heurística de versão.
## Ativação local
Na raiz do framework:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' -r apps/agent_gateway/requirements.txt
export ENABLE_A2A=true
export A2A_PUBLIC_URL=https://meu-gateway.example
export BACKENDS_CONFIG_PATH="$PWD/apps/agent_gateway/config/backends.a2a.example.yaml"
python -m uvicorn app.main:app --app-dir apps/agent_gateway --env-file .env --port 8010
```
Edite primeiro as URLs do catálogo de exemplo. Para agentes externos, informe o
endpoint JSON-RPC anunciado no Agent Card como `url` + `a2a_rpc_path`.
Credenciais ficam em variáveis de ambiente referenciadas por `a2a_headers_env`.
Os valores não são publicados em `/backends`.
A imagem opcional pode ser construída a partir da raiz:
```sh
docker build -f apps/agent_gateway/Dockerfile.a2a -t agent-gateway-a2a .
```
Defina `A2A_PUBLIC_URL` no ambiente do container e monte o catálogo de backends.
A imagem e os comandos anteriores de implantação continuam disponíveis.
## Descoberta e chamada
O Agent Card fica em `GET /.well-known/agent-card.json`; as skills são derivadas
dos backends habilitados. As mensagens chegam em `POST /a2a`.
Exemplo A2A 1.0, com cabeçalho `A2A-Version: 1.0`:
```json
{
"jsonrpc": "2.0",
"id": "request-1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "message-1",
"contextId": "conversation-123",
"role": "ROLE_USER",
"parts": [
{"text": "Quero consultar minha fatura"},
{"data": {"customer_id": "123"}}
],
"metadata": {"tenant_id": "default"}
}
}
}
```
No modo de tarefas (padrão), a resposta fica em `result.task`. O resultado de
negócio está em `artifacts[].parts`, com texto e uma parte `data` contendo a
resposta do framework. Com `A2A_TASK_MODE=false`, permanece em `result.message`. Reutilize `contextId` nos próximos turnos. No modo 0.3,
use o cabeçalho `A2A-Version: 0.3`, método `message/send`, role `user` e os
campos `kind` da especificação 0.3; o resultado é uma Task diretamente
(ou Message com `A2A_TASK_MODE=false`).
Partes `data` devem conter objetos e são incorporadas ao payload de negócio.
A identidade da sessão e o texto do protocolo prevalecem sobre esses dados.
Arquivos recebidos pela fachada são rejeitados explicitamente.
## Conversas, tarefas e limites
- A fachada cria **Tasks A2A** e permite envio imediato, consulta, cancelamento
e streaming SSE. `A2A_TASK_MODE=false` mantém o modo anterior de Messages.
Consulte [A2A assíncrono](A2A_ASSINCRONO.md) para os exemplos completos.
- O adaptador de saída aceita Message ou Task; consulta `GetTask`/`tasks/get`
enquanto o agente está trabalhando, dentro de um prazo total configurável
por `BACKEND_TIMEOUT_SECONDS`. Não repete automaticamente operações de envio.
- Tarefas que pedem input/autenticação preservam estado e identidade para o
próximo turno. Tarefas concluídas não são reutilizadas.
- A continuidade remota é guardada no processo, por tenant/backend/sessão,
com validade de uma hora e limite de 10 mil entradas. Em múltiplas réplicas,
use afinidade de sessão ou implemente armazenamento compartilhado antes de
depender dessa continuidade. Reiniciar o processo limpa esse cache.
- `metadata.a2a_result` preserva o resultado remoto, incluindo dados/artefatos;
`a2a_context_id`, `a2a_task_id` e `a2a_state` permitem inspeção e continuidade
explícita no payload quando necessário.
- Streaming SSE está disponível no modo de tarefas. Push notifications e gRPC
não são oferecidos. O proxy de eventos nativo não é utilizado para agentes
A2A: use `SendStreamingMessage`/`SubscribeToTask` e seus equivalentes 0.3.
- O template `templates/agent_template_backend` também expõe A2A diretamente
quando `ENABLE_A2A=true`, sem passar pelo gateway global.
## Autenticação
As rotas A2A compartilham o middleware de autenticação já instalado no gateway
(`AGENT_GATEWAY_AUTH_*`). Habilitar A2A não habilita autenticação automaticamente.
O tenant em metadata mantém a semântica atual do framework: é dado do pedido,
não uma autorização concedida pelo protocolo. Políticas de acesso a tenants
continuam sendo responsabilidade da configuração de autenticação/autorização.
Quando houver autenticação, declare-a também no Agent Card, por exemplo:
```sh
export A2A_SECURITY_SCHEMES='{"gatewayBearer":{"httpAuthSecurityScheme":{"scheme":"bearer"}}}'
export A2A_SECURITY='[{"gatewayBearer":[]}]'
```
Essas declarações documentam o mecanismo; quem verifica as credenciais é o
middleware. Se desejar descoberta pública, adicione somente o caminho do Agent
Card à configuração de caminhos públicos; `/a2a` permanece protegido.
## Validação
Testes em `tests/test_a2a_interoperability.py` cobrem os dois formatos de entrada,
o contrato nativo, os dois formatos de saída, polling, erros JSON-RPC,
continuidade de tarefas, separação entre tenants e limite de tempo.
```sh
PYTHONPATH=libs/agent_framework/src pytest tests/test_a2a_interoperability.py -q
```
Validação com transportes HTTP simulados e aplicação FastAPI local; não substitui
um teste de integração com os serviços OCI e agentes externos da implantação.
Referências oficiais:
- https://a2a-protocol.org/latest/specification/
- https://a2a-protocol.org/v0.3.0/specification/
- https://github.com/a2aproject/a2a-python

View File

@@ -1,105 +0,0 @@
# A2A Integration in Agent Framework OCI
## Architecture decision
The existing Agent Gateway gains an optional A2A interface. There is no need to maintain a second router, duplicate the supervisor, or migrate existing agents.
```text
Existing consumer ── /gateway/message ─┐
├─ Agent Gateway / supervisor ─┬─ native backend
A2A consumer ─────── /a2a ─────────────┘ └─ A2A backend
```
The native backend continues receiving `{channel, payload, agent_id, tenant_id}`. Omitting `protocol` from the catalog preserves `native`. A2A is disabled by default; the SDK is imported only when the interface is enabled or an A2A backend is selected. The agent runtime, workflows, channels, and MCPs were not changed.
The inbound interface uses A2A **1.0 / JSON-RPC**, the official `a2a-sdk==1.2.2`, with a 0.3 adapter provided by the SDK itself. External agents can be registered with `a2a_version: '1.0'` or `'0.3'`; there is no heuristic version detection.
## Local setup
From the framework root:
```sh
python -m pip install -e 'libs/agent_framework[a2a]' -r apps/agent_gateway/requirements.txt
export ENABLE_A2A=true
export A2A_PUBLIC_URL=https://meu-gateway.example
export BACKENDS_CONFIG_PATH="$PWD/apps/agent_gateway/config/backends.a2a.example.yaml"
python -m uvicorn app.main:app --app-dir apps/agent_gateway --env-file .env --port 8010
```
First, edit the URLs in the sample catalog. For external agents, specify the JSON-RPC endpoint advertised in the Agent Card as `url` + `a2a_rpc_path`. Credentials are stored in environment variables referenced by `a2a_headers_env`. Their values are not exposed through `/backends`.
An optional image can be built from the framework root:
```sh
docker build -f apps/agent_gateway/Dockerfile.a2a -t agent-gateway-a2a .
```
Set `A2A_PUBLIC_URL` in the container environment and mount the backend catalog. The existing deployment images and commands remain available.
## Discovery and invocation
The Agent Card is available at `GET /.well-known/agent-card.json`; skills are derived from enabled backends. Messages arrive at `POST /a2a`.
A2A 1.0 example, with the `A2A-Version: 1.0` header:
```json
{
"jsonrpc": "2.0",
"id": "request-1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "message-1",
"contextId": "conversation-123",
"role": "ROLE_USER",
"parts": [
{"text": "I want to check my bill"},
{"data": {"customer_id": "123"}}
],
"metadata": {"tenant_id": "default"}
}
}
}
```
In Task mode (the default), the response is in `result.task`. The business result is in `artifacts[].parts`, with text and a `data` part containing the framework response. With `A2A_TASK_MODE=false`, it remains in `result.message`. Reuse `contextId` on subsequent turns. In 0.3 mode, use the `A2A-Version: 0.3` header, the `message/send` method, the `user` role, and the `kind` fields from the 0.3 specification; the result is a Task directly (or a Message with `A2A_TASK_MODE=false`).
`data` parts must contain objects and are incorporated into the business payload. Session identity and protocol text take precedence over those data. Files received through the interface are explicitly rejected.
## Conversations, tasks, and limits
- The interface creates **A2A Tasks** and supports immediate submission, retrieval, cancellation, and SSE streaming. `A2A_TASK_MODE=false` preserves the previous Message mode. See [Asynchronous A2A](A2A_ASSINCRONO.md) for complete examples.
- The output adapter accepts a Message or a Task; it polls `GetTask`/`tasks/get` while the agent is working, within an overall timeout configured by `BACKEND_TIMEOUT_SECONDS`. It does not automatically retry submission operations.
- Tasks requiring input/authentication preserve state and identity for the next turn. Completed tasks are not reused.
- Remote continuity is stored in-process by tenant/backend/session, with a one-hour lifetime and a limit of 10,000 entries. With multiple replicas, use session affinity or implement shared storage before relying on this continuity. Process restarts clear the cache.
- `metadata.a2a_result` preserves the remote result, including data/artifacts; `a2a_context_id`, `a2a_task_id`, and `a2a_state` allow explicit inspection and continuity in the payload when needed.
- SSE streaming is available in Task mode. Push notifications and gRPC are not provided. The native event proxy is not used for A2A agents: use `SendStreamingMessage`/`SubscribeToTask` and their 0.3 equivalents.
- The `templates/agent_template_backend` template also exposes A2A directly when `ENABLE_A2A=true`, without going through the global gateway.
## Authentication
A2A routes share the authentication middleware already installed in the gateway (`AGENT_GATEWAY_AUTH_*`). Enabling A2A does not automatically enable authentication. The tenant in metadata retains the framework's current semantics: it is request data, not an authorization granted by the protocol. Tenant access policies remain the responsibility of the authentication/authorization configuration.
When authentication is enabled, also declare it in the Agent Card, for example:
```sh
export A2A_SECURITY_SCHEMES='{"gatewayBearer":{"httpAuthSecurityScheme":{"scheme":"bearer"}}}'
export A2A_SECURITY='[{"gatewayBearer":[]}]'
```
These declarations document the mechanism; the middleware validates credentials. If public discovery is desired, add only the Agent Card path to the public-path configuration; `/a2a` remains protected.
## Validation
Tests in `tests/test_a2a_interoperability.py` cover both input formats, the native contract, both output formats, polling, JSON-RPC errors, task continuity, tenant isolation, and timeouts.
```sh
PYTHONPATH=libs/agent_framework/src pytest tests/test_a2a_interoperability.py -q
```
Validation uses simulated HTTP transports and a local FastAPI application; it does not replace integration testing with OCI services and external agents in the deployment environment.
Official references:
- https://a2a-protocol.org/latest/specification/
- https://a2a-protocol.org/v0.3.0/specification/
- https://github.com/a2aproject/a2a-python

View File

@@ -1,65 +0,0 @@
# Fix: contextual reentry before terminal workflow resume
## Problem
A paused workflow with a declarative `expected_input.semantic_classifier` could receive a
compound reply such as:
```text
ainda nao. Eu nao contratei esse neymar jr
```
If the classifier returned only `NAO`, the framework immediately resumed the paused workflow.
A backend workflow was then free to enter a terminal branch (for example human handoff) before
the substantive remainder of the same user turn was reinterpreted by normal routing.
This is an orchestration problem in the framework, not a domain rule for billing and not a
frontend problem.
## Corrected precedence
Literal `expected_input` matches keep absolute precedence and are unchanged. For non-literal
semantic matches, when the workflow itself declares an option with
`option_actions.<option>.action: contextual_reentry`, the framework now performs one generic
precedence validation before committing to a normal workflow resume.
The validation asks only whether the current utterance is completely consumed by the selected
resume option or whether it also contains substantive information that still needs interpretation.
It contains no domain vocabulary and no hardcoded `SIM`/`NAO` business meaning.
The effective order is now:
1. deterministic `expected_input` exact match;
2. explicit global human-handoff control;
3. workflow `semantic_classifier`;
4. if the semantic result would resume and the contract declares `contextual_reentry`, validate
whether substantive remainder exists;
5. substantive remainder -> `contextual_reentry` and normal routing;
6. pure reply -> resume paused workflow normally;
7. only after a genuine resume can the backend workflow reach its own terminal/handoff branch.
A workflow can explicitly disable this safeguard with:
```yaml
semantic_classifier:
contextual_reentry_precedence: false
```
The default is enabled whenever a `contextual_reentry` option is declared.
## Observability
When reentry preempts a semantic resume, route metadata now includes:
- `contextual_reentry_preempted_resume: true`
- `initial_classifier_output`
- `contextual_reentry_precedence_raw_output`
- the existing contextual-reentry metadata (`original_input`, `classifier_output`, bounded context)
## Regression tests
Tests cover both directions:
- a compound negative reply whose first classifier result is `NAO` is protected from premature
resume and is rerouted through contextual reentry;
- a pure semantic negative reply remains a normal workflow resume.

View File

@@ -1,58 +0,0 @@
# Correção — `max_tokens` por `llm_profiles.yaml` / `.env`
## Objetivo
Remover `max_tokens` efetivos fixados nos pontos de inferência do framework sem perder compatibilidade com os budgets anteriores.
A precedência usada pelos pontos corrigidos passa a ser:
1. `max_tokens` do profile selecionado em `llm_profiles.yaml`;
2. `max_tokens` do profile `default` do mesmo YAML, quando aplicável ao resolver;
3. `LLM_MAX_TOKENS` configurado no ambiente / `.env`;
4. valor legado do ponto de inferência, usado somente como fallback de compatibilidade.
O parâmetro interno `fallback_max_tokens` **não é uma nova configuração do usuário**. Ele apenas preserva o valor histórico quando não existe configuração externa.
## Pontos corrigidos
- Enterprise Router:
- transaction parameter relevance: fallback legado `256`;
- transaction intent shift: fallback legado `512`;
- multi-intent: fallback legado `768`;
- routing normal: fallback legado `512`.
- MCP parameter extraction: fallback `80`.
- Processing interruption classifier: fallback `8`.
- LLM guardrail: fallback `600`.
- RAG query rewrite: fallback `300`.
- RAG context compression: mantém a fórmula histórica `max(512, max_chars // 3)` apenas como fallback.
- Conversation summary memory: mantém a fórmula histórica `max(256, max_summary_chars // 4)` apenas como fallback.
## Atualização de `llm_profiles.yaml`
Como os quatro pontos do Enterprise Router usam o mesmo profile `router`, o profile foi ajustado para o **maior budget histórico**, conforme solicitado:
```yaml
router:
max_tokens: 768
```
Também foram garantidos os profiles explícitos:
```yaml
processing_interruption_classifier:
max_tokens: 8
mcp_parameter_extraction:
max_tokens: 80
```
Os profiles já existentes foram mantidos:
- `guardrail: 600`
- `rag_rewriter: 300`
- `rag_compressor: 1200`
- `summary_memory: 1200`
## Compatibilidade
Se `llm_profiles.yaml` não estiver disponível e `LLM_MAX_TOKENS` não estiver definido, cada ponto corrigido continua usando seu budget histórico. Portanto a correção não transforma a ausência de configuração em `2048` apenas por causa do default global de `Settings`.

View File

@@ -1,452 +0,0 @@
# MCP Multi-item — Guia do Desenvolvedor
## Objetivo
Use este guia quando uma única intenção do usuário precisa operar sobre **mais de um item do mesmo tipo** — por exemplo, cancelar vários VAS/produtos, devolver ou consultar vários pedidos, alterar vários ativos ou processar várias faturas/linhas/contratos.
O framework já possui suporte genérico para **resolução/canonicalização de múltiplos itens**, **confirmação transacional**, **execução MCP/workflow**, **preservação de evidência por item** e **normalização de `SUCCESS` / `PARTIAL_SUCCESS` / `FAILED`**. O desenvolvedor não deve criar um segundo motor de multi-item dentro do agente.
> **Regra central:** multi-item é uma característica da resolução, execução e resultado da operação. **Não é definido pelo tipo do parâmetro de entrada.** Uma tool pode receber `subject: string`, `id: string`, `items: array` ou outro contrato natural de domínio e ainda assim processar múltiplos itens.
## Regra principal
**Não faça loop de negócio no agente.** O agente seleciona a intenção/tool e usa o runtime. A expansão para múltiplos itens pode ocorrer no pre-validator, na própria tool MCP ou em um workflow de domínio.
Fluxo recomendado:
```text
Usuário cita N itens
-> Router/Agent seleciona a tool
-> Runtime coleta os parâmetros declarados pela tool
-> Entrada pode ser escalar ou coleção
-> (opcional) pre-validation resolve/expande/canonicaliza N itens
-> Runtime pede UMA confirmação coerente para o conjunto efetivo
-> MCP tool/workflow processa 1 ou N itens
-> Tool primária devolve results[] com success/ok por item
-> Framework normaliza SUCCESS / PARTIAL_SUCCESS / FAILED
-> Composição final relata sucessos e falhas individualmente
```
## 1. Declare o contrato natural da operação em `tools.yaml`
O framework **não exige `array/list`** para que uma operação seja multi-item.
Use o contrato que faz sentido para a tool e para o domínio:
- **parâmetro escalar** (`subject`, `id`, `query`, etc.) quando a operação recebe uma expressão que pode representar uma ou várias entidades;
- **`array/list`** quando a interface MCP é naturalmente batch e recebe explicitamente uma coleção.
### Padrão A — parâmetro escalar que pode representar vários itens
Este é o padrão usado pelo agente de Contas para cancelamento de VAS.
```yaml
tools:
cancelar_vas_avulso:
description: Cancela um ou mais serviços VAS.
mcp_server: contas
enabled: true
tool_type: action
args_schema:
subject:
type: string
label: o serviço ou os serviços
description: Serviço ou expressão com um ou mais serviços a cancelar.
```
Entrada possível:
```text
subject = "TIM Fashion, Aya Audiobooks, Neymar Jr"
```
O `subject` continua sendo uma `string`. O suporte multi-item aparece posteriormente quando o validator/tool resolve essa expressão em itens independentes.
### Padrão B — coleção explícita
Também é válido quando a API/tool é naturalmente batch:
```yaml
tools:
cancelar_produtos:
description: Cancela um ou mais produtos do cliente.
mcp_server: contas
enabled: true
tool_type: action
args_schema:
items:
type: array
label: os produtos
description: Lista de produtos que o cliente deseja cancelar.
```
Os dois padrões são suportados. **Não altere uma interface escalar existente para array apenas para “habilitar multi-item”.**
## 2. `tool_policies.yaml` não possui sintaxe especial de multi-item
`tool_policies.yaml` continua definindo tipo de operação, confirmação, parâmetros obrigatórios e pre-validation. Não é necessário criar `multi_item: true`, trocar `subject` por `items` ou introduzir outra configuração.
Exemplo realista do agente de Contas:
```yaml
version: 1
defaults:
operation_type: read_only
require_confirmation: false
tool_policies:
cancelar_vas_avulso:
operation_type: transactional
require_confirmation: true
requires:
- subject
pre_validation:
enabled: true
tool: validar_vas_subject
fail_open: false
```
`requires: [subject]` significa apenas que a operação precisa saber **o que** será processado. Esse `subject` pode representar um item ou vários.
## 3. Pre-validation pode transformar um parâmetro escalar em múltiplos itens
Quando necessário, o pre-validator resolve nomes, canonicaliza entidades e pode materializar uma lista estrutural em `transaction_decision.resolved_arguments`.
Exemplo:
```json
{
"eligible": true,
"subject": "TIM Fashion, Aya Audiobooks, Neymar Jr",
"resolved_subject": "TIM Fashion Mensal, Aya Audiobooks Premium, Neymar Jr",
"resolved_subjects": [
"TIM Fashion Mensal",
"Aya Audiobooks Premium",
"Neymar Jr"
],
"transaction_decision": {
"resolved_arguments": {
"subject": "TIM Fashion Mensal, Aya Audiobooks Premium, Neymar Jr",
"items": [
{"name": "TIM Fashion Mensal"},
{"name": "Aya Audiobooks Premium"},
{"name": "Neymar Jr"}
]
},
"target_tool": "cancelar_vas_avulso",
"confirmation_message": "Você confirma o cancelamento de TIM Fashion Mensal, Aya Audiobooks Premium e Neymar Jr?"
}
}
```
`resolved_arguments.items[]` é uma representação canônica útil para a execução, mas **não obriga** `tools.yaml` a declarar `items` como parâmetro de entrada.
### Validação parcial e `fail_open: false`
`eligible` é global. Com `fail_open: false`, retornar `eligible: false` encerra a transação inteira.
Portanto, quando alguns itens são válidos e outros não:
1. mantenha `eligible: true` se existe pelo menos um item acionável;
2. preserve o estado individual dos itens resolvidos/rejeitados;
3. deixe a tool primária produzir o resultado terminal por item em `results[]`.
Exemplo conceitual:
```json
{
"eligible": true,
"status": "PARTIALLY_ELIGIBLE",
"transaction_decision": {
"resolved_arguments": {
"items": [
{"name": "TIM Fashion Mensal", "eligible": true},
{"name": "Aya Audiobooks Premium", "eligible": false, "reason": "item_nao_encontrado"},
{"name": "Neymar Jr", "eligible": true}
]
}
}
}
```
Se nenhum item puder ser processado, `eligible: false` é apropriado.
## 4. Contrato de entrada do MCP: uma operação lógica, não necessariamente uma coleção explícita
Prefira **uma única operação lógica MCP para o agente**. Ela pode ser implementada de três formas:
1. receber um parâmetro escalar e expandi-lo no validator/workflow;
2. receber uma coleção explícita (`array/list`);
3. receber uma expressão/identificador e descobrir os itens dentro da própria tool/workflow.
O agente não deve fazer fan-out chamando a mesma tool repetidamente por conta própria.
### Exemplo — escalar expandido
```json
{
"tool_name": "cancelar_vas_avulso",
"arguments": {
"subject": "TIM Fashion Mensal, Aya Audiobooks Premium, Neymar Jr"
}
}
```
### Exemplo — batch explícito
```json
{
"tool_name": "cancelar_produtos",
"arguments": {
"items": [
{"name": "A", "id": "1001"},
{"name": "B", "id": "1002"}
]
}
}
```
Se o backend só possui API unitária, o fan-out deve ficar encapsulado no **MCP Server ou workflow de domínio**, preservando uma única operação lógica para o agente.
## 5. Contrato obrigatório de resultado por item
Para o framework reconhecer automaticamente o resultado como multi-item, a **tool primária solicitada** deve expor uma lista `results` com **dois ou mais itens**, e cada item deve possuir `success` ou `ok` booleano.
```json
{
"results": [
{"subject": "TIM Fashion Mensal", "success": true},
{"subject": "Aya Audiobooks Premium", "success": false, "reason": "not_found"},
{"subject": "Neymar Jr", "success": true}
]
}
```
Campos como `id`, `subject`, `name`, `reason`, `error`, `protocol` e outros são de domínio e opcionais. O sinal estrutural do normalizador é `success: true|false` ou `ok: true|false` por item.
### Workflow-backed tool
Se a tool é executada por workflow, o caminho preferencial e autoritativo é:
```text
workflow.output[tool_name].results
```
Uma falha auxiliar posterior não deve apagar itens já concluídos pela tool primária.
## 6. O que o framework produz automaticamente
Com `results[]` válido, o runtime classifica o agregado como:
| Situação | `multi_item_status` |
|---|---|
| todos os itens concluíram | `SUCCESS` |
| parte concluiu e parte falhou | `PARTIAL_SUCCESS` |
| todos falharam | `FAILED` |
Exemplo:
```json
{
"multi_item_status": "PARTIAL_SUCCESS",
"partial_success": true,
"metadata": {
"multi_item": {
"status": "PARTIAL_SUCCESS",
"items_count": 3,
"items_succeeded_count": 2,
"items_failed_count": 1,
"primary_tool": "cancelar_vas_avulso"
}
}
}
```
Se um wrapper/etapa posterior retornar erro depois de existirem resultados terminais válidos, o framework preserva os sucessos individuais e pode registrar o erro como secundário, em vez de converter tudo para falha total.
## 7. O que NÃO fazer
Não crie motor multi-item dentro do agente:
```python
# NÃO
for product in products:
await tool_router.call("cancelar_produto", {"product": product})
```
Não reduza resultado misto a um único booleano global sem evidência por item:
```json
{
"approved": false,
"error": "um item falhou"
}
```
Também não altere uma interface existente apenas para trocar:
```text
subject: string
```
por:
```text
items: array
```
quando o validator/workflow já consegue expandir corretamente o `subject`.
## 8. Exemplo completo — padrão escalar do Contas
### `tools.yaml`
```yaml
tools:
cancelar_vas_avulso:
description: Cancela um ou mais VAS.
mcp_server: contas
enabled: true
tool_type: action
args_schema:
subject:
type: string
```
### `tool_policies.yaml`
```yaml
tool_policies:
cancelar_vas_avulso:
operation_type: transactional
require_confirmation: true
requires:
- subject
pre_validation:
enabled: true
tool: validar_vas_subject
fail_open: false
```
### Validator
```json
{
"eligible": true,
"transaction_decision": {
"resolved_arguments": {
"subject": "TIM Fashion Mensal, Aya Audiobooks Premium, Neymar Jr",
"items": [
{"name": "TIM Fashion Mensal"},
{"name": "Aya Audiobooks Premium"},
{"name": "Neymar Jr"}
]
}
}
}
```
### Resultado MCP/workflow
```json
{
"results": [
{"subject": "TIM Fashion Mensal", "success": true},
{"subject": "Aya Audiobooks Premium", "success": false, "reason": "not_found"},
{"subject": "Neymar Jr", "success": true}
]
}
```
Resultado agregado: `PARTIAL_SUCCESS`.
## 9. Exemplo alternativo — API naturalmente batch
Quando o MCP já define coleção como contrato de entrada, `array/list` continua válido:
```yaml
args_schema:
order_ids:
type: array
```
```json
{
"results": [
{"order_id": "P-100", "success": true},
{"order_id": "P-101", "success": false, "reason": "already_shipped"},
{"order_id": "P-102", "success": true}
]
}
```
O ponto importante é: **array é uma opção de contrato, não o gatilho do mecanismo multi-item.**
## 10. Read-only, conversational e transactional podem ser multi-item
Multi-item é independente do `operation_type`.
É possível ter:
```text
read_only + multi-item
conversational + multi-item
transactional + multi-item
internal + multi-item
```
O que muda é a política de confirmação e execução, não a capacidade de representar vários itens.
## 11. Idempotência e protocolos
Em operações com efeito colateral:
- use chave de idempotência de requisição e, quando necessário, por item;
- devolva protocolo/operation id no próprio item;
- preserve itens já concluídos em retry;
- não dependa apenas de mensagem textual agregada.
A política específica de idempotência do sistema de destino continua pertencendo ao MCP/workflow de domínio.
## 12. Checklist do desenvolvedor
- [ ] O agente seleciona a operação e **não** implementa fan-out/loop MCP de negócio.
- [ ] `tools.yaml` declara o contrato natural da operação, que pode ser escalar ou coleção.
- [ ] Nenhuma chave especial `multi_item: true` foi criada ou é necessária.
- [ ] `tool_policies.yaml` usa os `requires` reais do domínio; não é necessário trocar `subject` por `items`.
- [ ] Se um escalar representar múltiplas entidades, validator/tool/workflow expande e canonicaliza os itens.
- [ ] A confirmação representa o conjunto efetivo resolvido.
- [ ] Com `fail_open: false`, um item inválido não força `eligible: false` global quando outros ainda são acionáveis.
- [ ] A tool primária retorna `results[]` com `success` ou `ok` por item quando há múltiplos resultados.
- [ ] Workflow usa `output[tool_name].results` como evidência terminal preferencial da tool primária.
- [ ] Resultado parcial não é reduzido a um único `approved=false` ou `success=false` global.
- [ ] Testes cobrem todos-sucesso, parcial, todos-falha e falha auxiliar posterior.
- [ ] A apresentação final menciona sucessos e falhas individualmente quando houver `PARTIAL_SUCCESS`.
## 13. Onde o comportamento existe no framework
```text
libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py
- suporte existente a coerção de array/list quando esse for o contrato declarado
libs/agent_framework/src/agent_framework/runtime/agent_runtime.py
- pre-validation e resolved_arguments
- _primary_multi_item_outcomes()
- _normalize_multi_item_tool_result()
- instrução de composição multi-item para o LLM
tests/test_multi_item_result_normalization.py
- regressões de SUCCESS/PARTIAL_SUCCESS e proteção contra falha auxiliar
```
## 14. Resumo de responsabilidade
| Camada | Responsabilidade |
|---|---|
| Agente | selecionar intenção/tool e usar o runtime; não implementar motor multi-item |
| `tools.yaml` | declarar o contrato natural da operação; pode ser escalar ou coleção |
| `tool_policies.yaml` | confirmação, `requires` e pre-validation; sem sintaxe especial de multi-item |
| Pre-validator MCP | identificar, resolver, canonicalizar e, quando necessário, expandir 1 parâmetro em N itens |
| Tool MCP/workflow | executar 1 ou N itens e produzir resultado por item |
| Framework runtime | manter estado/confirmação, interpretar `results[]`, preservar evidência e calcular `SUCCESS` / `PARTIAL_SUCCESS` / `FAILED` |
| LLM/presentation | transformar evidências em resposta sem apagar sucesso/falha individual |

View File

@@ -1,170 +0,0 @@
# MCP Multi-item — Developer Guide
## Purpose
Use this guide when one user intent must operate on **more than one item of the same kind**. The framework already supports item resolution/canonicalization, transactional confirmation, MCP/workflow execution, per-item evidence preservation, and `SUCCESS` / `PARTIAL_SUCCESS` / `FAILED` normalization.
> **Core rule:** multi-item is a property of resolution, execution, and results. **It is not defined by the input parameter type.** A tool may receive `subject: string`, `id: string`, `items: array`, or another natural domain contract and still process multiple items.
## Main rule
**Do not implement business fan-out in the agent.** The agent selects the intent/tool and uses the runtime. Expansion to multiple items may happen in the pre-validator, MCP tool, or domain workflow.
```text
User names N items
-> Router/Agent selects the tool
-> Runtime collects the parameters declared by the tool
-> Input may be scalar or a collection
-> optional pre-validation resolves/expands/canonicalizes N items
-> Runtime requests ONE coherent confirmation for the effective set
-> MCP tool/workflow processes 1 or N items
-> Primary tool returns results[] with success/ok per item
-> Framework normalizes SUCCESS / PARTIAL_SUCCESS / FAILED
-> Final composition reports successes and failures individually
```
## 1. Declare the natural operation contract in `tools.yaml`
The framework **does not require `array/list`** for multi-item support.
Use:
- a scalar (`subject`, `id`, `query`, etc.) when the natural interface accepts an expression that may represent one or many entities;
- `array/list` when the MCP interface is naturally batch-oriented.
### Pattern A — scalar expanded later
```yaml
tools:
cancelar_vas_avulso:
args_schema:
subject:
type: string
```
Possible input:
```text
subject = "TIM Fashion, Aya Audiobooks, Neymar Jr"
```
### Pattern B — explicit collection
```yaml
tools:
cancelar_produtos:
args_schema:
items:
type: array
```
Both patterns are valid. **Do not change an existing scalar interface to array only to enable multi-item.**
## 2. `tool_policies.yaml` has no special multi-item syntax
Example:
```yaml
tool_policies:
cancelar_vas_avulso:
operation_type: transactional
require_confirmation: true
requires:
- subject
pre_validation:
enabled: true
tool: validar_vas_subject
fail_open: false
```
`subject` may represent one or multiple entities.
## 3. Pre-validation may expand a scalar into multiple canonical items
```json
{
"eligible": true,
"resolved_subjects": ["A", "B", "C"],
"transaction_decision": {
"resolved_arguments": {
"subject": "A, B, C",
"items": [
{"name": "A"},
{"name": "B"},
{"name": "C"}
]
}
}
}
```
`resolved_arguments.items[]` is a useful canonical execution representation, but it does **not** require `tools.yaml` to expose `items` as its input parameter.
With `fail_open: false`, do not return global `eligible: false` merely because one item fails while other items remain actionable. Keep `eligible: true`, preserve per-item eligibility, and let the primary tool produce terminal `results[]`.
## 4. MCP input contract: one logical operation, not necessarily an explicit collection
Prefer one logical MCP operation for the agent. It may:
1. receive a scalar and expand it in validator/workflow;
2. receive an explicit collection;
3. receive an expression/identifier and discover items inside the tool/workflow.
If the backend only exposes a unit API, keep fan-out inside the **MCP Server or domain workflow**, not inside the agent.
## 5. Required per-item result contract
Automatic multi-item normalization is driven by the **primary tool result**, not by the input schema. The primary tool must expose `results` with at least two items and each item must contain boolean `success` or `ok`.
```json
{
"results": [
{"subject": "A", "success": true},
{"subject": "B", "success": false, "reason": "not_found"},
{"subject": "C", "success": true}
]
}
```
For workflow-backed tools, the preferred authoritative path is:
```text
workflow.output[tool_name].results
```
## 6. Framework normalization
| Situation | `multi_item_status` |
|---|---|
| all items succeed | `SUCCESS` |
| some succeed and some fail | `PARTIAL_SUCCESS` |
| all fail | `FAILED` |
A later auxiliary error must not erase terminal per-item evidence already produced by the primary tool.
## 7. Do not
Do not create an MCP loop in the agent, reduce mixed results to one global boolean, or redesign a working scalar contract merely to make it an array.
## 8. Checklist
- [ ] Agent selects the operation and does not implement business fan-out.
- [ ] `tools.yaml` declares the natural contract, scalar or collection.
- [ ] No special `multi_item: true` setting is required.
- [ ] `tool_policies.yaml` keeps the real domain `requires`; `subject` does not need to become `items`.
- [ ] Validator/tool/workflow expands and canonicalizes multiple entities when needed.
- [ ] Confirmation represents the effective resolved set.
- [ ] With `fail_open: false`, one invalid item does not force global `eligible: false` if other items remain actionable.
- [ ] Primary tool returns `results[]` with `success`/`ok` per item.
- [ ] Workflow prefers `output[tool_name].results` for terminal evidence.
- [ ] Partial results remain visible and are reported individually.
## 9. Responsibility summary
| Layer | Responsibility |
|---|---|
| Agent | select intent/tool; do not implement a multi-item engine |
| `tools.yaml` | declare the natural input contract; scalar or collection |
| `tool_policies.yaml` | confirmation, `requires`, pre-validation; no special multi-item syntax |
| MCP pre-validator | resolve/canonicalize and optionally expand one parameter into N items |
| MCP tool/workflow | execute 1 or N items and return per-item results |
| Framework runtime | maintain state/confirmation, interpret `results[]`, preserve evidence, calculate aggregate status |
| LLM/presentation | report per-item outcomes without erasing successful/failed evidence |

View File

@@ -82,31 +82,6 @@ projeto_multi_agent_isolado/
### Main components ### Main components
### MCP multi-item: product, order, and resource lists
When one intent must operate on several items, **do not create an MCP loop inside the agent**. The framework supports scalar or `array/list` contracts, pre-validation with `resolved_arguments`, set-level transaction confirmation, and automatic normalization of per-item `results[]` into `SUCCESS`, `PARTIAL_SUCCESS`, or `FAILED`. Multi-item support does not depend on the input parameter type.
The complete contract, `tools.yaml` / `tool_policies.yaml` examples, MCP payloads, per-item output, workflow behavior, partial validation, and anti-patterns are documented in:
- [MCP Multi-item — Developer Guide](../../MCP_MULTI_ITEM_DEVELOPER_GUIDE_en.md)
Minimum structural result from the primary tool:
```json
{
"result": {
"results": [
{"subject": "A", "success": true},
{"subject": "B", "success": false, "reason": "not_found"}
]
}
}
```
Every item must expose boolean `success` or `ok`. For workflow-backed tools, prefer `workflow.output[tool_name].results`. A later auxiliary failure must not erase terminal item outcomes.
### Simplified HTTP contract used by the project ### Simplified HTTP contract used by the project
``` ```

View File

@@ -27,7 +27,6 @@ If something is not working, use **Search by problem** below.
| A tool does not appear or cannot be found | `tools.yaml`, MCP catalog, discovery | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) | | A tool does not appear or cannot be found | `tools.yaml`, MCP catalog, discovery | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| MCP Server does not appear in the catalog | registration, manifest/discovery, MCP Gateway | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) and [Gateways](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) | | MCP Server does not appear in the catalog | registration, manifest/discovery, MCP Gateway | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) and [Gateways](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) |
| Parameters sent to the tool are wrong | schema, mapping, BusinessContext, extractor | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) | | Parameters sent to the tool are wrong | schema, mapping, BusinessContext, extractor | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| I need to run one operation for multiple products/orders/items | scalar or collection contract, pre-validation, single confirmation, per-item outcome | [MCP Multi-item](../../MCP_MULTI_ITEM_DEVELOPER_GUIDE_en.md) and [MCP/Tools](./04_mcp_integration_tools_and_policies.md) |
| A transactional operation executes without confirmation | tool policy, `require_confirmation` | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) | | A transactional operation executes without confirmation | tool policy, `require_confirmation` | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| A name search requires an overly exact match | parameter extraction/mapping and agent logic | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) | | A name search requires an overly exact match | parameter extraction/mapping and agent logic | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| I receive 401 between gateway/backend/MCP | Basic Auth, credentials per hop | [Gateways and Auth](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) | | I receive 401 between gateway/backend/MCP | Basic Auth, credentials per hop | [Gateways and Auth](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) |
@@ -74,9 +73,9 @@ If something is not working, use **Search by problem** below.
### [04 — MCP, Tools, Policies, and Parameter Extraction](docs/developer/en/04_mcp_integration_tools_and_policies.md) ### [04 — MCP, Tools, Policies, and Parameter Extraction](docs/developer/en/04_mcp_integration_tools_and_policies.md)
**What it is:** reference for tools, MCP Servers, mappings, policies, parameter extraction, and multi-item calls. **What it is:** reference for tools, MCP Servers, mappings, policies, and parameter extraction.
**Use when:** tool integration/execution is incorrect, needs to be created, or must process item lists without parallel logic in the agent. **Use when:** tool integration/execution is incorrect or needs to be created.
### [05 — Agent Gateway, MCP Gateway, and Authentication](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) ### [05 — Agent Gateway, MCP Gateway, and Authentication](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md)

View File

@@ -83,31 +83,6 @@ projeto_multi_agent_isolado/
### Componentes principais ### Componentes principais
### MCP multi-item: listas de produtos, pedidos e outros recursos
Quando uma única intenção precisa operar sobre vários itens, **não crie um loop MCP dentro do agente**. O framework suporta contratos escalares ou `array/list`, pre-validation com `resolved_arguments`, confirmação transacional do conjunto e normalização de `results[]` por item em `SUCCESS`, `PARTIAL_SUCCESS` ou `FAILED`. Multi-item não depende do tipo do parâmetro de entrada.
O contrato completo, exemplos de `tools.yaml`, `tool_policies.yaml`, payload MCP, retorno por item, workflows, validação parcial e anti-patterns estão em:
- [MCP Multi-item — Guia do Desenvolvedor](../../MCP_MULTI_ITEM_DEVELOPER_GUIDE.md)
Regra estrutural mínima do retorno da tool primária:
```json
{
"result": {
"results": [
{"subject": "A", "success": true},
{"subject": "B", "success": false, "reason": "not_found"}
]
}
}
```
Cada item deve expor `success` ou `ok` booleano. Para tool baseada em workflow, prefira `workflow.output[tool_name].results`. Uma falha auxiliar posterior não deve apagar resultados terminais já executados.
### Contrato HTTP simplificado usado no projeto ### Contrato HTTP simplificado usado no projeto
``` ```

View File

@@ -28,7 +28,6 @@ Se algo não está funcionando, use **Buscar pelo problema** abaixo.
| Uma tool não aparece ou não é encontrada | `tools.yaml`, catálogo MCP, discovery | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) | | Uma tool não aparece ou não é encontrada | `tools.yaml`, catálogo MCP, discovery | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) |
| MCP Server não aparece no catálogo | registration, manifest/discovery, MCP Gateway | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) e [Gateways](./05_agent_gateway_mcp_gateway_and_auth.md) | | MCP Server não aparece no catálogo | registration, manifest/discovery, MCP Gateway | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) e [Gateways](./05_agent_gateway_mcp_gateway_and_auth.md) |
| Parâmetros enviados à tool estão errados | schema, mapping, BusinessContext, extractor | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) | | Parâmetros enviados à tool estão errados | schema, mapping, BusinessContext, extractor | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) |
| Preciso chamar a mesma operação para vários produtos/pedidos/itens | contrato escalar ou coleção, pre-validation, confirmação única, resultado por item | [MCP Multi-item](../../MCP_MULTI_ITEM_DEVELOPER_GUIDE.md) e [MCP/Tools](./04_mcp_integration_tools_and_policies.md) |
| Uma operação transacional executa sem confirmação | tool policy, `require_confirmation` | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) | | Uma operação transacional executa sem confirmação | tool policy, `require_confirmation` | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) |
| Uma busca por nome exige correspondência exata demais | extração/mapeamento de parâmetros e lógica do agente | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) | | Uma busca por nome exige correspondência exata demais | extração/mapeamento de parâmetros e lógica do agente | [MCP/Tools](./04_mcp_integration_tools_and_policies.md) |
| Recebo 401 entre gateway/backend/MCP | Basic Auth, credenciais por hop | [Gateways e Auth](./05_agent_gateway_mcp_gateway_and_auth.md) | | Recebo 401 entre gateway/backend/MCP | Basic Auth, credenciais por hop | [Gateways e Auth](./05_agent_gateway_mcp_gateway_and_auth.md) |
@@ -75,9 +74,9 @@ Se algo não está funcionando, use **Buscar pelo problema** abaixo.
### [04 — MCP, Tools, Policies e Extração de Parâmetros](./04_mcp_integration_tools_and_policies.md) ### [04 — MCP, Tools, Policies e Extração de Parâmetros](./04_mcp_integration_tools_and_policies.md)
**O que é:** referência de tools, MCP Servers, mappings, policies, parameter extraction e chamadas multi-item. **O que é:** referência de tools, MCP Servers, mappings, policies e parameter extraction.
**Use quando:** integração/execução de tool está incorreta, precisa ser criada ou deve processar listas de itens sem lógica paralela no agente. **Use quando:** integração/execução de tool está incorreta ou precisa ser criada.
### [05 — Agent Gateway, MCP Gateway e Autenticação](./05_agent_gateway_mcp_gateway_and_auth.md) ### [05 — Agent Gateway, MCP Gateway e Autenticação](./05_agent_gateway_mcp_gateway_and_auth.md)

View File

@@ -9,11 +9,3 @@ profiles:
model: mock-judge model: mock-judge
temperature: 0 temperature: 0
max_tokens: 300 max_tokens: 300
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5

View File

@@ -1,43 +0,0 @@
"""Local frontend demo, using the actual framework A2A adapter and SDK.
From the framework root:
pip install -e 'libs/agent_framework[a2a]' uvicorn
uvicorn examples.a2a_frontend_demo:app --host 127.0.0.1 --port 8000
This demo simulates business processing; it never calls OCI or an external tool.
"""
import asyncio
import os
from pathlib import Path
from types import SimpleNamespace
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from agent_framework.a2a.server import install_backend_a2a
from agent_framework.a2a.progress import report_progress
app = FastAPI(title='Agent Framework · A2A Frontend Demo')
async def demo_agent(request):
text = request['payload']['message']
await report_progress('workflow.started', {'agent': request.get('agent_id') or 'demo'})
await asyncio.sleep(1)
if text.strip().lower() == '/erro':
raise RuntimeError('Simulated demo error')
if text.strip().lower() == '/lento':
await asyncio.sleep(20)
await report_progress('workflow.completed', {'agent': 'demo'})
if text.strip().lower() == '/confirmar':
return {'text': 'Deseja continuar com a demonstração? Responda sim para prosseguir.',
'metadata': {'a2a_task_state': 'input-required', 'agent_id': 'demo'}}
return {'text': f'Recebi sua mensagem: “{text}”.\n\nA tarefa A2A foi concluída. Este exemplo usa o adaptador real do framework, com processamento de negócio simulado.',
'session_id': request['session_id'], 'metadata': {'agent_id': 'demo'}}
profiles = SimpleNamespace(list_profiles=lambda: [SimpleNamespace(
agent_id='demo', name='Agente de demonstração', description='Exemplo local de agente A2A assíncrono')])
settings = SimpleNamespace(A2A_PUBLIC_URL=os.getenv('A2A_PUBLIC_URL', 'http://localhost:8000'),
A2A_TASK_MODE=True, A2A_TASK_TIMEOUT_SECONDS=60, A2A_SECURITY_SCHEMES={}, A2A_SECURITY=[], APP_NAME='Agente de demonstração')
install_backend_a2a(app, demo_agent, profiles, settings)
app.mount('/', StaticFiles(directory=Path(__file__).resolve().parents[1] / 'apps' / 'agent_frontend_a2a', html=True), name='frontend')

View File

@@ -58,7 +58,7 @@ async def classify_processing_interruption(
response = await llm.ainvoke( response = await llm.ainvoke(
[{"role": "system", "content": prompt}], [{"role": "system", "content": prompt}],
temperature=0, temperature=0,
fallback_max_tokens=8, max_tokens=8,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -1,201 +0,0 @@
"""Deterministic, framework-owned sanitization for DLEX_OUT.
The DLEX classifier decides whether a candidate response contains information
that must not be exposed. When the finding is recoverable, this module masks
only the sensitive values so the response can be revalidated and preserved.
Rules are intentionally generic and live in the framework, never in domain
agents. Deployments may extend the built-ins without changing agent code via:
* ``GUARDRAIL_DLEX_OUT_EXTRA_MASKS_JSON`` (JSON list of rule objects), or
* the DLEX_OUT rail policy key ``sanitize_patterns`` in guardrails.yaml.
Each extension rule accepts ``name``, ``pattern`` and ``replacement``. Patterns
are regular expressions. Raw matched values are never returned in findings.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import os
import re
from typing import Any, Iterable
@dataclass(frozen=True, slots=True)
class DlexMaskRule:
name: str
pattern: str
replacement: str
flags: int = re.IGNORECASE
# Conservative defaults: values that are broadly unsafe to echo verbatim and
# can be redacted without changing the semantic meaning of an answer.
_BUILTIN_RULES: tuple[DlexMaskRule, ...] = (
DlexMaskRule(
"payment_barcode",
# Brazilian boleto/payment representations normally contain 44, 47 or
# 48 digits and may be formatted with dots/spaces. Keep the match on the
# numeric token only so surrounding prose is preserved.
r"(?<!\d)(?=[0-9.\-\s]{44,90}(?!\d))(?:\d[.\-\s]*){44,48}(?!\d)",
"[CODIGO_DE_PAGAMENTO_OCULTADO]",
flags=0,
),
DlexMaskRule(
"jwt",
r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b",
"[TOKEN_OCULTADO]",
flags=0,
),
DlexMaskRule(
"openai_style_secret",
r"\bsk-[A-Za-z0-9_-]{10,}\b",
"[CREDENCIAL_OCULTADA]",
flags=0,
),
DlexMaskRule(
"labelled_secret",
r"(?i)\b(api[_ -]?key|secret|segredo|token|senha|password|credential|credencial)\b(\s*[:=]\s*)([^\s,;]+)",
r"\1\2[DADO_PROTEGIDO]",
flags=0,
),
DlexMaskRule(
"cpf",
r"\b\d{3}\.\d{3}\.\d{3}-\d{2}\b",
"[CPF_OCULTADO]",
flags=0,
),
DlexMaskRule(
"cnpj",
r"\b\d{2}\.\d{3}\.\d{3}/\d{4}-\d{2}\b",
"[CNPJ_OCULTADO]",
flags=0,
),
DlexMaskRule(
"email",
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
"[EMAIL_OCULTADO]",
),
DlexMaskRule(
"labelled_phone",
r"(?i)\b(telefone|celular|whatsapp|msisdn)\b(\s*[:=]?\s*)(?:\+?55\s*)?(?:\(?\d{2}\)?[\s.-]*)?\d{4,5}[\s.-]*\d{4}\b",
r"\1\2[TELEFONE_OCULTADO]",
flags=0,
),
DlexMaskRule(
"uuid",
r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b",
"[IDENTIFICADOR_OCULTADO]",
flags=0,
),
)
def _parse_rule(raw: Any) -> DlexMaskRule | None:
if not isinstance(raw, dict):
return None
name = str(raw.get("name") or "custom").strip()
pattern = str(raw.get("pattern") or "").strip()
replacement = str(raw.get("replacement") or "[DADO_PROTEGIDO]")
if not pattern:
return None
try:
re.compile(pattern)
except re.error:
return None
return DlexMaskRule(name=name, pattern=pattern, replacement=replacement, flags=0)
def _extra_rules_from_env() -> list[DlexMaskRule]:
raw = os.getenv("GUARDRAIL_DLEX_OUT_EXTRA_MASKS_JSON", "").strip()
if not raw:
return []
try:
data = json.loads(raw)
except Exception:
return []
if isinstance(data, dict):
data = [data]
return [rule for item in (data or []) if (rule := _parse_rule(item)) is not None]
def _extra_rules_from_policy(policy: dict[str, Any] | None) -> list[DlexMaskRule]:
data = dict(policy or {}).get("sanitize_patterns") or []
if isinstance(data, dict):
data = [data]
return [rule for item in data if (rule := _parse_rule(item)) is not None]
def _mask_payment_sequences(text: str) -> tuple[str, int]:
"""Mask 44/47/48-digit payment tokens without consuming surrounding prose.
Regex alone is awkward because formatted boletos have variable punctuation.
We scan numeric/punctuation runs, count digits, and only redact the run when
it has a canonical payment digit count.
"""
token_re = re.compile(r"(?<!\d)\d(?:[.\-\s]*\d){43,47}(?!\d)")
count = 0
def repl(match: re.Match[str]) -> str:
nonlocal count
token = match.group(0)
digits = re.sub(r"\D", "", token)
if len(digits) in {44, 47, 48}:
count += 1
return "[CODIGO_DE_PAGAMENTO_OCULTADO]"
return token
return token_re.sub(repl, text), count
def sanitize_dlex_output(
text: str,
*,
policy: dict[str, Any] | None = None,
exclude_values: Iterable[str] | None = None,
) -> tuple[str, list[dict[str, Any]]]:
"""Return ``(sanitized_text, findings)`` without exposing raw matches.
``exclude_values`` contains explicitly authorized values (for example a
workflow-generated protocol). They are temporarily protected so generic
masking never alters them.
"""
original = text or ""
sanitized = original
findings: list[dict[str, Any]] = []
protected: dict[str, str] = {}
for index, value in enumerate(exclude_values or []):
value = str(value or "").strip()
if not value or value not in sanitized:
continue
marker = f"__DLEX_AUTHORIZED_{index}__"
sanitized = sanitized.replace(value, marker)
protected[marker] = value
# Payment sequences use digit-count validation rather than a plain rule.
sanitized, payment_count = _mask_payment_sequences(sanitized)
if payment_count:
findings.append({"type": "payment_barcode", "action": "redact", "count": payment_count})
rules = [r for r in _BUILTIN_RULES if r.name != "payment_barcode"]
rules.extend(_extra_rules_from_env())
rules.extend(_extra_rules_from_policy(policy))
for rule in rules:
try:
regex = re.compile(rule.pattern, rule.flags)
except re.error:
continue
sanitized, count = regex.subn(rule.replacement, sanitized)
if count:
findings.append({"type": rule.name, "action": "redact", "count": count})
for marker, value in protected.items():
sanitized = sanitized.replace(marker, value)
return sanitized, findings
__all__ = ["DlexMaskRule", "sanitize_dlex_output"]

View File

@@ -47,7 +47,7 @@ class LLMGuardrailRail(Guardrail):
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], ],
temperature=0, temperature=0,
fallback_max_tokens=600, max_tokens=600,
profile_name=self.profile_name, profile_name=self.profile_name,
component_name=self.profile_name, component_name=self.profile_name,
generation_name=f"llm.{self.profile_name}", generation_name=f"llm.{self.profile_name}",

View File

@@ -25,7 +25,6 @@ from .calibrated.output_sanitization import mascarar_pii_output, sanitizar_toxic
from .calibrated.rules.pinj_patterns import _PINJ_PATTERNS, is_obvious_injection from .calibrated.rules.pinj_patterns import _PINJ_PATTERNS, is_obvious_injection
from .calibrated.rules.tox_blocklist import _EXPLICIT_TERMS, _THREAT_PATTERNS, is_obvious_toxic from .calibrated.rules.tox_blocklist import _EXPLICIT_TERMS, _THREAT_PATTERNS, is_obvious_toxic
from .framework_llm_client import classify_with_framework_llm from .framework_llm_client import classify_with_framework_llm
from .dlex_sanitizer import sanitize_dlex_output
from agent_framework.workflows.input_contract import has_meaningful_unmatched_policy, has_semantic_classifier from agent_framework.workflows.input_contract import has_meaningful_unmatched_policy, has_semantic_classifier
@@ -324,28 +323,10 @@ class CoherenceRail(Guardrail):
"data": out, "data": out,
}, },
) )
try:
out = await classify_with_framework_llm( out = await classify_with_framework_llm(
_llm(ctx), "COER", {"text": text or "", "context": ctx}, _llm(ctx), "COER", {"text": text or "", "context": ctx},
profile_name="guardrail", component_name="guardrail.coer", generation_name="guardrail.coer", profile_name="guardrail", component_name="guardrail.coer", generation_name="guardrail.coer",
) )
except Exception as exc:
# COER is a conversational-coherence rail, not a security boundary.
# If its semantic classifier/provider is unavailable, the graph must
# continue and let the normal router/workflow clarification logic own
# the turn instead of surfacing an infrastructure exception.
return RailDecision(
code=self.code,
allowed=True,
reason="Classificador de coerência indisponível; continuidade delegada ao runtime",
sanitized_text=text,
metadata={
"mechanism": "infrastructure_fail_open",
"calibrated": True,
"error_type": type(exc).__name__,
"error": str(exc)[:500],
},
)
return RailDecision( return RailDecision(
code=self.code, allowed=bool(out.get("allowed", True)), code=self.code, allowed=bool(out.get("allowed", True)),
reason=str(out.get("reason") or out.get("label") or "COER avaliado"), reason=str(out.get("reason") or out.get("label") or "COER avaliado"),
@@ -363,9 +344,6 @@ class LoopRail(Guardrail):
"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING", "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING",
"PAUSED", "WAITING_INPUT", "PAUSED", "WAITING_INPUT",
} }
# A queued multi-intent transaction may be promoted immediately after
# the previous one completed. Prefer the newest live MCP evidence over
# a stale top-level COMPLETED status left by checkpoint merge order.
for result in reversed(list(ctx.get("mcp_results") or [])): for result in reversed(list(ctx.get("mcp_results") or [])):
if isinstance(result, dict): if isinstance(result, dict):
result_status = str(result.get("transaction_status") or "").strip().upper() result_status = str(result.get("transaction_status") or "").strip().upper()
@@ -381,11 +359,6 @@ class LoopRail(Guardrail):
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision: async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
ctx = _ctx(context) ctx = _ctx(context)
transaction_status = self._transaction_status(ctx) transaction_status = self._transaction_status(ctx)
# Parameter values and short confirmations may legitimately repeat in
# the same session. VLOOP must not consume input owned by the active
# transaction contract; extraction/confirmation remains responsible
# for deciding whether the value is valid.
if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}: if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
return RailDecision( return RailDecision(
code=self.code, code=self.code,
@@ -400,7 +373,6 @@ class LoopRail(Guardrail):
"calibrated": True, "calibrated": True,
}, },
) )
normalized = _lower(text).strip() normalized = _lower(text).strip()
history = [_lower(h).strip() for h in ctx.get("history_texts", [])[-6:]] history = [_lower(h).strip() for h in ctx.get("history_texts", [])[-6:]]
repeated = history.count(normalized) >= 2 if normalized else False repeated = history.count(normalized) >= 2 if normalized else False
@@ -833,64 +805,11 @@ class DataLeakageOutputRail(Guardrail):
} }
protocol_authorization_verified = True protocol_authorization_verified = True
# DLEX_OUT is permissive for recoverable leakage: when classification
# rejects the candidate, mask only generic sensitive values and re-run
# the same classifier. The agent/domain remains unaware of this detail.
# If sanitization cannot change the candidate, or the sanitized version
# is still unsafe, the original blocking behavior is preserved.
dlex_sanitized_text = None
dlex_findings: list[dict[str, Any]] = []
sanitization_recheck = None
original_dlex_result = dict(out)
if not bool(out.get("allowed", True)):
policy = dict(getattr(self, "_guardrail_policy", {}) or {})
candidate_sanitized, dlex_findings = sanitize_dlex_output(
original_text,
policy=policy,
exclude_values=matched_expected_protocols,
)
if candidate_sanitized != original_text:
sanitized_classifier_text = candidate_sanitized
sanitized_classifier_ctx: dict[str, Any] = ctx
if matched_expected_protocols:
sanitized_classifier_text = _mask_authorized_protocol_values(
sanitized_classifier_text, matched_expected_protocols
)
sanitized_classifier_ctx = _mask_authorized_protocol_values(
ctx, matched_expected_protocols
)
sanitization_recheck = await classify_with_framework_llm(
_llm(ctx),
"DLEX_OUT",
{"text": sanitized_classifier_text, "context": sanitized_classifier_ctx},
profile_name="grl",
component_name="guardrail.dlex_out.sanitization_recheck",
generation_name="guardrail.dlex_out.sanitization_recheck",
)
if bool(sanitization_recheck.get("allowed", True)):
dlex_sanitized_text = candidate_sanitized
out = {
"allowed": True,
"label": "SANITIZED",
"reason": "DLEX_OUT sanitizado e revalidado com segurança",
"original_reason": original_dlex_result.get("reason"),
}
metadata = { metadata = {
"mechanism": "llm_rail", "mechanism": "llm_rail",
"data": out, "data": out,
"calibrated": True, "calibrated": True,
} }
if dlex_findings:
metadata.update(
{
"action": "sanitize" if dlex_sanitized_text is not None else "block",
"sanitized": dlex_sanitized_text is not None,
"findings": dlex_findings,
"original_decision": original_dlex_result,
"sanitization_recheck": sanitization_recheck,
}
)
if matched_expected_protocols: if matched_expected_protocols:
metadata.update( metadata.update(
{ {
@@ -904,7 +823,7 @@ class DataLeakageOutputRail(Guardrail):
code=self.code, code=self.code,
allowed=bool(out.get("allowed", True)), allowed=bool(out.get("allowed", True)),
reason=str(out.get("reason") or out.get("label") or "DLEX_OUT avaliado"), reason=str(out.get("reason") or out.get("label") or "DLEX_OUT avaliado"),
sanitized_text=dlex_sanitized_text, sanitized_text=text,
metadata=metadata, metadata=metadata,
) )

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import copy import copy
import logging import logging
import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -165,66 +164,6 @@ class LLMProfileResolver:
return effective return effective
def resolve_configured_value(
self,
profile_name: str | None,
key: str,
*,
env_var: str | None = None,
fallback: Any = None,
) -> Any:
"""Resolve one configuration value with an explicit compatibility fallback.
Precedence is intentionally aligned with the framework configuration model:
1. specific ``llm_profiles.yaml`` profile;
2. YAML ``default`` profile;
3. explicitly configured general environment variable;
4. the caller-provided legacy fallback.
The last step is important for old inference points that historically had a
local constant. Pydantic's Settings object always has framework defaults, so
checking only ``settings.LLM_MAX_TOKENS`` would make an *unset* environment
indistinguishable from the global default.
"""
selected_name = self.normalize_profile_name(profile_name)
if self.enabled:
specific_profile = self._profiles.get(selected_name) or {}
default_profile = self._profiles.get("default") or {}
if key in specific_profile and specific_profile.get(key) is not None:
return specific_profile.get(key)
if key in default_profile and default_profile.get(key) is not None:
return default_profile.get(key)
if env_var:
raw = os.getenv(env_var)
if raw is not None and str(raw).strip() != "":
raw = str(raw).strip()
try:
if isinstance(fallback, bool):
return raw.lower() in {"1", "true", "yes", "on"}
if isinstance(fallback, int) and not isinstance(fallback, bool):
return int(raw)
if isinstance(fallback, float):
return float(raw)
except (TypeError, ValueError):
logger.warning(
"Invalid %s=%r for profile=%s key=%s; using legacy fallback=%r",
env_var, raw, selected_name, key, fallback,
)
return fallback
return raw
# Also honor a value explicitly supplied when Settings was instantiated
# programmatically, without treating the class default as configuration.
field_name = env_var or ""
fields_set = getattr(self.settings, "model_fields_set", set()) or set()
if field_name and field_name in fields_set:
value = getattr(self.settings, field_name, None)
if value is not None:
return value
return fallback
def normalize_profile_name(self, profile_name: str | None) -> str: def normalize_profile_name(self, profile_name: str | None) -> str:
return _canonical_profile_name(profile_name) return _canonical_profile_name(profile_name)

View File

@@ -370,14 +370,6 @@ class OCICompatibleOpenAIProvider(LLMProvider):
profile_name = kwargs.pop("profile_name", None) profile_name = kwargs.pop("profile_name", None)
component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default" component_name = kwargs.pop("component_name", None) or kwargs.pop("component", None) or profile_name or "default"
generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}" generation_name = kwargs.pop("generation_name", None) or f"llm.{component_name}"
fallback_max_tokens = kwargs.pop("fallback_max_tokens", None)
if fallback_max_tokens is not None:
kwargs["max_tokens"] = self.profile_resolver.resolve_configured_value(
profile_name,
"max_tokens",
env_var="LLM_MAX_TOKENS",
fallback=fallback_max_tokens,
)
generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name) generation_name, generation_mapping_meta = _normalize_generation_name(self.telemetry, generation_name)
effective = self.profile_resolver.resolve(profile_name, **kwargs) effective = self.profile_resolver.resolve(profile_name, **kwargs)
provider = str(effective.get("provider") or self.provider_name) provider = str(effective.get("provider") or self.provider_name)

View File

@@ -167,7 +167,7 @@ class ConversationSummaryMemory:
summary = await self.llm.ainvoke([ summary = await self.llm.ainvoke([
{"role": "system", "content": "Você resume memória conversacional para agentes corporativos."}, {"role": "system", "content": "Você resume memória conversacional para agentes corporativos."},
{"role": "user", "content": prompt}, {"role": "user", "content": prompt},
], fallback_max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory") ], max_tokens=max(256, max_summary_chars // 4), temperature=0.1, profile_name="summary_memory", component_name="summary_memory", generation_name="llm.summary_memory")
summary = (summary or "").strip() summary = (summary or "").strip()
if not summary: if not summary:
return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars) return self._deterministic_summary(previous_summary=previous_summary, messages=messages, max_chars=max_summary_chars)

View File

@@ -84,7 +84,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
fallback_max_tokens=300, max_tokens=300,
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",
@@ -112,7 +112,7 @@ class RagService:
{'role': 'user', 'content': prompt}, {'role': 'user', 'content': prompt},
], ],
temperature=0, temperature=0,
fallback_max_tokens=max(512, max_chars // 3), max_tokens=max(512, max_chars // 3),
profile_name=profile_name, profile_name=profile_name,
component_name=profile_name, component_name=profile_name,
generation_name=f"llm.{profile_name}", generation_name=f"llm.{profile_name}",

View File

@@ -256,79 +256,6 @@ class EnterpriseRouter:
return matched, raw return matched, raw
return None, raw return None, raw
async def _prefer_contextual_reentry_over_semantic_resume(
self,
*,
text: str,
classified: str,
expected_input: dict[str, Any],
pause_prompt: str,
relevant_conversation_context: str,
) -> tuple[bool, str | None, str | None]:
"""Protect contextual reentry from a premature workflow resume.
``semantic_classifier`` is agent-declared and normally authoritative. A
special ambiguity exists when the same contract declares an option whose
action is ``contextual_reentry``: an LLM can correctly notice a leading
answer to the pause (for example a negative acknowledgement) but miss the
substantive remainder of the same utterance. Resuming the workflow in
that situation can immediately reach a terminal branch such as handoff,
before normal routing sees the new information.
The framework therefore performs a generic *precedence validation* only
for non-literal semantic matches and only when the contract explicitly
declares contextual reentry. No domain vocabulary or SIM/NAO semantics
are embedded here.
"""
if self.llm is None or not isinstance(expected_input, dict):
return False, None, None
classifier = expected_input.get("semantic_classifier")
if not isinstance(classifier, dict):
return False, None, None
option_actions = classifier.get("option_actions")
if not isinstance(option_actions, dict):
return False, None, None
reentry_option = None
for option, cfg in option_actions.items():
if isinstance(cfg, dict) and str(cfg.get("action") or "").strip().lower() == "contextual_reentry":
reentry_option = str(option)
break
if not reentry_option or str(classified).upper() == reentry_option.upper():
return False, reentry_option, None
if classifier.get("contextual_reentry_precedence") is False:
return False, reentry_option, None
prompt = (
"Você valida precedência entre retomar um workflow pausado e fazer reentrada contextual. "
"Não decida a intenção de negócio. Analise apenas se a mensagem atual é totalmente "
"consumida pela opção já classificada ou se contém informação substantiva adicional que "
"precisa ser interpretada pelo pipeline normal.\n\n"
f"Pergunta pendente: {str(pause_prompt or '').strip()}\n"
f"Opção inicialmente classificada: {str(classified)}\n"
f"Contexto relevante: {str(relevant_conversation_context or '').strip()}\n"
f"Mensagem atual: {str(text or '').strip()}\n\n"
"Responda somente REENTER se houver pedido, objeção, correção, explicação, motivo, entidade, "
"valor, referência ou outro fato novo que ainda precise de interpretação além da resposta "
"à pergunta pendente. Responda somente RESUME se a mensagem inteira for apenas a resposta "
"à pergunta pendente, admitindo somente cortesia sem nova demanda."
)
try:
answer = await self.llm.ainvoke(
[
{"role": "system", "content": prompt},
{"role": "user", "content": str(text or "")},
],
profile_name="router",
component_name="workflow.expected_input.precedence",
generation_name="workflow.expected_input.contextual_reentry_precedence",
)
except Exception as exc:
logger.warning("Falha ao validar precedência de contextual_reentry: %s", exc)
return False, reentry_option, None
raw = str(answer or "").strip()
return raw.upper() == "REENTER", reentry_option, raw
@staticmethod @staticmethod
def _last_assistant_prompt(state: dict[str, Any], current_text: str) -> str: def _last_assistant_prompt(state: dict[str, Any], current_text: str) -> str:
history = [item for item in (state.get("history") or []) if isinstance(item, dict)] history = [item for item in (state.get("history") or []) if isinstance(item, dict)]
@@ -597,39 +524,6 @@ class EnterpriseRouter:
option_actions = option_actions if isinstance(option_actions, dict) else {} option_actions = option_actions if isinstance(option_actions, dict) else {}
action_cfg = option_actions.get(str(classified)) or option_actions.get(str(classified).upper()) action_cfg = option_actions.get(str(classified)) or option_actions.get(str(classified).upper())
action_cfg = action_cfg if isinstance(action_cfg, dict) else {} action_cfg = action_cfg if isinstance(action_cfg, dict) else {}
# A semantic match that would resume the paused workflow
# must not prematurely win over an explicitly declared
# contextual_reentry option when the same utterance also
# carries substantive new information. This validation is
# intentionally generic and runs only after literal
# expected_input matching has already failed.
if str(action_cfg.get("action") or "").strip().lower() != "contextual_reentry":
prefer_reentry, reentry_option, precedence_raw = await self._prefer_contextual_reentry_over_semantic_resume(
text=str(text),
classified=str(classified),
expected_input=expected_input,
pause_prompt=str(pause.get("prompt") or ""),
relevant_conversation_context=relevant_context,
)
if prefer_reentry and reentry_option:
decision = await self._route_contextual_reentry(
state=state,
original_input=str(text),
relevant_context=relevant_context,
classifier_output=str(reentry_option),
raw_classifier=raw_classifier,
allowed_values=list(expected_input.get("allowed_values") or []),
)
decision.metadata = {
**dict(decision.metadata or {}),
"contextual_reentry_preempted_resume": True,
"initial_classifier_output": str(classified),
"contextual_reentry_precedence_raw_output": precedence_raw,
}
await self._emit(decision, state)
return decision
if str(action_cfg.get("action") or "").strip().lower() == "contextual_reentry": if str(action_cfg.get("action") or "").strip().lower() == "contextual_reentry":
decision = await self._route_contextual_reentry( decision = await self._route_contextual_reentry(
state=state, state=state,
@@ -736,144 +630,32 @@ class EnterpriseRouter:
if state_decision: if state_decision:
tx_status = str(state.get("transaction_status") or "").strip().upper() tx_status = str(state.get("transaction_status") or "").strip().upper()
active_tx = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {} # Confirmation is the only transaction input with absolute precedence:
legacy_tx = state.get("pending_tool_call") or state.get("selected_tool_call") or {} # an explicit yes/no answers the confirmation contract itself.
state_lock_name = str( if tx_status == "AWAITING_CONFIRMATION":
state_decision.next_state
or current_state
or state_decision.intent.removeprefix("state:")
or ""
).strip().upper()
transactional_state_lock = state_lock_name.startswith(
("WAITING_", "COLLECTING_", "AWAITING_")
)
# Rich transactional latches remain the primary source of truth.
# The domain state lock is only a recovery fallback for checkpoints
# where those latches were not restored. This prevents a generic
# COLLECTING_* state from stealing a legitimate parameter answer.
rich_transaction_context = bool(
active_tx.get("tool_name")
or (isinstance(legacy_tx, dict) and legacy_tx.get("tool_name"))
or tx_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}
)
fallback_state_lock_context = bool(
transactional_state_lock and not rich_transaction_context
)
has_transaction_context = bool(
rich_transaction_context or fallback_state_lock_context
)
# Some hosts restore the domain state but omit transaction_status.
# Infer only the generic phase needed by the precedence logic; do not
# replace richer transaction data when it exists.
effective_tx_status = tx_status
if not effective_tx_status:
if state_lock_name.startswith("COLLECTING_"):
effective_tx_status = "COLLECTING_PARAMETERS"
elif state_lock_name.startswith("AWAITING_") or "CONFIRMATION" in state_lock_name:
effective_tx_status = "AWAITING_CONFIRMATION"
precedence_state = state
if effective_tx_status and effective_tx_status != tx_status:
precedence_state = dict(state)
precedence_state["transaction_status"] = effective_tx_status
# Transactional interruption precedence is semantic, not lexical.
#
# COLLECTING_PARAMETERS is special: the pending parameter gets the
# first opportunity to consume the turn. If the input is pertinent
# to the parameter, extraction returns values and the transaction
# continues. Only when the input is not pertinent do we ask the
# semantic interruption classifier whether the user abandoned the
# active transaction. This preserves inputs such as
# ``motivo é que desisti`` as a legitimate ``reason`` value while
# still allowing ``não quero mais devolver; quero ver meus serviços``
# to abandon the current operation and start a new objective.
if effective_tx_status == "COLLECTING_PARAMETERS":
if rich_transaction_context:
relevance = await self._classify_transaction_parameter_relevance(
precedence_state, text=str(text)
)
if relevance is True:
consumed = await self._transaction_parameter_precedence( consumed = await self._transaction_parameter_precedence(
precedence_state, text=str(text), state_decision=state_decision state, text=str(text), state_decision=state_decision
) )
if consumed is not None: if consumed is not None:
consumed.metadata = {
**(consumed.metadata or {}),
"transaction_parameter_relevant": True,
}
await self._emit(consumed, state) await self._emit(consumed, state)
return consumed return consumed
# Pertinent to the requested parameter, but extraction did # Transaction parameter precedence is absolute while collecting:
# not produce a structured value. Keep transaction # first let the active transaction try to consume the current turn.
# ownership and let the agent reprompt/clarify; do not # Only when NO pending parameter can be extracted do we ask the
# reinterpret the same input as ABANDON or SHIFT. # semantic classifier whether the user changed goals. This prevents
state_decision.metadata = { # value/name/reference answers (for example "a de 14,99") from being
**(state_decision.metadata or {}), # stolen by a semantically plausible but incompatible intent.
"transaction_parameter_relevant": True, if tx_status == "COLLECTING_PARAMETERS":
"transaction_parameter_extraction_empty": True,
}
await self._emit(state_decision, state)
return state_decision
if relevance is None:
# Compatibility/fail-safe when the relevance classifier is
# unavailable: retain the previous extractor-first behavior.
consumed = await self._transaction_parameter_precedence( consumed = await self._transaction_parameter_precedence(
precedence_state, text=str(text), state_decision=state_decision state, text=str(text), state_decision=state_decision
)
if consumed is not None:
consumed.metadata = {
**(consumed.metadata or {}),
"transaction_parameter_relevance": "unknown",
}
await self._emit(consumed, state)
return consumed
if has_transaction_context:
abandonment = await self._transaction_state_interruption_candidate(
precedence_state,
text=str(text),
state_decision=state_decision,
abandon_only=True,
)
if abandonment is not None:
abandonment.metadata = {
**(abandonment.metadata or {}),
"transaction_parameter_relevant": False if rich_transaction_context else None,
}
await self._emit(abandonment, state)
return abandonment
# In confirmation/waiting phases there is no pending free-form
# parameter whose value can be confused with abandonment. Evaluate
# ABANDON semantically before confirmation/state-lock consumption.
# Plain confirmations (e.g. yes/no) are classified as CONTINUE by
# this probe and then follow the normal confirmation path below.
elif has_transaction_context:
abandonment = await self._transaction_state_interruption_candidate(
precedence_state,
text=str(text),
state_decision=state_decision,
abandon_only=True,
)
if abandonment is not None:
await self._emit(abandonment, state)
return abandonment
if effective_tx_status == "AWAITING_CONFIRMATION":
consumed = await self._transaction_parameter_precedence(
precedence_state, text=str(text), state_decision=state_decision
) )
if consumed is not None: if consumed is not None:
await self._emit(consumed, state) await self._emit(consumed, state)
return consumed return consumed
interruption = await self._transaction_state_interruption_candidate( interruption = await self._transaction_state_interruption_candidate(
precedence_state, text=str(text), state_decision=state_decision state, text=str(text), state_decision=state_decision
) )
if interruption is not None: if interruption is not None:
await self._emit(interruption, state) await self._emit(interruption, state)
@@ -904,60 +686,6 @@ class EnterpriseRouter:
method="state", method="state",
next_state=tx_status, next_state=tx_status,
) )
if tx_status == "COLLECTING_PARAMETERS":
relevance = await self._classify_transaction_parameter_relevance(
state, text=str(text)
)
if relevance is True:
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,
"transaction_parameter_relevant": True,
}
await self._emit(consumed, state)
return consumed
synthetic.metadata = {
**(synthetic.metadata or {}),
"transaction_state_recovered": True,
"transaction_parameter_relevant": True,
"transaction_parameter_extraction_empty": True,
}
await self._emit(synthetic, state)
return synthetic
if relevance is None:
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,
"transaction_parameter_relevance": "unknown",
}
await self._emit(consumed, state)
return consumed
# No parameter was consumed (or this is confirmation): now decide
# semantically whether the user abandoned the active transaction.
abandonment = await self._transaction_state_interruption_candidate(
state,
text=str(text),
state_decision=synthetic,
abandon_only=True,
)
if abandonment is not None:
abandonment.metadata = {
**(abandonment.metadata or {}),
"transaction_state_recovered": True,
}
await self._emit(abandonment, state)
return abandonment
if tx_status == "AWAITING_CONFIRMATION": if tx_status == "AWAITING_CONFIRMATION":
consumed = await self._transaction_parameter_precedence( consumed = await self._transaction_parameter_precedence(
state, text=str(text), state_decision=synthetic state, text=str(text), state_decision=synthetic
@@ -970,6 +698,18 @@ class EnterpriseRouter:
await self._emit(consumed, state) await self._emit(consumed, state)
return consumed return consumed
if tx_status == "COLLECTING_PARAMETERS":
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( interruption = await self._transaction_state_interruption_candidate(
state, text=str(text), state_decision=synthetic state, text=str(text), state_decision=synthetic
) )
@@ -1097,82 +837,6 @@ class EnterpriseRouter:
return decision return decision
async def _classify_transaction_parameter_relevance(
self, state: dict[str, Any], *, text: str
) -> bool | None:
"""Semantically decide whether the turn answers a pending parameter.
This is intentionally separate from extraction. A parameter extractor
may be able to manufacture a syntactically valid value from unrelated
text; relevance answers the higher-level question first: does the user
appear to be responding to what the active transaction asked for?
Returns True/False when classification succeeds and None when the
classifier is unavailable or its output cannot be trusted.
"""
if not (self.enable_llm_router and self.llm is not None):
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 "").strip()
if not tool_name:
return None
payload = {
"message": str(text or ""),
"transaction": {
"intent": active.get("started_from_intent") or state.get("intent"),
"tool_name": tool_name,
"tool_description": active.get("tool_description"),
"missing_parameters": missing,
"parameter_schema": active.get("parameter_schema") or {},
"known_arguments": active.get("arguments") or {},
"relevant_conversation_context": (
active.get("parameter_conversational_context")
or self._collect_transaction_parameter_context(state=state, current_text=str(text))
),
},
}
system = (
"Você decide somente se a mensagem do usuário é uma resposta pertinente ao(s) "
"parâmetro(s) que a transação ativa está pedindo. Avalie significado e contexto, "
"não palavras isoladas. Considere RELEVANT quando a mensagem fornece, esclarece, "
"corrige ou referencia plausivelmente o dado solicitado, mesmo em linguagem livre. "
"Considere NOT_RELEVANT quando a mensagem abandona a ação, inicia outro objetivo, "
"faz uma pergunta diferente ou não responde ao dado pedido. Não extraia valores e "
"não decida abandono/intent; classifique apenas pertinência. Retorne somente JSON "
"válido: {\"relevance\":\"RELEVANT|NOT_RELEVANT\",\"confidence\":0.0,\"reason\":\"...\"}."
)
try:
answer = await self.llm.ainvoke(
[
{"role": "system", "content": system},
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
],
temperature=0.0,
fallback_max_tokens=256,
profile_name="router",
component_name="router.transaction_parameter_relevance",
generation_name="llm.transaction_parameter_relevance",
)
data = self._parse_json(answer)
except Exception as exc:
logger.warning("Falha ao classificar pertinência do parâmetro transacional: %s", exc)
return None
relevance = str(data.get("relevance") or "").strip().upper()
try:
confidence = float(data.get("confidence") or 0.0)
except (TypeError, ValueError):
return None
if confidence < self.intent_shift_threshold:
return None
if relevance == "RELEVANT":
return True
if relevance == "NOT_RELEVANT":
return False
return None
async def _transaction_parameter_precedence( async def _transaction_parameter_precedence(
self, self,
state: dict[str, Any], state: dict[str, Any],
@@ -1182,12 +846,11 @@ class EnterpriseRouter:
) -> RouteDecision | None: ) -> RouteDecision | None:
"""Try to consume the turn under the active transaction contract first. """Try to consume the turn under the active transaction contract first.
In AWAITING_CONFIRMATION, an explicit ABANDON candidate is classified AWAITING_CONFIRMATION consumes an explicit confirmation before any shift
before confirmation consumption; plain yes/no responses still keep classification. COLLECTING_PARAMETERS also has precedence: if at least one
deterministic confirmation precedence because they do not pass the pending parameter can be extracted, the active transaction keeps ownership
abandonment gate. In COLLECTING_PARAMETERS, ABANDON is likewise checked of the turn. Semantic intent-shift is evaluated only when extraction returns
before extraction, while SHIFT remains after parameter extraction so valid no usable pending parameter.
referential/value answers are not stolen from the active transaction.
""" """
tx_status = str(state.get("transaction_status") or "").strip().upper() tx_status = str(state.get("transaction_status") or "").strip().upper()
if tx_status == "AWAITING_CONFIRMATION": if tx_status == "AWAITING_CONFIRMATION":
@@ -1303,14 +966,13 @@ class EnterpriseRouter:
*, *,
text: str, text: str,
state_decision: RouteDecision, state_decision: RouteDecision,
abandon_only: bool = False,
) -> RouteDecision | None: ) -> RouteDecision | None:
"""Detecta semanticamente mudança de intenção durante uma transação. """Detecta semanticamente mudança de intenção durante uma transação.
Não existe lista de palavras para desistência. ABANDON é sempre uma Não existe lista de palavras para desistência ou mudança de assunto. Uma
decisão semântica do LLM baseada no contexto transacional. Uma possível interrupção nasce de uma intent diferente resolvida por uma keyword
nova intenção pode aparecer como hint do ``routing.yaml``, mas esse hint configurada no ``routing.yaml`` ou, na ausência dela, por uma decisão
não decide abandono e não substitui a classificação semântica. semântica do LLM com o contexto da transação pendente.
""" """
active_tx = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {} active_tx = state.get("active_transaction") if isinstance(state.get("active_transaction"), dict) else {}
started_intent = str(active_tx.get("started_from_intent") or "").strip() started_intent = str(active_tx.get("started_from_intent") or "").strip()
@@ -1324,17 +986,7 @@ class EnterpriseRouter:
or (started_intent and configured_candidate.intent != started_intent) or (started_intent and configured_candidate.intent != started_intent)
or (previous_intent and not previous_intent.startswith("state:") and configured_candidate.intent != previous_intent) or (previous_intent and not previous_intent.startswith("state:") and configured_candidate.intent != previous_intent)
) )
if not different:
# A configured candidate that resolves to the same intent/agent is not
# sufficient to conclude that the active transaction continues. The
# user may be starting a *new instance of the same intent* with a
# different transactional target (for example cancelling TIM Fashion
# while Tamboro is awaiting confirmation). In LLM mode keep the
# candidate only as a semantic hint and let the transaction classifier
# decide CONTINUE/REPLACE/ABANDON. Without semantic classification we
# retain the legacy state lock because guessing replacement from a
# keyword alone would be unsafe.
if not different and not (self.enable_llm_router and self.llm is not None):
return None return None
# During parameter collection, a configured keyword may be present in # During parameter collection, a configured keyword may be present in
@@ -1375,11 +1027,6 @@ class EnterpriseRouter:
"current_intent": started_intent or previous_intent, "current_intent": started_intent or previous_intent,
"transaction_status": state.get("transaction_status"), "transaction_status": state.get("transaction_status"),
"tool_name": active_tx.get("tool_name"), "tool_name": active_tx.get("tool_name"),
# Structured arguments are part of the identity of the active
# transaction. They let the semantic classifier distinguish a new
# instance of the same intent from a continuation without relying on
# domain keywords or hard-coded product names.
"active_arguments": dict(active_tx.get("arguments") or {}),
"missing_parameters": list(state.get("missing_parameters") or []), "missing_parameters": list(state.get("missing_parameters") or []),
"configured_candidate": ( "configured_candidate": (
{ {
@@ -1391,30 +1038,12 @@ class EnterpriseRouter:
else None else None
), ),
} }
if abandon_only:
system = ( system = (
"Você decide somente se o usuário desistiu explicitamente da ação/transação ativa. " "Você decide apenas se o turno atual continua a transação ativa ou muda de intenção. "
"Quando a transação estava coletando parâmetros, a pertinência ao parâmetro já foi avaliada antes desta etapa; " "Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. "
"portanto não transforme uma resposta pertinente ao parâmetro em abandono. "
"Use o significado completo da mensagem e o contexto transacional; não use palavras isoladas como regra. "
"Se houver desistência explícita da ação atual, retorne ABANDON. "
"Quando o abandono vier acompanhado de um novo objetivo, também informe intent e agent desse novo objetivo. "
"Quando for apenas abandono sem novo objetivo, intent e agent podem ser nulos. "
"Se a mensagem apenas fornece parâmetro, confirma contexto, ou somente muda/adiciona outro objetivo sem "
"desistir explicitamente da ação atual, retorne CONTINUE. "
"Retorne somente JSON válido com decision, intent, agent, confidence, reason."
)
else:
system = (
"Você decide apenas se o turno atual continua a transação ativa, substitui a instância transacional ativa, "
"ou muda de intenção. Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. "
"A extração dos parâmetros pendentes já foi tentada antes desta etapa e não consumiu o turno. " "A extração dos parâmetros pendentes já foi tentada antes desta etapa e não consumiu o turno. "
"Se ainda assim a mensagem for apenas uma resposta referencial/valor/nome ao dado pendente, retorne CONTINUE. " "Se ainda assim a mensagem for apenas uma resposta referencial/valor/nome ao dado pendente, retorne CONTINUE. "
"Se o usuário continua com a MESMA intent, mas claramente inicia outra instância da operação com alvo/argumentos diferentes, " "Se o usuário passou claramente a perseguir outro objetivo sem desistir explicitamente da ação atual, "
"retorne REPLACE e informe a mesma intent e agent. REPLACE significa substituir a transação ativa; não significa estado novo. "
"Não use REPLACE quando o usuário apenas complementa/adiciona itens à mesma operação (por exemplo 'também', 'além desses'), "
"nem quando apenas reformula ou confirma o mesmo alvo: nesses casos retorne CONTINUE. "
"Se o usuário passou claramente a perseguir outro objetivo/intent sem desistir explicitamente da ação atual, "
"retorne SHIFT e a nova intent permitida. " "retorne SHIFT e a nova intent permitida. "
"Se o usuário desistiu explicitamente da ação/transação atual, retorne ABANDON. " "Se o usuário desistiu explicitamente da ação/transação atual, retorne ABANDON. "
"Quando ABANDON vier acompanhado de um novo objetivo, também informe intent e agent desse novo objetivo. " "Quando ABANDON vier acompanhado de um novo objetivo, também informe intent e agent desse novo objetivo. "
@@ -1434,7 +1063,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
fallback_max_tokens=512, max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.transaction_intent_shift", generation_name="llm.transaction_intent_shift",
@@ -1445,10 +1074,7 @@ class EnterpriseRouter:
return None return None
decision_kind = str(data.get("decision") or "").strip().upper() decision_kind = str(data.get("decision") or "").strip().upper()
if abandon_only: if decision_kind not in {"SHIFT", "ABANDON"}:
if decision_kind != "ABANDON":
return None
elif decision_kind not in {"SHIFT", "REPLACE", "ABANDON"}:
return None return None
confidence = float(data.get("confidence") or 0.0) confidence = float(data.get("confidence") or 0.0)
if confidence < self.intent_shift_threshold: if confidence < self.intent_shift_threshold:
@@ -1499,42 +1125,7 @@ class EnterpriseRouter:
mcp_tools=mcp_tools, mcp_tools=mcp_tools,
) )
current_intent = started_intent or previous_intent if not intent_name or intent_name == (started_intent or previous_intent):
if not intent_name:
return None
# Same-intent replacement is a transaction lifecycle event, not a new
# conversation state. It cancels only the active transaction instance
# and re-enters normal routing for the same configured intent.
if decision_kind == "REPLACE":
if not current_intent or intent_name != current_intent:
return None
agent = agent or str(self._agent_for_intent(intent_name) or "").strip()
if not agent:
return None
return RouteDecision(
route=agent,
agent=agent,
intent=intent_name,
confidence=confidence,
reason=str(data.get("reason") or "Nova instância transacional da mesma intenção."),
method="llm",
metadata={
"transaction_interruption": "same_intent_replacement",
"interrupted_state": state_decision.next_state,
"interrupted_agent": state_decision.agent,
"interrupted_intent": current_intent,
"interruption_source": "semantic_classifier",
"configured_routing_hint": (
configured_candidate.intent if configured_candidate is not None else None
),
"raw_llm_answer": answer[:1000],
},
domain=self._domain_for_intent(intent_name),
mcp_tools=self._tools_for_intent(intent_name),
)
if intent_name == current_intent:
return None return None
agent = agent or str(self._agent_for_intent(intent_name) or "").strip() agent = agent or str(self._agent_for_intent(intent_name) or "").strip()
if not agent: if not agent:
@@ -1780,7 +1371,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
fallback_max_tokens=768, max_tokens=768,
profile_name="router", profile_name="router",
component_name="router.multi_intent", component_name="router.multi_intent",
generation_name="llm.router.multi_intent", generation_name="llm.router.multi_intent",
@@ -1836,7 +1427,7 @@ class EnterpriseRouter:
{"role": "user", "content": json.dumps(user, ensure_ascii=False)}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)},
], ],
temperature=0.0, temperature=0.0,
fallback_max_tokens=512, max_tokens=512,
profile_name="router", profile_name="router",
component_name="router", component_name="router",
generation_name="llm.router", generation_name="llm.router",

View File

@@ -849,7 +849,7 @@ class AgentRuntimeMixin:
component_name="mcp_parameter_extraction", component_name="mcp_parameter_extraction",
generation_name="llm.mcp_parameter_extraction", generation_name="llm.mcp_parameter_extraction",
temperature=0.0, temperature=0.0,
fallback_max_tokens=80, max_tokens=80,
) )
raw = self._llm_response_text(response).strip() raw = self._llm_response_text(response).strip()
payload = parse_json_object(raw) payload = parse_json_object(raw)
@@ -1351,101 +1351,6 @@ class AgentRuntimeMixin:
# Compatibilidade com versões antigas do router. # Compatibilidade com versões antigas do router.
return None, arguments or {}, None return None, arguments or {}, None
@staticmethod
def _primary_multi_item_outcomes(tool_name: str, result: dict[str, Any]) -> list[dict[str, Any]]:
"""Return explicit per-item outcomes for the requested tool, when present.
The contract is intentionally structural and domain-agnostic: the framework
only recognizes an array named ``results`` whose entries expose an explicit
boolean ``success``/``ok`` signal. For workflow-backed tools, preference is
given to ``workflow.output[tool_name].results`` so a later auxiliary/composite
step cannot overwrite the already executed primary operation.
"""
if not isinstance(result, dict):
return []
body = result.get("result") if isinstance(result.get("result"), dict) else result
candidates: list[Any] = []
if isinstance(body, dict):
output = body.get("output")
if isinstance(output, dict):
primary = output.get(tool_name)
if isinstance(primary, dict):
candidates.append(primary.get("results"))
candidates.append(body.get("results"))
candidates.append(result.get("results"))
for candidate in candidates:
if not isinstance(candidate, list) or len(candidate) < 2:
continue
normalized: list[dict[str, Any]] = []
valid = True
for item in candidate:
if not isinstance(item, dict):
valid = False
break
signal = item.get("success") if isinstance(item.get("success"), bool) else item.get("ok")
if not isinstance(signal, bool):
valid = False
break
normalized.append(dict(item))
if valid and normalized:
return normalized
return []
@classmethod
def _normalize_multi_item_tool_result(cls, tool_name: str, result: dict[str, Any]) -> dict[str, Any]:
"""Preserve partial/full success of a multi-item primary operation.
Some composite workflows execute the requested tool successfully for one or
more items and then fail in a secondary validation/post-processing step. A
wrapper-level ``ok=false`` must not erase those terminal item outcomes.
This normalizer never invents business semantics. It activates only when
the requested tool itself exposes two or more explicit item outcomes with a
boolean success signal.
"""
if not isinstance(result, dict):
return result
outcomes = cls._primary_multi_item_outcomes(tool_name, result)
if not outcomes:
return result
succeeded = sum(1 for item in outcomes if bool(item.get("success") if isinstance(item.get("success"), bool) else item.get("ok")))
failed = len(outcomes) - succeeded
if succeeded and failed:
status = "PARTIAL_SUCCESS"
elif succeeded:
status = "SUCCESS"
else:
status = "FAILED"
normalized = dict(result)
metadata = dict(normalized.get("metadata") or {})
metadata["multi_item"] = {
"status": status,
"items_count": len(outcomes),
"items_succeeded_count": succeeded,
"items_failed_count": failed,
"primary_tool": tool_name,
}
normalized["metadata"] = metadata
normalized["multi_item_status"] = status
normalized["partial_success"] = status == "PARTIAL_SUCCESS"
# The requested action is considered operationally successful whenever at
# least one explicitly identified item completed. Preserve the original
# wrapper status/error for audit instead of discarding it.
if succeeded > 0 and normalized.get("ok") is False:
normalized["original_ok"] = False
normalized["ok"] = True
if normalized.get("error"):
normalized["secondary_error"] = normalized.get("error")
return normalized
async def _call_mcp_tool_uncached( async def _call_mcp_tool_uncached(
self, self,
tool_name: str, tool_name: str,
@@ -1479,7 +1384,6 @@ class AgentRuntimeMixin:
) )
result = res.model_dump(mode="json") if hasattr(res, "model_dump") else dict(res) result = res.model_dump(mode="json") if hasattr(res, "model_dump") else dict(res)
if isinstance(result, dict): if isinstance(result, dict):
result = self._normalize_multi_item_tool_result(tool_name, result)
result.setdefault("cached", False) result.setdefault("cached", False)
await self._emit_ic( await self._emit_ic(
"IC.MCP_TOOL_EXECUTED", "IC.MCP_TOOL_EXECUTED",
@@ -3209,33 +3113,21 @@ class AgentRuntimeMixin:
self._normalize_transaction_lifecycle(state) self._normalize_transaction_lifecycle(state)
# Uma transação em coleta/confirmação não pode aprisionar a sessão. O # Uma transação em coleta/confirmação não pode aprisionar a sessão. O
# EnterpriseRouter é a única fonte para interrupção semântica: SHIFT, # EnterpriseRouter é a única fonte para interrupção semântica: SHIFT ou
# ABANDON ou replacement da instância transacional na mesma intent. Não # ABANDON. Não existe interpretação lexical de desistência no runtime; o
# existe interpretação lexical de desistência no runtime; o runtime apenas # runtime apenas aplica a decisão do router e limpa os latches da ação.
# aplica a decisão do router e limpa os latches da ação.
active_before_interruption = self._active_transaction(state) active_before_interruption = self._active_transaction(state)
interruption = str(route_meta.get("transaction_interruption") or "").strip().lower() interruption = str(route_meta.get("transaction_interruption") or "").strip().lower()
if active_before_interruption and interruption in { if active_before_interruption and interruption in {"intent_shift", "explicit_abandonment"}:
"intent_shift",
"explicit_abandonment",
"same_intent_replacement",
}:
interrupted_tool = active_before_interruption.get("tool_name") interrupted_tool = active_before_interruption.get("tool_name")
self._finish_active_transaction(state, "CANCELLED") self._finish_active_transaction(state, "CANCELLED")
state["transaction_pre_validation"] = None state["transaction_pre_validation"] = None
# Generic framework boundary: consumers do not need to know why the
# previous transactional context was invalidated. This deliberately
# avoids leaking same_intent_replacement (or any other interruption
# subtype) into domain agents/workflows.
state["operational_context_reset"] = True
if interruption == "explicit_abandonment":
action = "cancelled_by_explicit_abandonment"
elif interruption == "same_intent_replacement":
action = "cancelled_by_same_intent_replacement"
else:
action = "cancelled_by_intent_shift"
state["tool_policy_result"] = { state["tool_policy_result"] = {
"action": action, "action": (
"cancelled_by_explicit_abandonment"
if interruption == "explicit_abandonment"
else "cancelled_by_intent_shift"
),
"tool_name": interrupted_tool, "tool_name": interrupted_tool,
} }
@@ -3935,21 +3827,6 @@ class AgentRuntimeMixin:
"Resultados MCP normalizados pelo framework:\n" "Resultados MCP normalizados pelo framework:\n"
+ self._compact_llm_value(mcp_results, max_chars=18000) + self._compact_llm_value(mcp_results, max_chars=18000)
) )
multi_item_summaries = []
for item in mcp_results:
metadata = item.get("metadata") if isinstance(item, dict) else None
summary = metadata.get("multi_item") if isinstance(metadata, dict) else None
if isinstance(summary, dict):
multi_item_summaries.append(summary)
if multi_item_summaries:
sections.append(
"Semântica operacional multi-item do framework:\n"
+ self._compact_llm_value(multi_item_summaries, max_chars=4000)
+ "\nOs resultados explícitos por item da operação primária são evidência terminal e "
"devem ser relatados individualmente. Um erro de etapa auxiliar/pós-processamento "
"não pode converter itens já concluídos com sucesso em falha, nem omiti-los. "
"Quando houver sucessos e falhas, descreva o resultado como parcial e informe ambos."
)
transaction_evidence = self.transaction_evidence_for_turn(state, mcp_results) transaction_evidence = self.transaction_evidence_for_turn(state, mcp_results)
if transaction_evidence: if transaction_evidence:
sections.append( sections.append(

View File

@@ -15,17 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
guardrail: guardrail:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
@@ -78,13 +72,6 @@ profiles:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0.2 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: transaction_parameter_extraction:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1-mini model: openai.gpt-4.1-mini

View File

@@ -15,18 +15,11 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 700 max_tokens: 700
processing_interruption_classifier:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 8
timeout_seconds: 5
router: router:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0 temperature: 0
max_tokens: 768 max_tokens: 500
# Safety / evaluation # Safety / evaluation
guardrail: guardrail:
@@ -94,11 +87,3 @@ profiles:
provider: oci_openai provider: oci_openai
model: openai.gpt-4.1 model: openai.gpt-4.1
temperature: 0.2 temperature: 0.2
mcp_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 80
timeout_seconds: 5

View File

@@ -27,9 +27,6 @@ dependencies = [
"PyJWT[crypto]>=2.9.0" "PyJWT[crypto]>=2.9.0"
] ]
[project.optional-dependencies]
a2a = ["a2a-sdk[http-server]==1.2.2"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
@@ -38,4 +35,4 @@ requires = ["setuptools>=80", "wheel>=0.45"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools.package-data] [tool.setuptools.package-data]
"agent_framework" = ["locales/*.json", "config/*.yaml", "guardrails/calibrated/capabilities/*.yaml"] "agent_framework" = ["config/*.yaml", "guardrails/calibrated/capabilities/*.yaml"]

View File

@@ -24,5 +24,3 @@ Requires-Dist: motor>=3.6.0
Requires-Dist: google-cloud-pubsub>=2.28.0 Requires-Dist: google-cloud-pubsub>=2.28.0
Requires-Dist: mcp>=1.9.0 Requires-Dist: mcp>=1.9.0
Requires-Dist: PyJWT[crypto]>=2.9.0 Requires-Dist: PyJWT[crypto]>=2.9.0
Provides-Extra: a2a
Requires-Dist: a2a-sdk[http-server]==1.2.2; extra == "a2a"

View File

@@ -3,7 +3,6 @@ src/agent_framework/__init__.py
src/agent_framework/extensions.py src/agent_framework/extensions.py
src/agent_framework/gateway_policy_context.py src/agent_framework/gateway_policy_context.py
src/agent_framework/idempotency.py src/agent_framework/idempotency.py
src/agent_framework/localization.py
src/agent_framework/observer.py src/agent_framework/observer.py
src/agent_framework/runtime_mcp_gateway_adapter.py src/agent_framework/runtime_mcp_gateway_adapter.py
src/agent_framework.egg-info/PKG-INFO src/agent_framework.egg-info/PKG-INFO
@@ -11,10 +10,6 @@ src/agent_framework.egg-info/SOURCES.txt
src/agent_framework.egg-info/dependency_links.txt src/agent_framework.egg-info/dependency_links.txt
src/agent_framework.egg-info/requires.txt src/agent_framework.egg-info/requires.txt
src/agent_framework.egg-info/top_level.txt src/agent_framework.egg-info/top_level.txt
src/agent_framework/a2a/__init__.py
src/agent_framework/a2a/client.py
src/agent_framework/a2a/progress.py
src/agent_framework/a2a/server.py
src/agent_framework/analytics/__init__.py src/agent_framework/analytics/__init__.py
src/agent_framework/analytics/composite_publisher.py src/agent_framework/analytics/composite_publisher.py
src/agent_framework/analytics/event_builder.py src/agent_framework/analytics/event_builder.py
@@ -44,10 +39,6 @@ src/agent_framework/config/__init__.py
src/agent_framework/config/agent_registry.py src/agent_framework/config/agent_registry.py
src/agent_framework/config/observability_mapping.yaml src/agent_framework/config/observability_mapping.yaml
src/agent_framework/config/settings.py src/agent_framework/config/settings.py
src/agent_framework/decisions/__init__.py
src/agent_framework/decisions/client.py
src/agent_framework/decisions/adapters/__init__.py
src/agent_framework/decisions/adapters/typesafe.py
src/agent_framework/events/__init__.py src/agent_framework/events/__init__.py
src/agent_framework/events/oci_streaming.py src/agent_framework/events/oci_streaming.py
src/agent_framework/gateways/__init__.py src/agent_framework/gateways/__init__.py
@@ -62,7 +53,6 @@ src/agent_framework/guardrails/__init__.py
src/agent_framework/guardrails/base.py src/agent_framework/guardrails/base.py
src/agent_framework/guardrails/config_loader.py src/agent_framework/guardrails/config_loader.py
src/agent_framework/guardrails/custom_rails.py src/agent_framework/guardrails/custom_rails.py
src/agent_framework/guardrails/dlex_sanitizer.py
src/agent_framework/guardrails/executor.py src/agent_framework/guardrails/executor.py
src/agent_framework/guardrails/framework_llm_client.py src/agent_framework/guardrails/framework_llm_client.py
src/agent_framework/guardrails/langgraph_adapters.py src/agent_framework/guardrails/langgraph_adapters.py
@@ -147,8 +137,6 @@ src/agent_framework/llm/profile_resolver.py
src/agent_framework/llm/providers.py src/agent_framework/llm/providers.py
src/agent_framework/llm/structured_output.py src/agent_framework/llm/structured_output.py
src/agent_framework/llm/types.py src/agent_framework/llm/types.py
src/agent_framework/locales/en.json
src/agent_framework/locales/pt-BR.json
src/agent_framework/mcp/__init__.py src/agent_framework/mcp/__init__.py
src/agent_framework/mcp/client.py src/agent_framework/mcp/client.py
src/agent_framework/mcp/models.py src/agent_framework/mcp/models.py
@@ -185,7 +173,6 @@ src/agent_framework/observability/noc_events.py
src/agent_framework/observability/noc_otel.py src/agent_framework/observability/noc_otel.py
src/agent_framework/observability/observer.py src/agent_framework/observability/observer.py
src/agent_framework/observability/otel.py src/agent_framework/observability/otel.py
src/agent_framework/observability/severity_policy.py
src/agent_framework/observability/streaming_events.py src/agent_framework/observability/streaming_events.py
src/agent_framework/observability/streaming_exporter.py src/agent_framework/observability/streaming_exporter.py
src/agent_framework/observability/telemetry.py src/agent_framework/observability/telemetry.py

View File

@@ -19,6 +19,3 @@ motor>=3.6.0
google-cloud-pubsub>=2.28.0 google-cloud-pubsub>=2.28.0
mcp>=1.9.0 mcp>=1.9.0
PyJWT[crypto]>=2.9.0 PyJWT[crypto]>=2.9.0
[a2a]
a2a-sdk[http-server]==1.2.2

View File

@@ -1 +0,0 @@
"""Optional A2A interoperability; imported only when enabled."""

View File

@@ -1,108 +0,0 @@
"""A2A JSON-RPC adapter. URLs and credential references are operator configured."""
import asyncio
import json
import os
from uuid import uuid4, uuid5, NAMESPACE_URL
import httpx
from google.protobuf.json_format import ParseDict, MessageToDict
from a2a.types import SendMessageResponse, Task
from .progress import report_progress
def _normalize_legacy(result):
# Preserve the original wire response separately; normalize only presentation.
if result.get('kind') == 'message':
return {'message': result}
if result.get('kind') == 'task':
return {'task': result}
raise ValueError('Expected an A2A Message or Task')
async def call_a2a(backend, request, timeout_seconds):
payload = request.get('payload') or {}
session = payload.get('session_id') or str(uuid4())
tenant = request.get('tenant_id') or 'default'
context_id = str(uuid5(NAMESPACE_URL, json.dumps([tenant, backend.backend_id, session])))
legacy = backend.a2a_version == '0.3'
text = str(payload.get('message') or payload.get('text') or payload.get('transcript') or payload.get('content') or '')
parts = [{'text': text}, {'data': payload}]
message = {'role': 'ROLE_USER', 'messageId': str(uuid4()),
'contextId': payload.get('a2a_context_id') or context_id, 'parts': parts,
'metadata': {'tenant_id': tenant, 'agent_id': request.get('agent_id') or backend.default_agent_id}}
if payload.get('a2a_task_id'):
message['taskId'] = payload['a2a_task_id']
if legacy:
message.update(kind='message', role='user')
for part in parts:
part['kind'] = 'text' if 'text' in part else 'data'
headers = {'A2A-Version': backend.a2a_version}
for header, env_name in backend.a2a_headers_env.items():
value = os.environ.get(env_name)
if not value:
raise ValueError(f'Missing credential environment variable: {env_name}')
headers[header] = value
endpoint = backend.base_url + backend.a2a_rpc_path
async with asyncio.timeout(timeout_seconds):
async with httpx.AsyncClient(timeout=timeout_seconds, headers=headers) as client:
async def rpc(method, params):
request_id = str(uuid4())
response = await client.post(endpoint, json={
'jsonrpc': '2.0', 'id': request_id, 'method': method, 'params': params,
})
response.raise_for_status()
data = response.json()
if data.get('jsonrpc') != '2.0' or data.get('id') != request_id:
raise ValueError('Invalid A2A JSON-RPC response')
if 'error' in data:
raise RuntimeError(f'A2A error {data["error"].get("code")}')
result = data.get('result')
if not isinstance(result, dict):
raise ValueError('Missing A2A result')
return result
raw = await rpc('message/send' if legacy else 'SendMessage', {
'message': message,
'configuration': {'blocking': False} if legacy else {'returnImmediately': True},
})
result = _normalize_legacy(raw) if legacy else raw
if not legacy:
result = MessageToDict(ParseDict(result, SendMessageResponse()))
while 'task' in result:
task = result['task']
state = task.get('status', {}).get('state', '')
await report_progress('a2a.backend.status', {
'backend_id': backend.backend_id, 'task_id': task.get('id'), 'state': state,
})
if state not in {'submitted', 'working', 'TASK_STATE_SUBMITTED', 'TASK_STATE_WORKING'}:
break
await asyncio.sleep(backend.a2a_poll_interval_seconds)
raw = await rpc('tasks/get' if legacy else 'GetTask', {'id': task['id']})
result = {'task': raw if legacy else MessageToDict(ParseDict(raw, Task()))}
task = result.get('task')
if task:
state, task_id = task['status']['state'], task['id']
if state in {'failed', 'canceled', 'rejected', 'unknown', 'TASK_STATE_FAILED', 'TASK_STATE_CANCELED', 'TASK_STATE_REJECTED', 'TASK_STATE_UNSPECIFIED'}:
raise RuntimeError(f'A2A task ended with state: {state}')
output_parts = [p for a in task.get('artifacts', []) for p in a.get('parts', [])]
if not output_parts and task['status'].get('message'):
output_parts += task['status']['message'].get('parts', [])
if not output_parts:
messages = [m for m in task.get('history', []) if m.get('role') in {'agent', 'ROLE_AGENT'}]
output_parts = messages[-1].get('parts', []) if messages else []
remote_context = task.get('contextId')
elif result.get('message'):
reply = result['message']
output_parts = reply.get('parts', [])
state, task_id, remote_context = None, None, reply.get('contextId')
else:
raise ValueError('Expected an A2A Message or Task')
return {
'channel': request.get('channel', 'web'), 'session_id': session,
'text': '\n'.join(p['text'] for p in output_parts if 'text' in p),
'metadata': {'a2a_context_id': remote_context, 'a2a_task_id': task_id,
'a2a_state': state, 'a2a_result': raw},
}

View File

@@ -1,10 +0,0 @@
"""Task-local progress hook; safe to import without the optional A2A SDK."""
from contextvars import ContextVar
_progress_callback = ContextVar('framework_a2a_progress', default=None)
async def report_progress(event: str, payload: dict | None = None):
callback = _progress_callback.get()
if callback is not None:
await callback(event, payload or {})

View File

@@ -1,181 +0,0 @@
"""A2A 1.0 Tasks and SSE with SDK-managed 0.3 compatibility."""
import asyncio
import logging
from uuid import uuid4
from google.protobuf.json_format import MessageToDict, ParseDict
from a2a.auth.user import User
from a2a.server.agent_execution import AgentExecutor
from a2a.server.routes import (
DefaultServerCallContextBuilder, add_a2a_routes_to_fastapi,
create_agent_card_routes, create_jsonrpc_routes,
)
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
from a2a.types import AgentCard, Message, Part, Role, Task, TaskState, TaskStatus
from a2a.utils.errors import InvalidParamsError, TaskNotCancelableError, UnsupportedOperationError
from .progress import _progress_callback
logger = logging.getLogger(__name__)
class _PrincipalUser(User):
def __init__(self, principal):
self.principal = principal
@property
def is_authenticated(self):
return True
@property
def user_name(self):
# Use middleware identity, never client-supplied message metadata.
return f'{self.principal.scheme}:{self.principal.subject}'
class FrameworkCallContextBuilder(DefaultServerCallContextBuilder):
def build_user(self, request):
principal = getattr(request.state, 'auth_principal', None)
return _PrincipalUser(principal) if principal else super().build_user(request)
def _response_message(response, context_id, task_id=None):
data = {
'messageId': str(uuid4()), 'role': 'ROLE_AGENT', 'contextId': context_id,
'parts': [{'text': str(response.get('text') or response.get('speak') or '')},
{'data': response}],
}
if task_id:
data['taskId'] = task_id
return ParseDict(data, Message())
class GatewayExecutor(AgentExecutor):
"""Wrap any native async handler as an A2A task, without detached worker hacks."""
def __init__(self, handler, task_mode=True, timeout_seconds=120.0):
self.handler = handler
self.task_mode = task_mode
self.timeout_seconds = timeout_seconds
async def execute(self, context, event_queue):
message = context.message
if message is None or message.role != Role.ROLE_USER:
raise InvalidParamsError('Expected a user message')
payload = {}
for part in MessageToDict(message).get('parts', []):
if 'data' in part:
if not isinstance(part['data'], dict):
raise InvalidParamsError('Structured data must be an object')
payload.update(part['data'])
elif 'text' not in part:
raise UnsupportedOperationError('Only text and structured data are supported')
metadata = MessageToDict(message.metadata)
session_id = context.context_id or str(uuid4())
payload.update(message=context.get_user_input(), session_id=session_id)
request = {
'channel': 'web', 'payload': payload, 'session_id': session_id,
'tenant_id': metadata.get('tenant_id'), 'agent_id': metadata.get('agent_id'),
'metadata': metadata, 'backend_id': metadata.get('backend_id'),
}
if not self.task_mode:
async with asyncio.timeout(self.timeout_seconds):
response = await self.handler(request)
await event_queue.enqueue_event(_response_message(response, session_id))
return
updater = TaskUpdater(event_queue, context.task_id, session_id)
if context.current_task is None:
await event_queue.enqueue_event(Task(
id=context.task_id, context_id=session_id,
status=TaskStatus(state=TaskState.TASK_STATE_SUBMITTED),
history=[message],
))
await updater.update_status(TaskState.TASK_STATE_WORKING)
async def progress(event, data):
await updater.update_status(TaskState.TASK_STATE_WORKING, metadata={
'framework_event': event, 'framework_payload': data,
})
token = _progress_callback.set(progress)
try:
async with asyncio.timeout(self.timeout_seconds):
response = await self.handler(request)
reply = _response_message(response, session_id, context.task_id)
response_metadata = response.get('metadata') or {}
requested_state = response_metadata.get('a2a_state') or response_metadata.get('a2a_task_state')
state = {
'input-required': TaskState.TASK_STATE_INPUT_REQUIRED,
'auth-required': TaskState.TASK_STATE_AUTH_REQUIRED,
'TASK_STATE_INPUT_REQUIRED': TaskState.TASK_STATE_INPUT_REQUIRED,
'TASK_STATE_AUTH_REQUIRED': TaskState.TASK_STATE_AUTH_REQUIRED,
}.get(requested_state, TaskState.TASK_STATE_COMPLETED)
await updater.add_artifact(list(reply.parts), artifact_id='response', name='Framework response', last_chunk=True)
await updater.update_status(state, message=reply)
except asyncio.CancelledError:
raise
except Exception as exc:
logger.exception('A2A task %s failed', context.task_id)
text = 'Processing timed out' if isinstance(exc, TimeoutError) else 'Agent processing failed'
await updater.update_status(TaskState.TASK_STATE_FAILED, message=_response_message({'text': text}, session_id, context.task_id))
finally:
_progress_callback.reset(token)
async def cancel(self, context, event_queue):
if not self.task_mode:
raise TaskNotCancelableError()
# The SDK cancels the running execute coroutine after this callback.
await TaskUpdater(event_queue, context.task_id, context.context_id).update_status(TaskState.TASK_STATE_CANCELED)
def install_a2a(app, handler, registry, public_url, security_schemes=None, security=None,
*, task_mode=True, timeout_seconds=120.0, task_store=None,
name='Agent Framework OCI Gateway', skills=None):
"""Reuse parent middleware. A persistent SDK TaskStore may be injected."""
if timeout_seconds <= 0:
raise ValueError('A2A timeout_seconds must be positive')
if skills is None:
skills = [{'id': b.backend_id, 'name': b.name or b.backend_id,
'description': b.description or b.backend_id,
'tags': b.domains or [b.backend_id], 'examples': b.examples}
for b in registry.list()]
card = ParseDict({
'name': name, 'version': '1.1.0',
'description': 'Native framework agents and A2A interoperability',
'supportedInterfaces': [{'url': public_url.rstrip('/') + '/a2a',
'protocolBinding': 'JSONRPC', 'protocolVersion': '1.0'}],
'capabilities': {'streaming': task_mode, 'pushNotifications': False},
'defaultInputModes': ['text/plain', 'application/json'],
'defaultOutputModes': ['text/plain', 'application/json'],
'securitySchemes': security_schemes or {},
'securityRequirements': [{'schemes': {k: {'list': v} for k, v in item.items()}}
for item in (security or [])],
'skills': skills,
}, AgentCard())
request_handler = DefaultRequestHandler(
GatewayExecutor(handler, task_mode=task_mode, timeout_seconds=timeout_seconds),
task_store if task_store is not None else InMemoryTaskStore(), card,
)
add_a2a_routes_to_fastapi(
app, agent_card_routes=create_agent_card_routes(card),
jsonrpc_routes=create_jsonrpc_routes(
request_handler, rpc_url='/a2a', enable_v0_3_compat=True,
context_builder=FrameworkCallContextBuilder(),
),
)
return request_handler
def install_backend_a2a(app, handler, profiles, settings, *, task_store=None):
"""Expose a framework backend directly, with skills built from agent profiles."""
return install_a2a(
app, handler, None, settings.A2A_PUBLIC_URL,
settings.A2A_SECURITY_SCHEMES or None, settings.A2A_SECURITY or None,
task_mode=settings.A2A_TASK_MODE, timeout_seconds=settings.A2A_TASK_TIMEOUT_SECONDS,
task_store=task_store, name=settings.APP_NAME,
skills=[{'id': p.agent_id, 'name': p.name or p.agent_id,
'description': p.description or p.agent_id, 'tags': [p.agent_id]}
for p in profiles.list_profiles()],
)

Some files were not shown because too many files have changed in this diff Show More