mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-11-06 03:14:12 +00:00
Compare commits
62 Commits
main
...
a30656210e
| Author | SHA1 | Date | |
|---|---|---|---|
| a30656210e | |||
| 3765c3f43a | |||
|
|
b08affa48c | ||
|
|
05b34a696e | ||
|
|
f33909e258 | ||
| 01b44773ec | |||
| f942610bc5 | |||
| c582150e1c | |||
| 36f97811fa | |||
| d606744d2e | |||
| 0d2b0f1084 | |||
| 0d0c95f9f8 | |||
| 583d801d4e | |||
| 4712bcc0dc | |||
| e16d19083d | |||
| dd207b4078 | |||
| 2f9b053098 | |||
| 7a22b7af11 | |||
| e882afde25 | |||
| f00c2f721a | |||
| f62c7993bb | |||
| 3f884d028a | |||
| 05410adf09 | |||
| ffe47e8d49 | |||
| ae5887ce50 | |||
| 82636ec15b | |||
| 1711575abe | |||
| 2c3aa58b59 | |||
| d84d833694 | |||
| fe8c18093f | |||
|
|
f4638e287a | ||
|
|
e24963abe1 | ||
| 10a6f357dd | |||
| c8c83981c4 | |||
| 83bf961185 | |||
| 81f24d7357 | |||
| 1fd18531c0 | |||
| df2a1cbe1b | |||
| 42ab000c7b | |||
| 472d44074c | |||
|
|
faf5ca55ba | ||
| ac18d68eaf | |||
| 63d0fb51c4 | |||
|
|
a472daa1e4 | ||
|
|
d93efd8972 | ||
| 727997aa41 | |||
| 2496e831a1 | |||
| b5d2a12953 | |||
| bdc44b15d6 | |||
| 05077a91bc | |||
| 19c4024693 | |||
| 22e8f56146 | |||
| 0a72afb94e | |||
| 57df723a24 | |||
| 6e43cc0158 | |||
| d271031198 | |||
| 34d33350ae | |||
| ad778f6889 | |||
| 762f6fb024 | |||
| f9c66b4792 | |||
| 560e79d21b | |||
| 23d32bbcfc |
@@ -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
|
|
||||||
|
|||||||
@@ -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
67
.idea/workspace.xml
generated
@@ -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[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"Shell Script.agent_frontend.executor": "Run",
|
||||||
"Shell Script.agent_frontend.executor": "Run",
|
"Shell Script.agent_template_backend.executor": "Run",
|
||||||
"Shell Script.agent_template_backend.executor": "Run",
|
"Shell Script.mcp_gateway.executor": "Run",
|
||||||
"Shell Script.mcp_gateway.executor": "Run",
|
"Shell Script.mcp_server.executor": "Run",
|
||||||
"Shell Script.mcp_server.executor": "Run",
|
"git-widget-placeholder": "master",
|
||||||
"git-widget-placeholder": "main",
|
"kotlin-language-version-configured": "true",
|
||||||
"kotlin-language-version-configured": "true",
|
"last_opened_file_path": "C:/Asus_Projects/kbdb/agent_framework_oci/templates/agent_template_backend",
|
||||||
"last_opened_file_path": "D:/Dropbox/ORACLE/TIM/FY27/Commits/agent_platform_oci",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"nodejs_package_manager_path": "npm",
|
||||||
"nodejs_package_manager_path": "npm",
|
"project.structure.last.edited": "Project",
|
||||||
"project.structure.last.edited": "Project",
|
"project.structure.proportion": "0.15",
|
||||||
"project.structure.proportion": "0.15",
|
"project.structure.side.proportion": "0.2",
|
||||||
"project.structure.side.proportion": "0.2",
|
"settings.editor.selected.configurable": "advanced.settings",
|
||||||
"settings.editor.selected.configurable": "advanced.settings",
|
"vue.rearranger.settings.migration": "true"
|
||||||
"vue.rearranger.settings.migration": "true"
|
|
||||||
}
|
}
|
||||||
}</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" />
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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.
|
|
||||||
133
LOCALIZACAO.md
133
LOCALIZACAO.md
@@ -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.
|
|
||||||
@@ -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`
|
|
||||||
42
README.md
42
README.md
@@ -49,91 +49,89 @@ If something is not working or if you want to understand the features of the Age
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
[English technical specifications (SPEC-001–SPEC-020)](specs/en/README.md)
|
|
||||||
|
|
||||||
## SPECs / SDDs of the Agent Platform OCI
|
## SPECs / SDDs of the Agent Platform OCI
|
||||||
|
|
||||||
The Agent Platform OCI documentation is organized into numbered SPECs/SDDs, each covering an architectural, operational, or governance area of the platform. The objective is to standardize the construction, evolution, operation, and certification of enterprise agents based on the Agent Framework OCI.
|
The Agent Platform OCI documentation is organized into numbered SPECs/SDDs, each covering an architectural, operational, or governance area of the platform. The objective is to standardize the construction, evolution, operation, and certification of enterprise agents based on the Agent Framework OCI.
|
||||||
|
|
||||||
>**/agent_platform_oci/specs**
|
>**/agent_platform_oci/specs**
|
||||||
|
|
||||||
### [SPEC-001 — Architecture](specs/en/SPEC-001-Architecture.md)
|
### [SPEC-001 — Architecture](specs/SPEC-001-Architecture.md)
|
||||||
|
|
||||||
Defines the overall platform architecture, its main components, repository structure, logical and physical architecture, core contracts, primary flow, non-functional requirements, and acceptance criteria.
|
Defines the overall platform architecture, its main components, repository structure, logical and physical architecture, core contracts, primary flow, non-functional requirements, and acceptance criteria.
|
||||||
|
|
||||||
### [SPEC-002 — Agent Runtime](specs/en/SPEC-002-Agent-Runtime.md)
|
### [SPEC-002 — Agent Runtime](specs/SPEC-002-Agent-Runtime.md)
|
||||||
|
|
||||||
Describes the conversational execution runtime for agents, including LangGraph, state management, memory, checkpoints, routing, supervisor, BusinessContext, MCP integration, RAG, events, and error handling.
|
Describes the conversational execution runtime for agents, including LangGraph, state management, memory, checkpoints, routing, supervisor, BusinessContext, MCP integration, RAG, events, and error handling.
|
||||||
|
|
||||||
### [SPEC-003 — Agent Gateway](specs/en/SPEC-003-Agent-Gateway.md)
|
### [SPEC-003 — Agent Gateway](specs/SPEC-003-Agent-Gateway.md)
|
||||||
|
|
||||||
Specifies the gateway responsible for centralizing LLM and embedding calls, including request/response contracts, profiles, providers, OCI authentication, fallback, rate limiting, metrics, security, and observability.
|
Specifies the gateway responsible for centralizing LLM and embedding calls, including request/response contracts, profiles, providers, OCI authentication, fallback, rate limiting, metrics, security, and observability.
|
||||||
|
|
||||||
### [SPEC-004 — MCP Gateway](specs/en/SPEC-004-MCP-Gateway.md)
|
### [SPEC-004 — MCP Gateway](specs/SPEC-004-MCP-Gateway.md)
|
||||||
|
|
||||||
Defines the MCP integration model, including the tool catalog, routing, execution, authorization, caching, retries, timeouts, parameter mapping, events, metrics, and standardized tool responses.
|
Defines the MCP integration model, including the tool catalog, routing, execution, authorization, caching, retries, timeouts, parameter mapping, events, metrics, and standardized tool responses.
|
||||||
|
|
||||||
### [SPEC-005 — Guardrails](specs/en/SPEC-005-Guardrails.md)
|
### [SPEC-005 — Guardrails](specs/SPEC-005-Guardrails.md)
|
||||||
|
|
||||||
Describes the platform guardrail model, covering input, output, tool, RAG, and final response policies. It also defines phases, execution modes, guardrail types, LLM profiles, base codes, events, tests, and acceptance criteria.
|
Describes the platform guardrail model, covering input, output, tool, RAG, and final response policies. It also defines phases, execution modes, guardrail types, LLM profiles, base codes, events, tests, and acceptance criteria.
|
||||||
|
|
||||||
### [SPEC-006 — Evals](specs/en/SPEC-006-Evals.md)
|
### [SPEC-006 — Evals](specs/SPEC-006-Evals.md)
|
||||||
|
|
||||||
Defines the platform evaluation layer, including online evaluation, offline evaluation, regression testing, certification, datasets, judges, metrics, CLI, API, result persistence, and evidence publication.
|
Defines the platform evaluation layer, including online evaluation, offline evaluation, regression testing, certification, datasets, judges, metrics, CLI, API, result persistence, and evidence publication.
|
||||||
|
|
||||||
### [SPEC-007 — Observability](specs/en/SPEC-007-Observability.md)
|
### [SPEC-007 — Observability](specs/SPEC-007-Observability.md)
|
||||||
|
|
||||||
Specifies the observability model, including logs, traces, metrics, Langfuse, OpenTelemetry, IC/NOC/GRL events, dashboards, alerts, data masking, and operational evidence generation.
|
Specifies the observability model, including logs, traces, metrics, Langfuse, OpenTelemetry, IC/NOC/GRL events, dashboards, alerts, data masking, and operational evidence generation.
|
||||||
|
|
||||||
### [SPEC-008 — Deployment](specs/en/SPEC-008-Deployment.md)
|
### [SPEC-008 — Deployment](specs/SPEC-008-Deployment.md)
|
||||||
|
|
||||||
Describes the platform packaging and deployment process, including deployable components, CI/CD pipelines, Kubernetes/OKE, Docker, secrets, OCI authentication, health checks, rollback, smoke tests, and certification stages.
|
Describes the platform packaging and deployment process, including deployable components, CI/CD pipelines, Kubernetes/OKE, Docker, secrets, OCI authentication, health checks, rollback, smoke tests, and certification stages.
|
||||||
|
|
||||||
### [SPEC-009 — Channel Gateway](specs/en/SPEC-009-Channel-Gateway.md)
|
### [SPEC-009 — Channel Gateway](specs/SPEC-009-Channel-Gateway.md)
|
||||||
|
|
||||||
Defines the channel gateway responsible for normalizing external payloads into the platform's canonical contract and translating responses for each channel. Covers operating modes, idempotency, versioning, security, errors, and anti-patterns.
|
Defines the channel gateway responsible for normalizing external payloads into the platform's canonical contract and translating responses for each channel. Covers operating modes, idempotency, versioning, security, errors, and anti-patterns.
|
||||||
|
|
||||||
### [SPEC-010 — Agent Development](specs/en/SPEC-010-Agent-Development.md)
|
### [SPEC-010 — Agent Development](specs/SPEC-010-Agent-Development.md)
|
||||||
|
|
||||||
Describes the standard for agent development using templates, YAML configuration, BusinessContext, MCP, guardrails, judges, RAG, memory, observability, and evaluations. It also differentiates framework and agent responsibilities.
|
Describes the standard for agent development using templates, YAML configuration, BusinessContext, MCP, guardrails, judges, RAG, memory, observability, and evaluations. It also differentiates framework and agent responsibilities.
|
||||||
|
|
||||||
### [SPEC-011 — Governance Model](specs/en/SPEC-011-Governance-Model.md)
|
### [SPEC-011 — Governance Model](specs/SPEC-011-Governance-Model.md)
|
||||||
|
|
||||||
Defines the platform governance model, including ownership, roles and responsibilities, RACI, agent governance, prompts, guardrails, judges, models, MCP, datasets, approval processes, and mandatory evidence.
|
Defines the platform governance model, including ownership, roles and responsibilities, RACI, agent governance, prompts, guardrails, judges, models, MCP, datasets, approval processes, and mandatory evidence.
|
||||||
|
|
||||||
### [SPEC-012 — Canonical Contracts](specs/en/SPEC-012-Canonical-Contracts.md)
|
### [SPEC-012 — Canonical Contracts](specs/SPEC-012-Canonical-Contracts.md)
|
||||||
|
|
||||||
Documents the platform's canonical contracts, including GatewayRequest, ChannelResponse, BusinessContext, AgentState, ToolInvocation, ToolResult, LLMRequest, LLMResponse, EvaluationRun, and EventEnvelope. It also defines contract evolution rules.
|
Documents the platform's canonical contracts, including GatewayRequest, ChannelResponse, BusinessContext, AgentState, ToolInvocation, ToolResult, LLMRequest, LLMResponse, EvaluationRun, and EventEnvelope. It also defines contract evolution rules.
|
||||||
|
|
||||||
### [SPEC-013 — Versioning and Compatibility Model](specs/en/SPEC-013-Versioning-and-Compatibility-Model.md)
|
### [SPEC-013 — Versioning and Compatibility Model](specs/SPEC-013-Versioning-and-Compatibility-Model.md)
|
||||||
|
|
||||||
Defines the platform versioning and compatibility model, including Semantic Versioning, versioned artifacts, contract versioning, compatibility matrices, deprecation policies, migration, and rollback.
|
Defines the platform versioning and compatibility model, including Semantic Versioning, versioned artifacts, contract versioning, compatibility matrices, deprecation policies, migration, and rollback.
|
||||||
|
|
||||||
### [SPEC-014 — Templates and Agent Creation Model](specs/en/SPEC-014-Templates-and-Agent-Creation-Model.md)
|
### [SPEC-014 — Templates and Agent Creation Model](specs/SPEC-014-Templates-and-Agent-Creation-Model.md)
|
||||||
|
|
||||||
Describes the official templates and the agent creation model from scratch. Explains what belongs to the framework, what belongs to the agent, the standard structure, and the step-by-step process for copying templates, defining scope, registering agents, configuring routes, tools, BusinessContext, prompts, datasets, and tests.
|
Describes the official templates and the agent creation model from scratch. Explains what belongs to the framework, what belongs to the agent, the standard structure, and the step-by-step process for copying templates, defining scope, registering agents, configuring routes, tools, BusinessContext, prompts, datasets, and tests.
|
||||||
|
|
||||||
### [SPEC-015 — Adoption and Eligibility Criteria](specs/en/SPEC-015-Adoption-and-Eligibility-Criteria.md)
|
### [SPEC-015 — Adoption and Eligibility Criteria](specs/SPEC-015-Adoption-and-Eligibility-Criteria.md)
|
||||||
|
|
||||||
Defines clear platform adoption criteria, including recommended and non-recommended use cases, business entry criteria, architecture, security, quality, operations, exception processes, and adoption checklists.
|
Defines clear platform adoption criteria, including recommended and non-recommended use cases, business entry criteria, architecture, security, quality, operations, exception processes, and adoption checklists.
|
||||||
|
|
||||||
### [SPEC-016 — Agent Development Lifecycle](specs/en/SPEC-016-Agent-Development-Lifecycle.md)
|
### [SPEC-016 — Agent Development Lifecycle](specs/SPEC-016-Agent-Development-Lifecycle.md)
|
||||||
|
|
||||||
Describes the complete agent development lifecycle, from discovery and scope definition through design, prompting, MCP, RAG, implementation, testing, evaluation, certification, validation, and production.
|
Describes the complete agent development lifecycle, from discovery and scope definition through design, prompting, MCP, RAG, implementation, testing, evaluation, certification, validation, and production.
|
||||||
|
|
||||||
### [SPEC-017 — Release Management and CI/CD](specs/en/SPEC-017-Release-Management-and-CICD.md)
|
### [SPEC-017 — Release Management and CI/CD](specs/SPEC-017-Release-Management-and-CICD.md)
|
||||||
|
|
||||||
Defines the release and CI/CD model, including standard pipelines, stages, release artifacts, quality gates, rollback strategies, common errors, and acceptance criteria.
|
Defines the release and CI/CD model, including standard pipelines, stages, release artifacts, quality gates, rollback strategies, common errors, and acceptance criteria.
|
||||||
|
|
||||||
### [SPEC-018 — Security and Identity Model](specs/en/SPEC-018-Security-and-Identity-Model.md)
|
### [SPEC-018 — Security and Identity Model](specs/SPEC-018-Security-and-Identity-Model.md)
|
||||||
|
|
||||||
Specifies the platform security and identity model, covering authentication, Workload Identity, authorization, secrets, data protection, MCP security, channel security, auditing, and acceptance criteria.
|
Specifies the platform security and identity model, covering authentication, Workload Identity, authorization, secrets, data protection, MCP security, channel security, auditing, and acceptance criteria.
|
||||||
|
|
||||||
### [SPEC-019 — Evaluation and Certification Framework](specs/en/SPEC-019-Evaluation-and-Certification-Framework.md)
|
### [SPEC-019 — Evaluation and Certification Framework](specs/SPEC-019-Evaluation-and-Certification-Framework.md)
|
||||||
|
|
||||||
Details the evaluation and certification framework, including evaluation architecture, metrics, datasets, EvaluationRun, CLI, certification processes, mandatory evidence, and acceptance criteria.
|
Details the evaluation and certification framework, including evaluation architecture, metrics, datasets, EvaluationRun, CLI, certification processes, mandatory evidence, and acceptance criteria.
|
||||||
|
|
||||||
### [SPEC-020 — Operational Readiness and SRE Model](specs/en/SPEC-020-Operational-Readiness-and-SRE-Model.md)
|
### [SPEC-020 — Operational Readiness and SRE Model](specs/SPEC-020-Operational-Readiness-and-SRE-Model.md)
|
||||||
|
|
||||||
Defines the platform operational readiness and SRE model, including managed components, health checks, readiness, SLOs, metrics, dashboards, alerts, runbooks, incident management, capacity planning, and production checklists.
|
Defines the platform operational readiness and SRE model, including managed components, health checks, readiness, SLOs, metrics, dashboards, alerts, runbooks, incident management, capacity planning, and production checklists.
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function renderResponse(response) {
|
|||||||
for (const part of agentMessages(response)) {
|
for (const part of agentMessages(response)) {
|
||||||
const section=document.createElement('section'); section.className='agent-section';
|
const section=document.createElement('section'); section.className='agent-section';
|
||||||
const header=document.createElement('div'); header.className='agent-header';
|
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 avatar=document.createElement('span'); avatar.className='agent-avatar'; avatar.textContent='✳'; avatar.setAttribute('aria-hidden','true');
|
||||||
const name=document.createElement('span'); name.textContent=String(part.agent).replaceAll('_',' ');
|
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);
|
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);
|
header.append(avatar,name); section.append(header,body); group.append(section);
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<html lang="pt-BR">
|
<html lang="pt-BR">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<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">
|
<title>Agent Workspace · A2A</title><link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="workspace">
|
<div class="workspace">
|
||||||
<aside class="sidebar">
|
<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>
|
<a class="brand" href="./"><span class="brand-mark" aria-hidden="true">a</span><span>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>
|
<div class="sidebar-heading"><span>CONEXÃO</span><span class="badge" id="versionBadge">A2A</span></div>
|
||||||
<form id="connectionForm" class="connection-form">
|
<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>
|
<label>Endereço do serviço<input id="serviceUrl" type="url" value="http://localhost:8000" required spellcheck="false"></label>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</section>
|
</section>
|
||||||
<div id="notice" class="notice" role="alert" hidden></div>
|
<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">
|
<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>
|
<div id="welcome" class="welcome"><span class="welcome-icon" aria-hidden="true">✳</span><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>
|
</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>
|
<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>
|
<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>
|
||||||
|
|||||||
@@ -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 |
File diff suppressed because one or more lines are too long
@@ -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,
|
||||||
|
|||||||
@@ -46,13 +46,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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
159
deploy/oke/load-test/.env.runtime
Normal file
159
deploy/oke/load-test/.env.runtime
Normal 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
|
||||||
@@ -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`.
|
|
||||||
@@ -29,7 +29,7 @@ python -m pip install -e 'libs/agent_framework[a2a]' -r apps/agent_gateway/requi
|
|||||||
export ENABLE_A2A=true
|
export ENABLE_A2A=true
|
||||||
export A2A_PUBLIC_URL=https://meu-gateway.example
|
export A2A_PUBLIC_URL=https://meu-gateway.example
|
||||||
export BACKENDS_CONFIG_PATH="$PWD/apps/agent_gateway/config/backends.a2a.example.yaml"
|
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
|
python -m uvicorn app.main:app --app-dir apps/agent_gateway --port 8010
|
||||||
```
|
```
|
||||||
|
|
||||||
Edite primeiro as URLs do catálogo de exemplo. Para agentes externos, informe o
|
Edite primeiro as URLs do catálogo de exemplo. Para agentes externos, informe o
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -38,4 +38,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"]
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -147,8 +138,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 +174,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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agent_framework.decisions import invoke as decision_invoke, predicate
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -57,11 +55,8 @@ async def classify_processing_interruption(
|
|||||||
f"Complemento/interrupção atual: {supplement_client}\n"
|
f"Complemento/interrupção atual: {supplement_client}\n"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
response = await decision_invoke(llm,
|
response = await llm.ainvoke(
|
||||||
[{"role": "system", "content": prompt}],
|
[{"role": "system", "content": prompt}],
|
||||||
decision_point="processing_interruption",
|
|
||||||
decision_questions=[predicate("regenerate", "Does the interruption require regenerating the response?")],
|
|
||||||
decision_render=lambda d: "1" if d["regenerate"] else "0",
|
|
||||||
temperature=0,
|
temperature=0,
|
||||||
fallback_max_tokens=8,
|
fallback_max_tokens=8,
|
||||||
profile_name=profile_name,
|
profile_name=profile_name,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,10 +21,7 @@ class Settings(BaseSettings):
|
|||||||
A2A_SECURITY_SCHEMES: dict = Field(default_factory=dict)
|
A2A_SECURITY_SCHEMES: dict = Field(default_factory=dict)
|
||||||
A2A_SECURITY: list[dict[str, list[str]]] = Field(default_factory=list)
|
A2A_SECURITY: list[dict[str, list[str]]] = Field(default_factory=list)
|
||||||
|
|
||||||
AGENT_LOCALE: str = 'pt-BR'
|
APP_NAME: str = 'ai-agent-template'
|
||||||
AGENT_LOCALES_DIR: str | None = None
|
|
||||||
|
|
||||||
APP_NAME: str = 'ai-agent-template'
|
|
||||||
APP_ENV: str = 'local'
|
APP_ENV: str = 'local'
|
||||||
LOG_LEVEL: str = 'INFO'
|
LOG_LEVEL: str = 'INFO'
|
||||||
API_HOST: str = '0.0.0.0'
|
API_HOST: str = '0.0.0.0'
|
||||||
@@ -285,7 +282,3 @@ def get_settings() -> Settings:
|
|||||||
return Settings()
|
return Settings()
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
# Fail early for invalid locale catalogs rather than during a transaction.
|
|
||||||
from agent_framework.localization import catalog
|
|
||||||
catalog(settings.AGENT_LOCALE)
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
"""Opt-in typed decision providers. Generative tasks keep their existing provider."""
|
|
||||||
from .client import choice, predicate, score, invoke, selected, evaluate
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
"""Optional decision-provider adapters; no changes to the default engine."""
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,97 +0,0 @@
|
|||||||
"""Isolated TypeSafe API adapter. Uses the verified `noul` wire primitive.
|
|
||||||
|
|
||||||
Choice is implemented as independent binary evaluations with strict rejection of
|
|
||||||
ambiguous outcomes. This is an adapter strategy, NOT a native TypeSafe choice
|
|
||||||
contract. The framework's original decision engine is never changed.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
|
|
||||||
logger = logging.getLogger("agent_framework.decisions")
|
|
||||||
|
|
||||||
class TypeSafeContractError(ValueError):
|
|
||||||
def __init__(self, code: str):
|
|
||||||
self.code = code
|
|
||||||
super().__init__(code)
|
|
||||||
|
|
||||||
|
|
||||||
def _wire_name(index: int, option: int | None = None) -> str:
|
|
||||||
return f"__jev_q{index}" if option is None else f"__jev_q{index}_o{option}"
|
|
||||||
|
|
||||||
|
|
||||||
def build_request(state: str, questions: list[dict]) -> dict:
|
|
||||||
converted = {}
|
|
||||||
for i, question in enumerate(questions):
|
|
||||||
kind = question["type"]
|
|
||||||
if kind == "predicate":
|
|
||||||
converted[_wire_name(i)] = {"type": "noul", "instructions": question["instructions"]}
|
|
||||||
elif kind == "choice":
|
|
||||||
options = question["choices"]
|
|
||||||
if not options:
|
|
||||||
raise TypeSafeContractError("EMPTY_CHOICE")
|
|
||||||
for j, option in enumerate(options):
|
|
||||||
# Do not interpolate the user's state into the instruction.
|
|
||||||
converted[_wire_name(i, j)] = {
|
|
||||||
"type": "noul",
|
|
||||||
"instructions": (
|
|
||||||
f"Assess whether '{option['value']}' is the correct selection for "
|
|
||||||
f"the decision described here: {question['instructions']}. "
|
|
||||||
"Evaluate the supplied state and policy; answer yes only if this "
|
|
||||||
"specific option is appropriate."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
elif kind == "score":
|
|
||||||
# The direct API score contract has not been verified. Fail closed.
|
|
||||||
raise TypeSafeContractError("UNSUPPORTED_DIRECT_SCORE")
|
|
||||||
else:
|
|
||||||
raise TypeSafeContractError("UNSUPPORTED_QUESTION_TYPE")
|
|
||||||
return {"model": os.getenv("JEV_MODEL", "jev-latest"), "state": state, "questions": converted}
|
|
||||||
|
|
||||||
|
|
||||||
def _probability(answer: object) -> float:
|
|
||||||
if not isinstance(answer, dict) or answer.get("type") not in (None, "noul", "predicate"):
|
|
||||||
raise TypeSafeContractError("INVALID_NOUL_ANSWER")
|
|
||||||
value = answer.get("noul", answer.get("probability"))
|
|
||||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not 0 <= value <= 1:
|
|
||||||
raise TypeSafeContractError("INVALID_NOUL_PROBABILITY")
|
|
||||||
return float(value)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_answers(response: dict, questions: list[dict]) -> list[dict]:
|
|
||||||
if not isinstance(response, dict) or not isinstance(response.get("answers"), dict):
|
|
||||||
raise TypeSafeContractError("INVALID_ANSWERS_CONTAINER")
|
|
||||||
answers = response["answers"]
|
|
||||||
expected = set()
|
|
||||||
for i, q in enumerate(questions):
|
|
||||||
if q["type"] == "choice":
|
|
||||||
expected.update(_wire_name(i, j) for j in range(len(q["choices"])))
|
|
||||||
else:
|
|
||||||
expected.add(_wire_name(i))
|
|
||||||
if set(answers) != expected:
|
|
||||||
raise TypeSafeContractError("MISSING_OR_EXTRA_ANSWER")
|
|
||||||
result = []
|
|
||||||
for i, q in enumerate(questions):
|
|
||||||
kind = q["type"]
|
|
||||||
if kind == "predicate":
|
|
||||||
result.append({"name": q["name"], "type": "predicate", "probability": _probability(answers[_wire_name(i)])})
|
|
||||||
elif kind == "choice":
|
|
||||||
probabilities = [_probability(answers[_wire_name(i, j)]) for j in range(len(q["choices"]))]
|
|
||||||
ranked = sorted(range(len(probabilities)), key=lambda j: probabilities[j], reverse=True)
|
|
||||||
winner, runner_up = probabilities[ranked[0]], probabilities[ranked[1]] if len(ranked) > 1 else 0.0
|
|
||||||
# Require a single positive, unambiguous choice. Low confidence
|
|
||||||
# remains subject to the unchanged JEV_MIN_CONFIDENCE in client.py.
|
|
||||||
if os.getenv("JEV_DECISION_DIAGNOSTICS", "false").strip().lower() == "true":
|
|
||||||
logger.info("jev.choice.diagnostics question_index=%d probabilities=%s winner_index=%d winner=%.4f runner_up=%.4f margin=%.4f status=%s",
|
|
||||||
i, json.dumps([round(v, 4) for v in probabilities]), ranked[0],
|
|
||||||
winner, runner_up, winner - runner_up,
|
|
||||||
"ambiguous" if winner <= 0.5 or winner - runner_up < 0.15 else "candidate")
|
|
||||||
if winner <= 0.5 or winner - runner_up < 0.15:
|
|
||||||
raise TypeSafeContractError("AMBIGUOUS_DIRECT_CHOICE")
|
|
||||||
result.append({"name": q["name"], "type": "choice",
|
|
||||||
"choice": q["choices"][ranked[0]]["value"], "confidence": winner,
|
|
||||||
"option_probabilities": probabilities})
|
|
||||||
else:
|
|
||||||
raise TypeSafeContractError("UNSUPPORTED_DIRECT_QUESTION_TYPE")
|
|
||||||
return result
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
logger = logging.getLogger("agent_framework.decisions")
|
|
||||||
|
|
||||||
|
|
||||||
def env_key(point: str) -> str:
|
|
||||||
return "DECISION_" + re.sub(r"[^A-Z0-9]+", "_", point.upper()).strip("_") + "_PROVIDER"
|
|
||||||
|
|
||||||
|
|
||||||
def selected(point: str, group: str | None = None) -> bool:
|
|
||||||
default = os.getenv(env_key(group), os.getenv("DECISION_PROVIDER", "current")) if group else os.getenv("DECISION_PROVIDER", "current")
|
|
||||||
value = os.getenv(env_key(point), default).strip().lower()
|
|
||||||
if value not in {"current", "jev"}:
|
|
||||||
raise ValueError(f"{env_key(point)} must be current or jev")
|
|
||||||
return value == "jev"
|
|
||||||
|
|
||||||
|
|
||||||
def choice(name: str, values: list, instructions: str = "") -> dict:
|
|
||||||
values = list(dict.fromkeys(str(v) for v in values))
|
|
||||||
return {"type": "choice", "name": name, "instructions": instructions or f"Select {name} following the supplied policy.",
|
|
||||||
"choices": [{"value": v, "description": v} for v in values]}
|
|
||||||
|
|
||||||
|
|
||||||
def predicate(name: str, instructions: str = "") -> dict:
|
|
||||||
return {"type": "predicate", "name": name, "instructions": instructions or f"Is {name} true under the supplied policy?"}
|
|
||||||
|
|
||||||
|
|
||||||
def score(name: str, instructions: str = "") -> dict:
|
|
||||||
return {"type": "score", "name": name, "instructions": instructions or "Apply the supplied rubric on a 0 to 10 scale.",
|
|
||||||
"levels": [{"label": str(i)} for i in range(11)]}
|
|
||||||
|
|
||||||
|
|
||||||
def _number(value: Any, low: float, high: float) -> float:
|
|
||||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or not low <= value <= high:
|
|
||||||
raise _validation_error("INVALID_PROBABILITY_OR_SCORE")
|
|
||||||
return float(value)
|
|
||||||
|
|
||||||
|
|
||||||
def _jev_endpoint() -> str:
|
|
||||||
"""Return a trusted HTTPS decision endpoint configured by the deployer.
|
|
||||||
|
|
||||||
The endpoint is an operator setting, never derived from model output.
|
|
||||||
Set JEV_ALLOWED_HOSTS in production to restrict credential destinations.
|
|
||||||
"""
|
|
||||||
endpoint = os.getenv("JEV_ENDPOINT", "https://ai-gateway.vercel.sh/v1/decisions").strip()
|
|
||||||
parsed = urllib.parse.urlsplit(endpoint)
|
|
||||||
if (parsed.scheme != "https" or not parsed.hostname or parsed.username or
|
|
||||||
parsed.password or parsed.fragment or not parsed.path):
|
|
||||||
raise ValueError("JEV_ENDPOINT must be a valid HTTPS endpoint URL")
|
|
||||||
allowed = [host.strip().lower() for host in os.getenv(
|
|
||||||
"JEV_ALLOWED_HOSTS", "ai-gateway.vercel.sh,api.typesafe.ai").split(",") if host.strip()]
|
|
||||||
if parsed.hostname.lower() not in allowed:
|
|
||||||
raise ValueError("JEV_ENDPOINT host is not in JEV_ALLOWED_HOSTS")
|
|
||||||
return endpoint
|
|
||||||
|
|
||||||
|
|
||||||
def _post(body: dict) -> dict:
|
|
||||||
key = (os.getenv("JEV_API_KEY") or os.getenv("AI_GATEWAY_API_KEY") or "").strip()
|
|
||||||
if not key:
|
|
||||||
raise ValueError("JEV_API_KEY or AI_GATEWAY_API_KEY is required for JEV")
|
|
||||||
timeout = float(os.getenv("JEV_TIMEOUT_SECONDS", "15"))
|
|
||||||
if not math.isfinite(timeout) or timeout <= 0:
|
|
||||||
raise ValueError("JEV_TIMEOUT_SECONDS must be positive")
|
|
||||||
request = urllib.request.Request(_jev_endpoint(),
|
|
||||||
data=json.dumps(body, ensure_ascii=False).encode("utf-8"), method="POST",
|
|
||||||
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
|
|
||||||
# urllib does not automatically follow redirects for POST in all cases;
|
|
||||||
# disable redirects to avoid sending credentials to another host.
|
|
||||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
||||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
||||||
return None
|
|
||||||
|
|
||||||
opener = urllib.request.build_opener(NoRedirect)
|
|
||||||
with opener.open(request, timeout=timeout) as response:
|
|
||||||
return json.load(response)
|
|
||||||
|
|
||||||
|
|
||||||
def _safe_http_diagnostic(exc: urllib.error.HTTPError) -> str:
|
|
||||||
"""Extract only structural validation hints; never log echoed inputs or tokens."""
|
|
||||||
if exc.code not in (400, 422):
|
|
||||||
return "unavailable"
|
|
||||||
try:
|
|
||||||
raw = exc.read(16384)
|
|
||||||
payload = json.loads(raw)
|
|
||||||
except (ValueError, TypeError, OSError, UnicodeError):
|
|
||||||
return "unavailable"
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
return "unavailable"
|
|
||||||
detail = payload.get("detail")
|
|
||||||
if not isinstance(detail, list):
|
|
||||||
return "unavailable"
|
|
||||||
safe = []
|
|
||||||
for item in detail[:5]:
|
|
||||||
if not isinstance(item, dict):
|
|
||||||
continue
|
|
||||||
# Only the Pydantic error *type* is safe to expose, not message/input/context.
|
|
||||||
kind = item.get("type")
|
|
||||||
if not isinstance(kind, str) or not re.fullmatch(r"[a-z][a-z0-9_]{0,63}", kind):
|
|
||||||
continue
|
|
||||||
loc = item.get("loc")
|
|
||||||
# Restrict locations to known schema tokens; never echo dynamic names.
|
|
||||||
known = {"body", "model", "state", "questions", "type", "instructions",
|
|
||||||
"choices", "levels", "value", "description", "label"}
|
|
||||||
path = ".".join(part for part in loc if isinstance(part, str) and part in known) if isinstance(loc, list) else ""
|
|
||||||
safe.append(kind + (":" + path if path else ""))
|
|
||||||
return ",".join(safe) or "unavailable"
|
|
||||||
|
|
||||||
|
|
||||||
def _diagnostics_enabled() -> bool:
|
|
||||||
"""Opt-in numeric decision diagnostics; never expose state, prompts or choice text."""
|
|
||||||
return os.getenv("JEV_DECISION_DIAGNOSTICS", "false").strip().lower() == "true"
|
|
||||||
|
|
||||||
|
|
||||||
def _validation_error(code: str) -> ValueError:
|
|
||||||
# Explicit, stable codes for logs; existing fallback/exception behavior stays unchanged.
|
|
||||||
return ValueError(code)
|
|
||||||
|
|
||||||
|
|
||||||
async def evaluate(point: str, messages: list, questions: list[dict], *, llm=None, operation=None, group=None) -> dict:
|
|
||||||
if not questions or len({q["name"] for q in questions}) != len(questions):
|
|
||||||
raise ValueError("Decision questions must have unique names")
|
|
||||||
if any(q["type"] == "choice" and not q["choices"] for q in questions):
|
|
||||||
raise ValueError("Decision choice requires options")
|
|
||||||
started = time.monotonic()
|
|
||||||
state = json.dumps({"policy_and_context": messages}, ensure_ascii=False, default=str)
|
|
||||||
# The default Vercel wire contract remains byte-for-byte equivalent.
|
|
||||||
direct_typesafe = urllib.parse.urlsplit(_jev_endpoint()).hostname == "api.typesafe.ai"
|
|
||||||
if direct_typesafe:
|
|
||||||
from .adapters.typesafe import build_request, normalize_answers
|
|
||||||
request_body = build_request(state, questions)
|
|
||||||
else:
|
|
||||||
request_body = {"model": os.getenv("JEV_MODEL", "typesafe-ai/jev"),
|
|
||||||
"input": state, "questions": questions}
|
|
||||||
response = await asyncio.to_thread(_post, request_body)
|
|
||||||
await _record(llm, point, response, operation)
|
|
||||||
answers = normalize_answers(response, questions) if direct_typesafe else response.get("answers")
|
|
||||||
if not isinstance(answers, list) or len(answers) != len(questions):
|
|
||||||
raise _validation_error("MISSING_DECISION_ANSWERS")
|
|
||||||
indexed = {a.get("name"): a for a in answers if isinstance(a, dict)}
|
|
||||||
if len(indexed) != len(questions):
|
|
||||||
raise _validation_error("DUPLICATE_OR_INVALID_ANSWERS")
|
|
||||||
result, confidences, predicate_probabilities = {}, [], []
|
|
||||||
diagnostic_questions = []
|
|
||||||
for q in questions:
|
|
||||||
a = indexed.get(q["name"], {})
|
|
||||||
if a.get("type") != q["type"]:
|
|
||||||
raise _validation_error("ANSWER_TYPE_MISMATCH")
|
|
||||||
if q["type"] == "choice":
|
|
||||||
value = a.get("choice")
|
|
||||||
if value not in {v["value"] for v in q["choices"]}:
|
|
||||||
raise _validation_error("UNDECLARED_CHOICE")
|
|
||||||
# Missing confidence must never be promoted to certainty.
|
|
||||||
confidences.append(_number(a.get("confidence", 0), 0, 1))
|
|
||||||
if _diagnostics_enabled():
|
|
||||||
options = [v["value"] for v in q["choices"]]
|
|
||||||
detail = {"question_index": len(diagnostic_questions), "type": "choice",
|
|
||||||
"selected_option_index": options.index(value),
|
|
||||||
"confidence": round(confidences[-1], 4)}
|
|
||||||
if isinstance(a.get("option_probabilities"), list):
|
|
||||||
detail["option_probabilities"] = [round(_number(v, 0, 1), 4) for v in a["option_probabilities"]]
|
|
||||||
diagnostic_questions.append(detail)
|
|
||||||
elif q["type"] == "predicate":
|
|
||||||
probability = _number(a.get("probability"), 0, 1)
|
|
||||||
value = probability >= 0.5
|
|
||||||
confidences.append(max(probability, 1-probability))
|
|
||||||
predicate_probabilities.append((q["name"], probability))
|
|
||||||
if _diagnostics_enabled():
|
|
||||||
diagnostic_questions.append({"question_index": len(diagnostic_questions), "type": "predicate",
|
|
||||||
"decision": value, "probability_true": round(probability, 4),
|
|
||||||
"confidence": round(confidences[-1], 4)})
|
|
||||||
else:
|
|
||||||
value = _number(a.get("score"), 0, len(q["levels"])-1)
|
|
||||||
confidences.append(_number(a.get("confidence", 0), 0, 1))
|
|
||||||
if _diagnostics_enabled():
|
|
||||||
diagnostic_questions.append({"question_index": len(diagnostic_questions), "type": "score",
|
|
||||||
"score": value, "confidence": round(confidences[-1], 4)})
|
|
||||||
result[q["name"]] = value
|
|
||||||
confidence = min(confidences)
|
|
||||||
default_minimum = os.getenv(env_key(group).replace("_PROVIDER", "_MIN_CONFIDENCE"), os.getenv("JEV_MIN_CONFIDENCE", "0.7")) if group else os.getenv("JEV_MIN_CONFIDENCE", "0.7")
|
|
||||||
minimum = float(os.getenv(env_key(point).replace("_PROVIDER", "_MIN_CONFIDENCE"), default_minimum))
|
|
||||||
_number(minimum, 0, 1)
|
|
||||||
if _diagnostics_enabled():
|
|
||||||
logger.info("jev.decision.diagnostics point=%s status=%s confidence=%.4f minimum_confidence=%.4f questions=%s",
|
|
||||||
point, "accepted" if confidence >= minimum else "rejected_low_confidence",
|
|
||||||
confidence, minimum, json.dumps(diagnostic_questions, separators=(",", ":")))
|
|
||||||
if confidence < minimum:
|
|
||||||
# A NOUL value is P(True), not confidence in the True answer.
|
|
||||||
# Log only numeric calibration data, never decision input or question text.
|
|
||||||
logger.warning("decision point=%s provider=jev confidence=%.4f minimum_confidence=%.4f status=rejected predicate_probabilities=%s",
|
|
||||||
point, confidence, minimum,
|
|
||||||
[(name, round(probability, 4)) for name, probability in predicate_probabilities])
|
|
||||||
raise _validation_error("CONFIDENCE_BELOW_THRESHOLD")
|
|
||||||
result["confidence"] = confidence
|
|
||||||
result["reason"] = "JEV typed decision (no generated explanation)."
|
|
||||||
logger.info("decision point=%s provider=jev model=%s elapsed_ms=%.1f usage=%s gateway=%s",
|
|
||||||
point, response.get("model"), (time.monotonic()-started)*1000,
|
|
||||||
response.get("usage"), response.get("provider_metadata", {}).get("gateway", {}))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
async def invoke(llm: Any, messages: list, *, decision_point: str, decision_questions: list,
|
|
||||||
decision_render=None, decision_generative: bool = False, decision_group: str | None = None, **kwargs: Any) -> str:
|
|
||||||
if decision_generative or not selected(decision_point, decision_group):
|
|
||||||
return await llm.ainvoke(messages, **kwargs)
|
|
||||||
try:
|
|
||||||
result = await evaluate(decision_point, messages, decision_questions, llm=llm, operation=kwargs.get("generation_name"), group=decision_group)
|
|
||||||
return decision_render(result) if decision_render else json.dumps(result, ensure_ascii=False)
|
|
||||||
except Exception as exc:
|
|
||||||
# Do not log exception text, input, response bodies or credentials.
|
|
||||||
from urllib.error import HTTPError
|
|
||||||
from .adapters.typesafe import TypeSafeContractError
|
|
||||||
category = (exc.code if isinstance(exc, TypeSafeContractError) else
|
|
||||||
"HTTP_ERROR" if isinstance(exc, HTTPError) else
|
|
||||||
"VALIDATION_ERROR" if isinstance(exc, ValueError) else
|
|
||||||
"TRANSPORT_ERROR")
|
|
||||||
http_status = exc.code if isinstance(exc, HTTPError) else None
|
|
||||||
error_code = (exc.code if isinstance(exc, TypeSafeContractError) else
|
|
||||||
str(exc) if isinstance(exc, ValueError) and
|
|
||||||
str(exc) in {"MISSING_DECISION_ANSWERS", "DUPLICATE_OR_INVALID_ANSWERS",
|
|
||||||
"ANSWER_TYPE_MISMATCH", "UNDECLARED_CHOICE",
|
|
||||||
"CONFIDENCE_BELOW_THRESHOLD", "INVALID_PROBABILITY_OR_SCORE"}
|
|
||||||
else "UNCLASSIFIED")
|
|
||||||
http_detail = _safe_http_diagnostic(exc) if isinstance(exc, HTTPError) else "not_applicable"
|
|
||||||
logger.warning("decision point=%s provider=jev failure=%s category=%s http_status=%s "
|
|
||||||
"error_code=%s http_schema=%s question_types=%s",
|
|
||||||
decision_point, type(exc).__name__, category, http_status,
|
|
||||||
error_code, http_detail, ",".join(sorted({q.get("type", "unknown") for q in decision_questions if isinstance(q, dict)})))
|
|
||||||
fallback = os.getenv("DECISION_FALLBACK_TO_CURRENT", "true").strip().lower()
|
|
||||||
if fallback not in {"true", "false"}:
|
|
||||||
raise ValueError("DECISION_FALLBACK_TO_CURRENT must be true or false")
|
|
||||||
if fallback != "true":
|
|
||||||
raise
|
|
||||||
logger.info("decision point=%s provider=current fallback=true", decision_point)
|
|
||||||
return await llm.ainvoke(messages, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
async def multi_intent(llm, messages, *, intents, text, **kwargs):
|
|
||||||
"""Keep source-fragment extraction and request order; JEV selects each intent."""
|
|
||||||
raw = await llm.ainvoke(messages, **kwargs)
|
|
||||||
if not selected("multi_intent"):
|
|
||||||
return raw
|
|
||||||
from agent_framework.llm.structured_output import parse_json_object
|
|
||||||
candidates = parse_json_object(raw).get("operations")
|
|
||||||
if not isinstance(candidates, list) or not candidates:
|
|
||||||
raise ValueError("Multi-intent extraction returned no candidates")
|
|
||||||
if any(not isinstance(c, dict) or not str(c.get("source_text") or "").strip()
|
|
||||||
or str(c["source_text"]) not in text for c in candidates):
|
|
||||||
raise ValueError("Multi-intent source must be a literal user-message fragment")
|
|
||||||
questions = [choice("operation_" + str(n), [i.name for i in intents] + ["__unsupported__"],
|
|
||||||
"Classify ONLY this extracted objective: " + c["source_text"] + ". Use __unsupported__ for an objective outside the intent catalog.") for n, c in enumerate(candidates)]
|
|
||||||
|
|
||||||
class ExtractedProvider:
|
|
||||||
usage_repository = getattr(llm, "usage_repository", None)
|
|
||||||
telemetry = getattr(llm, "telemetry", None)
|
|
||||||
|
|
||||||
async def ainvoke(self, messages, **kwargs):
|
|
||||||
return raw
|
|
||||||
|
|
||||||
return await invoke(ExtractedProvider(), messages, decision_point="multi_intent", decision_questions=questions,
|
|
||||||
decision_render=lambda d: json.dumps({"confidence": d["confidence"], "operations": [
|
|
||||||
{"intent": None if d["operation_" + str(n)] == "__unsupported__" else d["operation_" + str(n)],
|
|
||||||
"unsupported": d["operation_" + str(n)] == "__unsupported__", "source_text": c["source_text"]}
|
|
||||||
for n, c in enumerate(candidates)]}))
|
|
||||||
|
|
||||||
|
|
||||||
async def _record(llm, point, response, operation):
|
|
||||||
"""Account for billed responses even if validation later rejects them."""
|
|
||||||
if llm is None:
|
|
||||||
return
|
|
||||||
usage = response.get("usage") or {}
|
|
||||||
gateway = (response.get("provider_metadata") or {}).get("gateway") or {}
|
|
||||||
model = response.get("model") or os.getenv("JEV_MODEL", "typesafe-ai/jev")
|
|
||||||
metadata = {"provider": "jev", "model": model, "decision_point": point,
|
|
||||||
"generation_id": gateway.get("generationId"), "gateway_cost": gateway.get("cost")}
|
|
||||||
try:
|
|
||||||
repository = getattr(llm, "usage_repository", None)
|
|
||||||
if repository is not None:
|
|
||||||
from agent_framework.billing.usage_repository import UsageRecord
|
|
||||||
await repository.record(UsageRecord.from_usage("jev", model, operation or "decision." + point,
|
|
||||||
{"prompt_tokens": usage.get("input_tokens", 0), "completion_tokens": usage.get("output_tokens", 0),
|
|
||||||
"total_tokens": usage.get("total_tokens", 0), "cost_usd": gateway.get("cost", 0)}, metadata))
|
|
||||||
telemetry = getattr(llm, "telemetry", None)
|
|
||||||
if telemetry is not None:
|
|
||||||
await telemetry.event("decision.jev", {**metadata, "usage": usage})
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning("decision accounting point=%s failure=%s", point, type(exc).__name__)
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agent_framework.decisions import invoke as decision_invoke, choice
|
|
||||||
|
|
||||||
from agent_framework.localization import terms
|
|
||||||
|
|
||||||
from agent_framework.llm.structured_output import parse_json_object
|
from agent_framework.llm.structured_output import parse_json_object
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -17,7 +13,9 @@ from .session_store import InMemoryGlobalSessionStore
|
|||||||
|
|
||||||
logger = logging.getLogger("agent_framework.global_supervisor")
|
logger = logging.getLogger("agent_framework.global_supervisor")
|
||||||
|
|
||||||
# Terminal vocabulary is read from the selected locale at request time.
|
_TERMINAL_WORDS = {
|
||||||
|
"obrigado", "obrigada", "valeu", "tchau", "encerrar", "fim", "cancelar atendimento"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GlobalSupervisorRouter:
|
class GlobalSupervisorRouter:
|
||||||
@@ -164,12 +162,10 @@ class GlobalSupervisorRouter:
|
|||||||
return fallback or self._route_by_rules(text, mode)
|
return fallback or self._route_by_rules(text, mode)
|
||||||
prompt = self._build_supervisor_prompt(text, request)
|
prompt = self._build_supervisor_prompt(text, request)
|
||||||
try:
|
try:
|
||||||
raw = await decision_invoke(self.llm, [
|
raw = await self.llm.ainvoke([
|
||||||
{"role": "system", "content": "Você é um supervisor global de backends. Responda somente JSON válido."},
|
{"role": "system", "content": "Você é um supervisor global de backends. Responda somente JSON válido."},
|
||||||
{"role": "user", "content": prompt},
|
{"role": "user", "content": prompt},
|
||||||
], temperature=0, decision_point='global_supervisor',
|
], temperature=0, profile_name="supervisor", component_name="supervisor", generation_name="llm.supervisor")
|
||||||
decision_questions=[choice("backend_id", list(self.registry.backends))],
|
|
||||||
profile_name="supervisor", component_name="supervisor", generation_name="llm.supervisor")
|
|
||||||
data = self._parse_json(raw)
|
data = self._parse_json(raw)
|
||||||
backend_id = str(data.get("backend") or data.get("backend_id") or "").strip()
|
backend_id = str(data.get("backend") or data.get("backend_id") or "").strip()
|
||||||
if backend_id not in self.registry.backends:
|
if backend_id not in self.registry.backends:
|
||||||
@@ -217,10 +213,10 @@ class GlobalSupervisorRouter:
|
|||||||
n = self._normalize(text)
|
n = self._normalize(text)
|
||||||
if not n:
|
if not n:
|
||||||
return True
|
return True
|
||||||
if n in {self._normalize(value) for value in terms("global_router.terminal")}:
|
if n in _TERMINAL_WORDS:
|
||||||
return False
|
return False
|
||||||
tokens = n.split()
|
tokens = n.split()
|
||||||
followup_markers = [self._normalize(value) for value in terms("global_router.followup")]
|
followup_markers = ["esse", "essa", "isso", "valor", "ele", "ela", "tambem", "e ", "entao", "nesse", "nessa"]
|
||||||
return len(tokens) <= 6 or any(marker in n for marker in followup_markers)
|
return len(tokens) <= 6 or any(marker in n for marker in followup_markers)
|
||||||
|
|
||||||
def _extract_text(self, request: GlobalRouteRequest) -> str:
|
def _extract_text(self, request: GlobalRouteRequest) -> str:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agent_framework.decisions import selected
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -22,24 +20,59 @@ from .prompts.pinj import build_pinj_prompt
|
|||||||
from .prompts.ragsec import build_ragsec_prompt
|
from .prompts.ragsec import build_ragsec_prompt
|
||||||
from .prompts.fallback import build_fallback_prompt
|
from .prompts.fallback import build_fallback_prompt
|
||||||
|
|
||||||
# Vocabulary is owned by framework_llm_client and the locale catalog.
|
_AOFERTA_TRIGGERS = (
|
||||||
|
"quer aproveitar",
|
||||||
|
"que tal tambem",
|
||||||
|
"que tal também",
|
||||||
|
"posso ja",
|
||||||
|
"posso já",
|
||||||
|
"ja que esta",
|
||||||
|
"já que está",
|
||||||
|
"aproveita e",
|
||||||
|
"aproveite e",
|
||||||
|
"tambem cancelar",
|
||||||
|
"também cancelar",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Mock determinístico do REVPREC: substrings de ação dada como FEITA (a pergunta do rail
|
# Mock determinístico do REVPREC: substrings de ação dada como FEITA (a pergunta do rail
|
||||||
# desde 2026-08-06). A detecção rica (fatura × ação, protocolo, histórico) é do prompt.
|
# desde 2026-08-06). A detecção rica (fatura × ação, protocolo, histórico) é do prompt.
|
||||||
# Vocabulary is owned by framework_llm_client and the locale catalog.
|
_REVPREC_MARKERS = (
|
||||||
|
"cancelamento confirmado",
|
||||||
|
"foi cancelado",
|
||||||
|
"cancelado com sucesso",
|
||||||
|
"cancelei",
|
||||||
|
"cancelamos",
|
||||||
|
"retiramos o valor",
|
||||||
|
"retirei o valor",
|
||||||
|
"contestacao foi registrada",
|
||||||
|
"contestação foi registrada",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Vocabulary is owned by framework_llm_client and the locale catalog.
|
_TOXOUT_MOCK_PATTERNS = (
|
||||||
|
r"\b(idiota|imbecil|burro|estúpido|inútil|maldito|miserável|incompetente)\b",
|
||||||
|
r"\b(idiots?|stupid|useless|moron)\b",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Vocabulary is owned by framework_llm_client and the locale catalog.
|
_OOS_MOCK_TRIGGERS = (
|
||||||
|
"política",
|
||||||
|
"religião",
|
||||||
|
"presidente",
|
||||||
|
"concorrente",
|
||||||
|
"vivo",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Substrings inequívocas de fraseado proibido (mock determinístico). Mantidas
|
# Substrings inequívocas de fraseado proibido (mock determinístico). Mantidas
|
||||||
# curtas e sem ambiguidade para não colidir com falas legítimas; a detecção rica
|
# curtas e sem ambiguidade para não colidir com falas legítimas; a detecção rica
|
||||||
# (allow-list, "entendo" no início etc.) é responsabilidade do prompt 20b real.
|
# (allow-list, "entendo" no início etc.) é responsabilidade do prompt 20b real.
|
||||||
# Vocabulary is owned by framework_llm_client and the locale catalog.
|
_FRASEOLOGIA_MOCK_TRIGGERS = (
|
||||||
|
"bundle",
|
||||||
|
"parceiro",
|
||||||
|
"terceiros",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Tasks cujo prompt pede UM DÍGITO (1 = passa, 0 = bloqueia) em vez de JSON, com o
|
# Tasks cujo prompt pede UM DÍGITO (1 = passa, 0 = bloqueia) em vez de JSON, com o
|
||||||
@@ -154,7 +187,7 @@ class GuardrailLLMClient:
|
|||||||
injete o `LangfuseCallbackHandler` para que o `ChatLLM` da reescrita
|
injete o `LangfuseCallbackHandler` para que o `ChatLLM` da reescrita
|
||||||
apareca como span no Langfuse.
|
apareca como span no Langfuse.
|
||||||
"""
|
"""
|
||||||
if self.use_mock and not selected("guardrail_" + task.lower()):
|
if self.use_mock:
|
||||||
return self._mock_classify(task, payload)
|
return self._mock_classify(task, payload)
|
||||||
|
|
||||||
# O caminho real usa exclusivamente o provider oficial do framework.
|
# O caminho real usa exclusivamente o provider oficial do framework.
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agent_framework.localization import message, protocol_pattern, vocalize_protocol
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ._compat import RailResult, span
|
from ._compat import RailResult, span
|
||||||
@@ -50,12 +48,26 @@ def ausencia_oferta_proativa(text: str, context: dict = None, *, callbacks: list
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Protocol vocabulary comes from the selected locale catalog.
|
_DIGIT_WORDS_RE = (
|
||||||
|
r"(?:zero|um|dois|tr[êe]s|quatro|cinco|seis|sete|oito|nove)"
|
||||||
|
)
|
||||||
# Token vocalizado: palavra de dígito ou letra única (a-z).
|
# Token vocalizado: palavra de dígito ou letra única (a-z).
|
||||||
# Protocol vocabulary comes from the selected locale catalog.
|
_SPOKEN_TOKEN_RE = rf"(?:{_DIGIT_WORDS_RE}|[a-z])"
|
||||||
# 6+ tokens vocalizados separados por espaço (cobre PRT-XXXX vocalizado).
|
# 6+ tokens vocalizados separados por espaço (cobre PRT-XXXX vocalizado).
|
||||||
# Protocol vocabulary comes from the selected locale catalog.
|
_SPOKEN_PROTOCOL_RE = (
|
||||||
# Protocol vocabulary comes from the selected locale catalog.
|
rf"(?:{_SPOKEN_TOKEN_RE}\s+){{5,}}{_SPOKEN_TOKEN_RE}\b"
|
||||||
|
)
|
||||||
|
_PROTOCOL_PATTERN = re.compile(
|
||||||
|
r"(?i)\bprotocolo\b"
|
||||||
|
r"[\s\S]{0,40}?"
|
||||||
|
r"(?:"
|
||||||
|
r"\d{6,}" # formato legado: 6+ dígitos literais
|
||||||
|
r"|"
|
||||||
|
r"PRT-[A-Z0-9]{6,}" # formato bruto da provedor (caso o LLM não vocalize)
|
||||||
|
r"|"
|
||||||
|
rf"{_SPOKEN_PROTOCOL_RE}" # formato vocalizado (palavras + letras)
|
||||||
|
r")"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def compliance_anatel(text: str, context: dict) -> RailResult:
|
def compliance_anatel(text: str, context: dict) -> RailResult:
|
||||||
@@ -84,7 +96,7 @@ def compliance_anatel(text: str, context: dict) -> RailResult:
|
|||||||
mechanism="regex",
|
mechanism="regex",
|
||||||
)
|
)
|
||||||
expected = list(context.get("expected_protocols") or [])
|
expected = list(context.get("expected_protocols") or [])
|
||||||
has_protocol = bool(protocol_pattern().search(text))
|
has_protocol = bool(_PROTOCOL_PATTERN.search(text))
|
||||||
if not has_protocol:
|
if not has_protocol:
|
||||||
return RailResult(
|
return RailResult(
|
||||||
allowed=False,
|
allowed=False,
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ Ambos retornam `RailResult.allowed=True`; o caller substitui o texto por
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agent_framework.localization import message
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -89,7 +87,9 @@ def _mask_card(match: "re.Match") -> str:
|
|||||||
return _CARD_MASK if _looks_like_card(digits) else digits
|
return _CARD_MASK if _looks_like_card(digits) else digits
|
||||||
|
|
||||||
|
|
||||||
# Canonical toxicity response is localized at call time.
|
_TOXOUT_CANONICAL_MESSAGE = (
|
||||||
|
"Não consegui formular uma resposta adequada, posso ajudar de outra forma?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_client = GuardrailLLMClient()
|
_client = GuardrailLLMClient()
|
||||||
@@ -228,7 +228,7 @@ def _sanitize_toxic(
|
|||||||
|
|
||||||
with span("rail.TOXOUT.canonical", mechanism="python"):
|
with span("rail.TOXOUT.canonical", mechanism="python"):
|
||||||
logger.warning("guardrails.toxic_fallback_canonical")
|
logger.warning("guardrails.toxic_fallback_canonical")
|
||||||
return message("toxicity.canonical"), "canonical"
|
return _TOXOUT_CANONICAL_MESSAGE, "canonical"
|
||||||
|
|
||||||
|
|
||||||
def mascarar_pii_output(text: str, context: dict = None) -> RailResult:
|
def mascarar_pii_output(text: str, context: dict = None) -> RailResult:
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ do agente.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from agent_framework.localization import CatalogMapping
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
@@ -44,7 +42,64 @@ _client = GuardrailLLMClient()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 2026-05-16
|
# 2026-05-16
|
||||||
_FALLBACK_BY_CODE = CatalogMapping("messages.fallback")
|
_FALLBACK_BY_CODE: dict[str, str] = {
|
||||||
|
"INPUT_SIZE": (
|
||||||
|
"Sua mensagem ficou muito longa pra eu processar de uma vez. "
|
||||||
|
"Pode reformular de forma mais curta ou dividir em partes menores "
|
||||||
|
"e me reenviar?"
|
||||||
|
),
|
||||||
|
"AOFERTA": (
|
||||||
|
"Posso te ajudar com mais alguma dúvida sobre sua conta ou fatura?"
|
||||||
|
),
|
||||||
|
"REVPREC": (
|
||||||
|
"No momento não consigo confirmar essa ação dessa forma. "
|
||||||
|
"Vou continuar verificando as informações disponíveis."
|
||||||
|
),
|
||||||
|
"CMP": (
|
||||||
|
"Não consegui validar todas as informações necessárias neste momento. "
|
||||||
|
"Vou seguir verificando os dados do atendimento."
|
||||||
|
),
|
||||||
|
"OOS": (
|
||||||
|
"Essa solicitação está fora do meu escopo de atendimento. "
|
||||||
|
"Posso te ajudar com dúvidas sobre contas, consumo ou faturas da provedor."
|
||||||
|
),
|
||||||
|
"DLEX_IN": (
|
||||||
|
"Não consegui interpretar essa solicitação com segurança. "
|
||||||
|
"Pode reformular sua mensagem de outra forma?"
|
||||||
|
),
|
||||||
|
"PINJ": (
|
||||||
|
"Não consegui processar essa solicitação da forma enviada. "
|
||||||
|
"Pode reformular sua pergunta para continuarmos?"
|
||||||
|
),
|
||||||
|
"RAGSEC": (
|
||||||
|
"Não encontrei informações suficientes para responder isso com segurança. "
|
||||||
|
"Pode detalhar melhor sua solicitação?"
|
||||||
|
),
|
||||||
|
"DLEX_OUT": (
|
||||||
|
"Prefiro reformular minha resposta para evitar informações incorretas. "
|
||||||
|
"Pode me confirmar exatamente o que deseja consultar?"
|
||||||
|
),
|
||||||
|
"TOX": (
|
||||||
|
"Entendo que essa situação é frustrante. Vou te ajudar a verificar isso."
|
||||||
|
),
|
||||||
|
"INTENCAO_CANCELAR": (
|
||||||
|
"Deixa eu confirmar o que você gostaria de fazer: você quer entender "
|
||||||
|
"o que é essa cobrança ou prefere cancelar o serviço?"
|
||||||
|
),
|
||||||
|
"CORRESPONDENCIA_ITEM": (
|
||||||
|
"Preciso confirmar um detalhe antes de prosseguirmos. Pode me confirmar "
|
||||||
|
"qual serviço você deseja cancelar e o valor que esperava?"
|
||||||
|
),
|
||||||
|
"ALCADA": (
|
||||||
|
"Este ajuste precisa ser analisado por um especialista provedor. "
|
||||||
|
"Vou encaminhar seu atendimento para continuar com um especialista "
|
||||||
|
"que poderá te ajudar melhor nesse caso."
|
||||||
|
),
|
||||||
|
"ACTION_CONFIRMATION_RETRY": (
|
||||||
|
"Antes de prosseguirmos, preciso confirmar: você gostaria mesmo de "
|
||||||
|
"realizar essa ação?"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
#2026-05-19
|
#2026-05-19
|
||||||
def _run_rail(
|
def _run_rail(
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user