diff --git a/.env.jev.example b/.env.jev.example new file mode 100644 index 00000000..cc13d33c --- /dev/null +++ b/.env.jev.example @@ -0,0 +1,56 @@ +# 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 diff --git a/JEV_INTEGRATION.md b/JEV_INTEGRATION.md new file mode 100644 index 00000000..a635528f --- /dev/null +++ b/JEV_INTEGRATION.md @@ -0,0 +1,84 @@ +# 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= +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__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_ | +| GUARDRAIL_PINJ, COER, TOX, REVPREC, AOFERTA, OOS, FRASEOLOGIA, RAGSEC, DLEX_IN, DLEX_OUT | Cada tarefa calibrada tem sua própria variável GUARDRAIL_ | +| JUDGE_CSI, JUDGE_VCTN, JUDGE_ALUC, JUDGE_RQLT | Cada tarefa de judge calibrado, como padrão de grupo | +| JUDGE_ | Judge genérico, usando seu `name`; o padrão é JUDGE_LLM_JUDGE | + +Um judge calibrado também aceita `DECISION_JUDGE__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__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. diff --git a/libs/agent_framework/src/agent_framework.egg-info/PKG-INFO b/libs/agent_framework/src/agent_framework.egg-info/PKG-INFO index a25a9f96..8dbcc582 100644 --- a/libs/agent_framework/src/agent_framework.egg-info/PKG-INFO +++ b/libs/agent_framework/src/agent_framework.egg-info/PKG-INFO @@ -24,3 +24,5 @@ Requires-Dist: motor>=3.6.0 Requires-Dist: google-cloud-pubsub>=2.28.0 Requires-Dist: mcp>=1.9.0 Requires-Dist: PyJWT[crypto]>=2.9.0 +Provides-Extra: a2a +Requires-Dist: a2a-sdk[http-server]==1.2.2; extra == "a2a" diff --git a/libs/agent_framework/src/agent_framework.egg-info/SOURCES.txt b/libs/agent_framework/src/agent_framework.egg-info/SOURCES.txt index f524a570..d1dd3a76 100644 --- a/libs/agent_framework/src/agent_framework.egg-info/SOURCES.txt +++ b/libs/agent_framework/src/agent_framework.egg-info/SOURCES.txt @@ -3,6 +3,7 @@ src/agent_framework/__init__.py src/agent_framework/extensions.py src/agent_framework/gateway_policy_context.py src/agent_framework/idempotency.py +src/agent_framework/localization.py src/agent_framework/observer.py src/agent_framework/runtime_mcp_gateway_adapter.py src/agent_framework.egg-info/PKG-INFO @@ -10,6 +11,10 @@ src/agent_framework.egg-info/SOURCES.txt src/agent_framework.egg-info/dependency_links.txt src/agent_framework.egg-info/requires.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/composite_publisher.py src/agent_framework/analytics/event_builder.py @@ -39,6 +44,10 @@ src/agent_framework/config/__init__.py src/agent_framework/config/agent_registry.py src/agent_framework/config/observability_mapping.yaml 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/oci_streaming.py src/agent_framework/gateways/__init__.py @@ -138,6 +147,8 @@ src/agent_framework/llm/profile_resolver.py src/agent_framework/llm/providers.py src/agent_framework/llm/structured_output.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/client.py src/agent_framework/mcp/models.py @@ -174,6 +185,7 @@ src/agent_framework/observability/noc_events.py src/agent_framework/observability/noc_otel.py src/agent_framework/observability/observer.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_exporter.py src/agent_framework/observability/telemetry.py diff --git a/libs/agent_framework/src/agent_framework.egg-info/requires.txt b/libs/agent_framework/src/agent_framework.egg-info/requires.txt index 199445b2..ee253b14 100644 --- a/libs/agent_framework/src/agent_framework.egg-info/requires.txt +++ b/libs/agent_framework/src/agent_framework.egg-info/requires.txt @@ -19,3 +19,6 @@ motor>=3.6.0 google-cloud-pubsub>=2.28.0 mcp>=1.9.0 PyJWT[crypto]>=2.9.0 + +[a2a] +a2a-sdk[http-server]==1.2.2 diff --git a/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc index e26dd07b..e2c503c7 100644 Binary files a/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc index d7015db0..a98f6de1 100644 Binary files a/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/__pycache__/idempotency.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/__pycache__/localization.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/localization.cpython-313.pyc new file mode 100644 index 00000000..a13d29bd Binary files /dev/null and b/libs/agent_framework/src/agent_framework/__pycache__/localization.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc index cb07b29b..d344151a 100644 Binary files a/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/__pycache__/observer.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc index 736d34bf..d43f65f5 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc index b27cca73..4ea8cd75 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/composite_publisher.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc index 99dc601e..a28e0ca3 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/event_builder.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc index e6291469..41113e12 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/factory.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc index d162b8c2..fd295b7f 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/publisher.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc index 53839a5e..00cce0da 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_payload_mapper.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc index d730909a..88ee4ff2 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/__pycache__/tim_sequence.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc index 2f832e92..2ce3be05 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc index 1c8ddcb8..46fc6e0b 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/kafka.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc index 31fee362..1a408e4c 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/langfuse.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc index d970a9cf..992a57e4 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/oci_streaming.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc index 2b7b5810..bb10194f 100644 Binary files a/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/analytics/providers/__pycache__/pubsub.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc index f7fa4328..20e9641f 100644 Binary files a/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/billing/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc index a3604e64..f270bb6d 100644 Binary files a/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/billing/__pycache__/usage_repository.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc index 5a030140..3d2b5dce 100644 Binary files a/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/cache/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc b/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc index f63d3a50..04c01872 100644 Binary files a/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/cache/__pycache__/cache.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc index 3ac254ef..8514b60a 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc index 24e30c3c..34808e84 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/adapters.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc index d2b66476..6672c1c7 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/base.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc index 8e6f6ff8..b123509d 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/gateway.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc index 70d9720f..377ead59 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/interruption.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc b/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc index b4d95169..7b95ef0c 100644 Binary files a/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/channels/__pycache__/transcription.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/channels/interruption.py b/libs/agent_framework/src/agent_framework/channels/interruption.py index 9151a1f0..69ec53d4 100644 --- a/libs/agent_framework/src/agent_framework/channels/interruption.py +++ b/libs/agent_framework/src/agent_framework/channels/interruption.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, predicate + from dataclasses import dataclass from typing import Any @@ -55,8 +57,11 @@ async def classify_processing_interruption( f"Complemento/interrupção atual: {supplement_client}\n" ) try: - response = await llm.ainvoke( + response = await decision_invoke(llm, [{"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, fallback_max_tokens=8, profile_name=profile_name, diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc index 7bb5d695..8c96604e 100644 Binary files a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc index f71ef5ce..08e24c18 100644 Binary files a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/checkpoint_repository.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc index 75738974..652826fe 100644 Binary files a/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/checkpoints/__pycache__/langgraph_saver.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc index 5644525b..f1524ee7 100644 Binary files a/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc index 191cf79f..d84a0ae7 100644 Binary files a/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/config/__pycache__/agent_registry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc index a7ebf2c6..903a14a5 100644 Binary files a/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/config/__pycache__/settings.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/decisions/__init__.py b/libs/agent_framework/src/agent_framework/decisions/__init__.py new file mode 100644 index 00000000..95a60eb5 --- /dev/null +++ b/libs/agent_framework/src/agent_framework/decisions/__init__.py @@ -0,0 +1,2 @@ +"""Opt-in typed decision providers. Generative tasks keep their existing provider.""" +from .client import choice, predicate, score, invoke, selected, evaluate diff --git a/libs/agent_framework/src/agent_framework/decisions/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/decisions/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..2bf47d6d Binary files /dev/null and b/libs/agent_framework/src/agent_framework/decisions/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/decisions/__pycache__/client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/decisions/__pycache__/client.cpython-313.pyc new file mode 100644 index 00000000..6894fe34 Binary files /dev/null and b/libs/agent_framework/src/agent_framework/decisions/__pycache__/client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/decisions/adapters/__init__.py b/libs/agent_framework/src/agent_framework/decisions/adapters/__init__.py new file mode 100644 index 00000000..2e42bb9d --- /dev/null +++ b/libs/agent_framework/src/agent_framework/decisions/adapters/__init__.py @@ -0,0 +1 @@ +"""Optional decision-provider adapters; no changes to the default engine.""" diff --git a/libs/agent_framework/src/agent_framework/decisions/adapters/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/decisions/adapters/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..3251e510 Binary files /dev/null and b/libs/agent_framework/src/agent_framework/decisions/adapters/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/decisions/adapters/__pycache__/typesafe.cpython-313.pyc b/libs/agent_framework/src/agent_framework/decisions/adapters/__pycache__/typesafe.cpython-313.pyc new file mode 100644 index 00000000..70df4204 Binary files /dev/null and b/libs/agent_framework/src/agent_framework/decisions/adapters/__pycache__/typesafe.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/decisions/adapters/typesafe.py b/libs/agent_framework/src/agent_framework/decisions/adapters/typesafe.py new file mode 100644 index 00000000..810a951a --- /dev/null +++ b/libs/agent_framework/src/agent_framework/decisions/adapters/typesafe.py @@ -0,0 +1,97 @@ +"""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 diff --git a/libs/agent_framework/src/agent_framework/decisions/client.py b/libs/agent_framework/src/agent_framework/decisions/client.py new file mode 100644 index 00000000..0dbcb82f --- /dev/null +++ b/libs/agent_framework/src/agent_framework/decisions/client.py @@ -0,0 +1,298 @@ +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__) diff --git a/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc index 9063960d..01c0689e 100644 Binary files a/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/gateways/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc index df983509..3eada9ed 100644 Binary files a/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/gateways/__pycache__/mcp_gateway_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/global_supervisor/router.py b/libs/agent_framework/src/agent_framework/global_supervisor/router.py index 39e99ef0..905d7ef8 100644 --- a/libs/agent_framework/src/agent_framework/global_supervisor/router.py +++ b/libs/agent_framework/src/agent_framework/global_supervisor/router.py @@ -1,5 +1,7 @@ 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 @@ -162,10 +164,12 @@ class GlobalSupervisorRouter: return fallback or self._route_by_rules(text, mode) prompt = self._build_supervisor_prompt(text, request) try: - raw = await self.llm.ainvoke([ + raw = await decision_invoke(self.llm, [ {"role": "system", "content": "Você é um supervisor global de backends. Responda somente JSON válido."}, {"role": "user", "content": prompt}, - ], temperature=0, profile_name="supervisor", component_name="supervisor", generation_name="llm.supervisor") + ], temperature=0, decision_point='global_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) backend_id = str(data.get("backend") or data.get("backend_id") or "").strip() if backend_id not in self.registry.backends: diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc index e9f5f7cc..a5f81857 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc index af465b5c..623e538c 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/base.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc index a20c3996..5056a856 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/config_loader.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc index 420e2999..55c18d95 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/custom_rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/dlex_sanitizer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/dlex_sanitizer.cpython-313.pyc index 7c7d0c9c..af5981b3 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/dlex_sanitizer.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/dlex_sanitizer.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc index 1e24755e..ecfc2e96 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/framework_llm_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc index f10be619..2b9a8610 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/llm_rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc index df216a07..df5af378 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/output_supervisor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc index 39f9b8b3..7f43504b 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/parallel_executor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc index bebcbb51..29225b02 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/pipeline.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc index a12b1423..caa69435 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_action.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc index 6d5d22a5..8108ff76 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_decision.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc index 339ed044..355f0695 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rail_result.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc index eb54420a..ed1424b5 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/__pycache__/rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc index fd957fa5..42476bee 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc index 1d169a79..69fc03e6 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/_compat.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc index 0df90b8a..34303c34 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contestation_validation.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc index 0cf8fdbb..991b0199 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/contracts.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc index b13dc0b5..51a7a2db 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/input_size.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc index 19861429..df8ad874 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_adapter.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc index 065d1778..b20d5691 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc index e728acc0..865f0d32 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/llm_rails.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc index 2d1dd3c8..83fba96c 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/output_sanitization.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc index 85eed048..515f4f49 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/__pycache__/pipeline.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/llm_client.py b/libs/agent_framework/src/agent_framework/guardrails/calibrated/llm_client.py index 0cce3a26..dd8dcf9b 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/calibrated/llm_client.py +++ b/libs/agent_framework/src/agent_framework/guardrails/calibrated/llm_client.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import selected + import json import os from typing import Any @@ -152,7 +154,7 @@ class GuardrailLLMClient: injete o `LangfuseCallbackHandler` para que o `ChatLLM` da reescrita apareca como span no Langfuse. """ - if self.use_mock: + if self.use_mock and not selected("guardrail_" + task.lower()): return self._mock_classify(task, payload) # O caminho real usa exclusivamente o provider oficial do framework. diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc index 50f04023..6de369ed 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc index b4d1aa4b..79569bb2 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/_context.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc index 9a3bd5a9..149b9f23 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ausencia_oferta_proativa.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc index 0ed66cba..9bbde66c 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/coerencia.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc index 2a371b78..e84fa79a 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_in.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc index cda4abbb..93cf17be 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/dlex_out.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc index bd3c7bcb..51575e8e 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fallback.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc index 324f3c5e..baa9c69f 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/fraseologia.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc index 77abe50c..cea72ea7 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/out_of_scope.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc index ca6552b4..fb105a77 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/pinj.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc index 8019c7d4..0928c7f0 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/ragsec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc index 0c8b7a81..7c3c1283 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/revprec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc index 4dbd2df0..9b688a4f 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/tox.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc index 889d4e54..2033ade5 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/prompts/__pycache__/toxicidade_output.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc index 5630abdc..f3acdd74 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc index afaaef9e..bf113b38 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/alcada.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc index 3aaca289..730ad6fb 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/anatel.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc index 3dcc28bb..347178e8 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/confirmation.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc index befde91c..29858008 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_in.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc index 6d00e20a..effd3923 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/dlex_out.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc index 196107e2..0a8069ec 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/ragsec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc index 0961bfe8..eb7a7a0a 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/revprec.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc index 53cbcc88..0f52a2e0 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rails/__pycache__/tox.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc index ebd23aae..6156c055 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc index 5c943779..38f73289 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/alcada.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc index fcf52507..b1f9e5f2 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/pinj_patterns.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc index c910508d..d608d9fa 100644 Binary files a/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/guardrails/calibrated/rules/__pycache__/tox_blocklist.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/guardrails/framework_llm_client.py b/libs/agent_framework/src/agent_framework/guardrails/framework_llm_client.py index 4f6983aa..becdca6f 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/framework_llm_client.py +++ b/libs/agent_framework/src/agent_framework/guardrails/framework_llm_client.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, selected, predicate + from agent_framework.localization import terms from agent_framework.llm.structured_output import parse_json_object @@ -369,6 +371,7 @@ async def classify_with_framework_llm( `profile_name`, `component_name` e `generation_name`, em vez de criar um cliente LLM paralelo fora da arquitetura do framework. """ + decision_point = component_name or "guardrail." + task.lower() selected_profile = _selected_profile_for_task(task, profile_name) owned_llm = llm is None llm = _ensure_framework_llm(llm) @@ -379,7 +382,7 @@ async def classify_with_framework_llm( # provider and surfaces the bad model/provider error instead of returning a # local fallback result. force_real_from_profile = _profile_forces_real_llm(llm, selected_profile) if llm is not None else False - if (llm is None) or (_truthy(os.getenv("USE_MOCK_LLM"), True) and not force_real_from_profile): + if (llm is None) or (_truthy(os.getenv("USE_MOCK_LLM"), True) and not force_real_from_profile and not selected(decision_point, "guardrail_" + task.lower())): out = _mock_classify(task, payload) out.setdefault("profile_name", selected_profile) out.setdefault("profile_forced_real_llm", False) @@ -396,11 +399,16 @@ async def classify_with_framework_llm( else "Responda apenas JSON válido, sem markdown." ) try: - raw = await llm.ainvoke( + raw = await decision_invoke(llm, [ {"role": "system", "content": system_instruction}, {"role": "user", "content": prompt}, ], + decision_point=decision_point, + decision_group="guardrail_" + task.lower(), + decision_generative=task in {"TOXOUT", "FALLBACK"}, + decision_questions=[predicate("allowed", "Apply the supplied guardrail policy and determine whether the text is allowed. " + ("The policy digit 1 means BLOCK, 0 means ALLOW." if task == "REVPREC" else ""))], + decision_render=lambda d: (str(int(d["allowed"]) if _BINARY_BLOCK_DIGIT.get(task, "0") == "0" else int(not d["allowed"])) if task in _BINARY_TASKS else json.dumps({**d, "label": "OK" if d["allowed"] else task})), profile_name=selected_profile, component_name=selected_component, generation_name=selected_generation, diff --git a/libs/agent_framework/src/agent_framework/guardrails/llm_rails.py b/libs/agent_framework/src/agent_framework/guardrails/llm_rails.py index b047ca6c..de356170 100644 --- a/libs/agent_framework/src/agent_framework/guardrails/llm_rails.py +++ b/libs/agent_framework/src/agent_framework/guardrails/llm_rails.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, predicate, choice + from agent_framework.llm.structured_output import parse_json_object import json @@ -43,13 +45,15 @@ class LLMGuardrailRail(Guardrail): f"Texto:\n{text[:12000]}" ) try: - raw = await self.llm.ainvoke( + raw = await decision_invoke(self.llm, [ {"role": "system", "content": "Responda apenas JSON válido, sem markdown."}, {"role": "user", "content": prompt}, ], temperature=0, fallback_max_tokens=600, + decision_point="guardrail_" + self.profile_name, + decision_questions=[predicate("allowed", "Can the ORIGINAL text be allowed UNCHANGED under the supplied guardrail policy? If sanitization or rewriting is required, return false."), choice("risk_level", ["none", "low", "medium", "high"])], profile_name=self.profile_name, component_name=self.profile_name, generation_name=f"llm.{self.profile_name}", diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc index a107a065..865c091c 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc index ff246bf0..f7036d9b 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/mcp_mapper.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc index 53a87620..ea18cc5d 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc index eb6deb85..56109ee3 100644 Binary files a/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/identity/__pycache__/resolver.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc index 1807483c..d501d6a1 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc index 6a632a9c..25632a3e 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/__pycache__/judge.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc index 22069715..8b6aac0a 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc index fce729e1..ebef89b6 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/__pycache__/llm_client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/llm_client.py b/libs/agent_framework/src/agent_framework/judges/calibrated/llm_client.py index 26f9b5fe..f282c6fc 100644 --- a/libs/agent_framework/src/agent_framework/judges/calibrated/llm_client.py +++ b/libs/agent_framework/src/agent_framework/judges/calibrated/llm_client.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, predicate, choice, score + from agent_framework.llm.structured_output import parse_json_object import json @@ -45,15 +47,23 @@ class CalibratedJudgeLLMClient: component = component_name or f'judge.{task.lower()}' generation = generation_name or f'llm.{component}' - raw = await self.llm.ainvoke( - [ - {'role': 'system', 'content': 'Responda apenas JSON válido, sem markdown.'}, - {'role': 'user', 'content': prompt}, - ], - profile_name=profile, - component_name=component, - generation_name=generation, - ) + messages = [ + {'role': 'system', 'content': 'Responda apenas JSON válido, sem markdown.'}, + {'role': 'user', 'content': prompt}, + ] + kwargs = dict(profile_name=profile, component_name=component, generation_name=generation) + if task == 'FALLBACK': + raw = await self.llm.ainvoke(messages, **kwargs) + else: + questions = [predicate('allowed', 'Under the supplied rubric, should the answer pass?')] + if task in {'CSI', 'RQLT'}: + questions.append(score('score')) + if task == 'CSI': + questions.append(choice('sentimento', ['Negativo', 'Neutro', 'Positivo'])) + raw = await decision_invoke( + self.llm, messages, decision_point=component, decision_group='judge_' + task.lower(), + decision_questions=questions, **kwargs, + ) return _parse_json(raw) def _build_prompt(self, task: str, payload: dict[str, Any]) -> str: diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc index 2ec04b11..0f0f5b12 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc index b7527e60..12a45094 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/aluc.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc index 9a409f13..dd6ff581 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/csi.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc index c3390974..337e6464 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/fallback.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc index de1c2322..b479d927 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/rqlt.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc index 58f0f6ca..94c0418a 100644 Binary files a/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/judges/calibrated/prompts/__pycache__/vctn.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/judges/judge.py b/libs/agent_framework/src/agent_framework/judges/judge.py index d4be8ade..6b7c3924 100644 --- a/libs/agent_framework/src/agent_framework/judges/judge.py +++ b/libs/agent_framework/src/agent_framework/judges/judge.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, score as decision_score, predicate + from agent_framework.llm.structured_output import parse_json_object import json @@ -349,11 +351,14 @@ class LLMJudge(CalibratedJudge): f'Contexto/evidência:\n{json.dumps(_safe_context(context), ensure_ascii=False)[: self.max_context_chars]}' ) try: - raw = await self.llm.ainvoke( + raw = await decision_invoke(self.llm, [ {'role': 'system', 'content': 'Responda apenas JSON válido, sem markdown.'}, {'role': 'user', 'content': prompt}, ], + decision_point="judge_" + self.name, + decision_questions=[decision_score("score"), predicate("passed")], + decision_render=lambda d: json.dumps({**d, "score": d["score"] / 10}), profile_name=self.profile_name, component_name=self.profile_name, generation_name=f"llm.{self.profile_name}", diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc index 575501df..0218e6c8 100644 Binary files a/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/llm/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc index 59ebf176..e37d6ef4 100644 Binary files a/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/llm/__pycache__/base.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc index 4425f211..9c3dd87f 100644 Binary files a/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/llm/__pycache__/profile_resolver.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc index 2ef52d57..503484f8 100644 Binary files a/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/llm/__pycache__/providers.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/structured_output.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/structured_output.cpython-313.pyc index f0c9dd83..3cb64ece 100644 Binary files a/libs/agent_framework/src/agent_framework/llm/__pycache__/structured_output.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/llm/__pycache__/structured_output.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/llm/__pycache__/types.cpython-313.pyc b/libs/agent_framework/src/agent_framework/llm/__pycache__/types.cpython-313.pyc index e2e30be5..53862680 100644 Binary files a/libs/agent_framework/src/agent_framework/llm/__pycache__/types.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/llm/__pycache__/types.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/llm/providers.py b/libs/agent_framework/src/agent_framework/llm/providers.py index eb15700f..c662abb3 100644 --- a/libs/agent_framework/src/agent_framework/llm/providers.py +++ b/libs/agent_framework/src/agent_framework/llm/providers.py @@ -187,7 +187,7 @@ def _validate_openai_base_url(base_url: str | None, *, provider: str) -> str: "Para OpenAI-compatible, use o endpoint terminando com /openai/v1." ) cleaned = cleaned.rstrip('/') - if '/openai/v1' not in cleaned: + if '/api.openai.com/v1' not in cleaned: raise RuntimeError( f"Endpoint inválido para LLM_PROVIDER={provider}: {cleaned}. " "OCI_GENAI_BASE_URL precisa conter/terminar com /openai/v1." diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc index 951ffa94..d731e187 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc index 98b54430..934164dd 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/client.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc index d83ce786..f4f78f7c 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc index 98616b24..64a7ee12 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/registry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc index b0f6c820..ab6def12 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc index d47c7d9d..04c7be78 100644 Binary files a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_router.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc index 8bb0c96a..99bb53ce 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc index 41a3c38c..1a7a4cc0 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_extractor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc index 9236ac0c..a86e6f3c 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_memory.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc index 2a984f32..5812fbd7 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc index af4743f7..3f7186a5 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/long_term_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc index b7092397..9fbcefc8 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/message_history.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc index 66b0a437..6e0ab129 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_memory.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc index 09749df7..bf104d68 100644 Binary files a/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/memory/__pycache__/summary_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc index 09129262..39372cb7 100644 Binary files a/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc index 385f333b..79e2ae78 100644 Binary files a/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/models/__pycache__/identity.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc b/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc index 1450b3c6..0ed8ac6e 100644 Binary files a/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/models/__pycache__/session.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc index 23477078..c9dd2ea9 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/code_mapper.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/code_mapper.cpython-313.pyc index 45c14f9f..3da42594 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/code_mapper.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/code_mapper.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc index 209d6224..2aa7a9d2 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/context.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc index 65e3a977..d23f135d 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/event_bus.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc index bec542e4..072f77ff 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/guardrail_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc index 5115402d..f9c9fba2 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/ic_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc index c1631a29..2c3ba847 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/judge_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc index c5341474..c919343f 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/langgraph_telemetry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc index 06486b4a..732d55a8 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/llm_advisors.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc index acfcfee6..8e76786b 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_contract.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc index 9a0b6d0b..a9d35837 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/noc_otel.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc index d70b0139..cf83357d 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/observer.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc index 4e40e8ba..c59a7a7e 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/otel.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/severity_policy.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/severity_policy.cpython-313.pyc new file mode 100644 index 00000000..4709466f Binary files /dev/null and b/libs/agent_framework/src/agent_framework/observability/__pycache__/severity_policy.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc index 8051bfe1..760da831 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/streaming_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc index d83b7767..4adef723 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/telemetry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc index ed16a563..b170f720 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/token_cost.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc index 48a7cf46..e73bf1a5 100644 Binary files a/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/observability/__pycache__/workflow_events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc index 983600bb..5c4bc2d7 100644 Binary files a/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/oci/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc b/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc index 412dea3c..6b3aa329 100644 Binary files a/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/oci/__pycache__/auth.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc index 1b0e5f16..88ee9ecb 100644 Binary files a/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc index e5661200..3f53e2e3 100644 Binary files a/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/oracle_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc index 839ae1fb..4a50682c 100644 Binary files a/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/persistence/__pycache__/sqlite_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc index 1520a9ee..bd1cec8d 100644 Binary files a/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/presentation/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc b/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc index 0a3c7f63..d0e75db8 100644 Binary files a/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/presentation/__pycache__/renderers.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc index feb23515..b24c8c6b 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc index 775e6dc7..ebf6ab41 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/embedding_provider.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc index d629751d..9fc47327 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/graph_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc index 5d5cb937..14df61d4 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/ingest.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc index 0aa1f224..2cd34627 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/rag_service.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc b/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc index eb077b66..f40a11a7 100644 Binary files a/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/rag/__pycache__/vector_store.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc index 641c1b4e..67f5c17e 100644 Binary files a/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/repositories/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc index 0ae4aa5a..9fe5c387 100644 Binary files a/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/repositories/__pycache__/session_repository.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc index 7d56f306..34455a93 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc index 0c65b288..36d54f00 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/config_loader.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc index d5cc794d..ad05f31b 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/continuity.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc index 13691fd7..5605ad22 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/enterprise_router.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc index 7e0193e7..3463822f 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/multi_intent.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/multi_intent.cpython-313.pyc index 03882ae1..fd2e381d 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/multi_intent.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/multi_intent.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/__pycache__/pending_topics.cpython-313.pyc b/libs/agent_framework/src/agent_framework/routing/__pycache__/pending_topics.cpython-313.pyc index 35316a30..78a8c874 100644 Binary files a/libs/agent_framework/src/agent_framework/routing/__pycache__/pending_topics.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/routing/__pycache__/pending_topics.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/routing/continuity.py b/libs/agent_framework/src/agent_framework/routing/continuity.py index 34d1cd6e..05f0fe23 100644 --- a/libs/agent_framework/src/agent_framework/routing/continuity.py +++ b/libs/agent_framework/src/agent_framework/routing/continuity.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, choice + from agent_framework.llm.structured_output import parse_json_object import json @@ -293,11 +295,13 @@ class SemanticRouteContinuity: "recent_history": history, "current_message": text, } - answer = await self.llm.ainvoke( + answer = await decision_invoke(self.llm, [ {"role": "system", "content": system}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, ], + decision_point='route_continuity', + decision_questions=[choice("decision", ["CONTINUE", "ROUTE", "HUMAN_HANDOFF", "END_SESSION"])], profile_name=self.profile_name, component_name="route_continuity", generation_name="llm.route_continuity", diff --git a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py index 91f68466..a44a6da2 100644 --- a/libs/agent_framework/src/agent_framework/routing/enterprise_router.py +++ b/libs/agent_framework/src/agent_framework/routing/enterprise_router.py @@ -1,5 +1,8 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, choice +from agent_framework.decisions.client import multi_intent as decide_multi_intent + import json import logging import re @@ -228,11 +231,14 @@ class EnterpriseRouter: f"opções permitidas, sem explicação adicional: {json.dumps(protocol_options, ensure_ascii=False)}." ) try: - answer = await self.llm.ainvoke( + answer = await decision_invoke(self.llm, [ {"role": "system", "content": rendered + protocol}, {"role": "user", "content": str(text or "")}, ], + decision_point=("transaction_confirmation" if component_name == "transaction.confirmation" else "workflow_expected_input"), + decision_questions=[choice("value", protocol_options + ["__unmatched__"], "Choose an allowed option only when the ENTIRE message fits the supplied policy; otherwise __unmatched__.")], + decision_render=lambda d: d["value"], profile_name=profile_name, component_name=component_name, generation_name=generation_name, @@ -314,11 +320,14 @@ class EnterpriseRouter: "à pergunta pendente, admitindo somente cortesia sem nova demanda." ) try: - answer = await self.llm.ainvoke( + answer = await decision_invoke(self.llm, [ {"role": "system", "content": prompt}, {"role": "user", "content": str(text or "")}, ], + decision_point='workflow_reentry', + decision_questions=[choice("decision", ["REENTER", "RESUME"])], + decision_render=lambda d: d["decision"], profile_name="router", component_name="workflow.expected_input.precedence", generation_name="workflow.expected_input.contextual_reentry_precedence", @@ -1145,13 +1154,15 @@ class EnterpriseRouter: "válido: {\"relevance\":\"RELEVANT|NOT_RELEVANT\",\"confidence\":0.0,\"reason\":\"...\"}." ) try: - answer = await self.llm.ainvoke( + answer = await decision_invoke(self.llm, [ {"role": "system", "content": system}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, ], temperature=0.0, fallback_max_tokens=256, + decision_point='transaction_parameter_relevance', + decision_questions=[choice("relevance", ["RELEVANT", "NOT_RELEVANT"])], profile_name="router", component_name="router.transaction_parameter_relevance", generation_name="llm.transaction_parameter_relevance", @@ -1428,13 +1439,16 @@ class EnterpriseRouter: "session_context": (state.get("context") or {}).get("session", {}), } try: - answer = await self.llm.ainvoke( + answer = await decision_invoke(self.llm, [ {"role": "system", "content": system}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)}, ], temperature=0.0, fallback_max_tokens=512, + decision_point='transaction_intent_shift', + decision_questions=[choice("decision", ["CONTINUE", "ABANDON"] if abandon_only else ["CONTINUE", "REPLACE", "SHIFT", "ABANDON"]), choice("intent", [i.name for i in allowed] + ["__none__"], "Select the explicitly requested target intent, or __none__ for no new target.")], + decision_render=lambda d: json.dumps({**d, "intent": None if d["intent"] == "__none__" else d["intent"], "agent": self._agent_for_intent(d["intent"])}), profile_name="router", component_name="router", generation_name="llm.transaction_intent_shift", @@ -1774,11 +1788,13 @@ class EnterpriseRouter: "tool_name": active_tx.get("tool_name"), } if active_tx else None), } - answer = await self.llm.ainvoke( + answer = await decide_multi_intent(self.llm, [ {"role": "system", "content": system}, {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}, ], + intents=allowed, + text=text, temperature=0.0, fallback_max_tokens=768, profile_name="router", @@ -1830,13 +1846,15 @@ class EnterpriseRouter: "session_context": ({} if state.get("operational_context_reset") else (state.get("context") or {}).get("session", {})), "transaction_context": transaction_context, } - answer = await self.llm.ainvoke( + answer = await decision_invoke(self.llm, [ {"role": "system", "content": system}, {"role": "user", "content": json.dumps(user, ensure_ascii=False)}, ], temperature=0.0, fallback_max_tokens=512, + decision_point='router', + decision_questions=[choice("intent", [i.name for i in allowed] + ["fallback"])], profile_name="router", component_name="router", generation_name="llm.router", diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc index 91b827a3..29b6f099 100644 Binary files a/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc index e75e35c4..07cfc717 100644 Binary files a/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc index 38810d18..f3950447 100644 Binary files a/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py b/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py index 902470a8..85649da0 100644 --- a/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py +++ b/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py @@ -1,5 +1,7 @@ from __future__ import annotations +from agent_framework.decisions import invoke as decision_invoke, selected, choice + from agent_framework.localization import confirmation_decision from agent_framework.llm.structured_output import parse_json_object @@ -168,6 +170,31 @@ async def reconcile_transaction_parameters( source = "current" fields[name] = {"decision": decision, "value": flat_value, "source": source} + try: + if selected("transaction_parameter_reconciliation"): + # Candidate values/provenance are never invented by the decision adapter. + async def original_candidates(messages, **kwargs): + return json.dumps({"fields": fields}, ensure_ascii=False) + + class CandidateProvider: + usage_repository = getattr(llm, "usage_repository", None) + telemetry = getattr(llm, "telemetry", None) + ainvoke = staticmethod(original_candidates) + + reviewed = await decision_invoke( + CandidateProvider(), + [{"role": "user", "content": prompt + "\nExtracted candidates: " + json.dumps(fields, ensure_ascii=False)}], + decision_point="transaction_parameter_reconciliation", + decision_questions=[choice("field_" + str(n), ["resolved", "preserve", "clear", "unresolved"], + "Decide reconciliation for parameter " + name + ". resolved requires a non-empty extracted candidate; preserve requires an existing known value.") for n, name in enumerate(names)], + decision_render=lambda d: json.dumps({"fields": {name: {**(fields.get(name) or {}), "decision": d["field_" + str(n)]} for n, name in enumerate(names)}}), + ) + fields = json.loads(reviewed)["fields"] + + except Exception as exc: + logger.warning("transaction.parameter.reconciliation_decision_failed type=%s", type(exc).__name__) + return {"values": {}, "decisions": {}, "provenance": {}, "clear_fields": []} + values: dict[str, Any] = {} decisions: dict[str, str] = {} provenance: dict[str, str] = {} diff --git a/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc index 5ac50c81..aee3900b 100644 Binary files a/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/sse/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc b/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc index 11389d3a..48e1be36 100644 Binary files a/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/sse/__pycache__/events.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc index 486ab59d..103b01ea 100644 Binary files a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc index 35dd1f3e..35a37ad5 100644 Binary files a/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/supervisor/__pycache__/supervisor.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc index c5632dd8..3ee094a7 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/__init__.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc index 30324c3b..e7dd0c80 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/graph.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/input_contract.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/input_contract.cpython-313.pyc index ae9ed74c..222ae192 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/input_contract.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/input_contract.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc index 7f21b403..ad4ddf08 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/models.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc index 2d8a7ce8..f9a1fd31 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/registry.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc index b268a6c0..3f545e1a 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/repository.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc index 6b6fb967..564919f3 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/runtime.cpython-313.pyc differ diff --git a/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc b/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc index f71694fb..e8ab3399 100644 Binary files a/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc and b/libs/agent_framework/src/agent_framework/workflows/__pycache__/tool_executor.cpython-313.pyc differ diff --git a/tests/__pycache__/test_jev_choice_fix.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_jev_choice_fix.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 00000000..bc8362b4 Binary files /dev/null and b/tests/__pycache__/test_jev_choice_fix.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_jev_endpoint.cpython-313.pyc b/tests/__pycache__/test_jev_endpoint.cpython-313.pyc new file mode 100644 index 00000000..16a0c8cf Binary files /dev/null and b/tests/__pycache__/test_jev_endpoint.cpython-313.pyc differ diff --git a/tests/__pycache__/test_jev_typesafe_adapter.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_jev_typesafe_adapter.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 00000000..5a8c5a15 Binary files /dev/null and b/tests/__pycache__/test_jev_typesafe_adapter.cpython-313-pytest-9.0.2.pyc differ diff --git a/tests/test_jev_choice_fix.py b/tests/test_jev_choice_fix.py new file mode 100644 index 00000000..14c4e972 --- /dev/null +++ b/tests/test_jev_choice_fix.py @@ -0,0 +1,28 @@ +import pytest +from agent_framework.decisions.adapters.typesafe import build_request, normalize_answers, TypeSafeContractError +from agent_framework.decisions import client + + +def test_choice_wire_uses_verified_noul(): + questions = [client.choice('decision', ['CONTINUE','ROUTE','END_SESSION'])] + body = build_request('state', questions) + assert len(body['questions']) == 3 + assert all(x['type'] == 'noul' for x in body['questions'].values()) + answer = {'answers': {'__jev_q0_o0': {'noul': .12}, '__jev_q0_o1': {'noul': .94}, '__jev_q0_o2': {'noul': .07}}} + assert normalize_answers(answer, questions) == [{'name': 'decision', 'type': 'choice', 'choice': 'ROUTE', 'confidence': .94}] + + +def test_ambiguous_choice_fails_closed(): + questions = [client.choice('decision', ['CONTINUE','ROUTE'])] + with pytest.raises(TypeSafeContractError, match='AMBIGUOUS_DIRECT_CHOICE'): + normalize_answers({'answers': {'__jev_q0_o0': {'noul': .81}, '__jev_q0_o1': {'noul': .78}}}, questions) + + +def test_predicate_wire(): + questions = [client.predicate('allowed')] + assert normalize_answers({'answers': {'__jev_q0': {'noul': .9}}}, questions)[0]['probability'] == .9 + + +def test_score_fails_closed(): + with pytest.raises(TypeSafeContractError, match='UNSUPPORTED_DIRECT_SCORE'): + build_request('state', [client.score('score')]) diff --git a/tests/test_jev_decisions.py b/tests/test_jev_decisions.py new file mode 100644 index 00000000..10dc5d1f --- /dev/null +++ b/tests/test_jev_decisions.py @@ -0,0 +1,133 @@ +"""Offline JEV transport/contract tests; no live API key or network required.""" +import json +from unittest.mock import AsyncMock + +import pytest + +from agent_framework.decisions import client + + +@pytest.fixture(autouse=True) +def clean_environment(monkeypatch): + import os + for key in list(os.environ): + if key.startswith(('DECISION_', 'JEV_')) or key == 'AI_GATEWAY_API_KEY': + monkeypatch.delenv(key) + + +def answer(questions, *, confidence=.95): + return {'model': 'typesafe-ai/jev', 'answers': [ + {'name': q['name'], 'type': q['type'], **( + {'choice': q['choices'][0]['value'], 'confidence': confidence} if q['type'] == 'choice' else + {'probability': confidence} if q['type'] == 'predicate' else + {'score': 8.0, 'confidence': confidence})} for q in questions]} + + +@pytest.mark.asyncio +async def test_default_preserves_current_call_and_kwargs(monkeypatch): + current = AsyncMock(return_value='legacy response') + monkeypatch.setattr(client, '_post', lambda body: pytest.fail('JEV must not run')) + result = await client.invoke(type('LLM', (), {'ainvoke': current})(), [{'content': 'x'}], + decision_point='router', decision_questions=[], profile_name='router', temperature=0) + assert result == 'legacy response' + current.assert_awaited_once_with([{'content': 'x'}], profile_name='router', temperature=0) + + +@pytest.mark.asyncio +async def test_individual_selection_and_override(monkeypatch): + monkeypatch.setenv('DECISION_PROVIDER', 'jev') + monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'current') + assert not client.selected('router') + assert client.selected('route_continuity') + monkeypatch.setenv('DECISION_PROVIDER', 'current') + monkeypatch.setenv('DECISION_ROUTE_CONTINUITY_PROVIDER', 'jev') + assert client.selected('route_continuity') + assert not client.selected('router') + + +@pytest.mark.asyncio +async def test_typed_contract_and_shared_request(monkeypatch): + monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'jev') + requests = [] + def transport(body): + requests.append(body) + return answer(body['questions']) + monkeypatch.setattr(client, '_post', transport) + llm = type('LLM', (), {'ainvoke': AsyncMock()})() + result = json.loads(await client.invoke(llm, [{'content': 'context'}], decision_point='router', + decision_questions=[client.choice('intent', ['billing', 'orders']), client.predicate('allowed'), client.score('score')])) + assert result['intent'] == 'billing' and result['allowed'] is True and result['score'] == 8 + assert len(requests) == 1 and requests[0]['model'] == 'typesafe-ai/jev' + assert 'context' in requests[0]['input'] + llm.ainvoke.assert_not_awaited() + + +@pytest.mark.asyncio +@pytest.mark.parametrize('failure', ['network', 'unknown_choice', 'missing', 'duplicate', 'nan', 'low', 'no_confidence', 'wrong_type']) +async def test_invalid_or_uncertain_jev_falls_back_once(monkeypatch, failure): + monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'jev') + def transport(body): + if failure == 'network': raise TimeoutError() + response = answer(body['questions']) + a = response['answers'][0] + if failure == 'unknown_choice': a['choice'] = 'undeclared' + if failure == 'missing': response['answers'] = [] + if failure == 'duplicate': response['answers'] *= 2 + if failure == 'nan': a['confidence'] = float('nan') + if failure == 'low': a['confidence'] = .4 + if failure == 'no_confidence': a.pop('confidence') + if failure == 'wrong_type': a['type'] = 'predicate' + return response + monkeypatch.setattr(client, '_post', transport) + llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='legacy')})() + assert await client.invoke(llm, [], decision_point='router', decision_questions=[client.choice('intent', ['a'])]) == 'legacy' + llm.ainvoke.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_disabled_fallback_surfaces_failure(monkeypatch): + monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'jev') + monkeypatch.setenv('DECISION_FALLBACK_TO_CURRENT', 'false') + monkeypatch.setattr(client, '_post', lambda body: {'answers': []}) + llm = type('LLM', (), {'ainvoke': AsyncMock()})() + with pytest.raises(ValueError): + await client.invoke(llm, [], decision_point='router', decision_questions=[client.predicate('allowed')]) + llm.ainvoke.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_generative_bypass_even_with_global_jev(monkeypatch): + monkeypatch.setenv('DECISION_PROVIDER', 'jev') + monkeypatch.setattr(client, '_post', lambda body: pytest.fail('rewrite must use current')) + llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='rewritten')})() + assert await client.invoke(llm, [], decision_point='rewrite', decision_questions=[], decision_generative=True) == 'rewritten' + + +@pytest.mark.asyncio +async def test_multi_intent_preserves_order_literal_fragments(monkeypatch): + from types import SimpleNamespace + monkeypatch.setenv('DECISION_MULTI_INTENT_PROVIDER', 'jev') + candidates = {'operations': [{'source_text': 'pedido', 'intent': 'x'}, {'source_text': 'fatura', 'intent': 'y'}]} + llm = type('LLM', (), {'ainvoke': AsyncMock(return_value=json.dumps(candidates))})() + monkeypatch.setattr(client, '_post', lambda body: answer(body['questions'])) + result = json.loads(await client.multi_intent(llm, [], intents=[SimpleNamespace(name='billing')], text='pedido e fatura')) + assert [o['source_text'] for o in result['operations']] == ['pedido', 'fatura'] + llm.ainvoke.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_legacy_semantic_classifier_and_jev_are_independent(monkeypatch, tmp_path): + from types import SimpleNamespace + from agent_framework.routing.enterprise_router import EnterpriseRouter + config = tmp_path / 'routing.yaml' + config.write_text('router: {fallback_agent: fallback}\nintents: []\n') + llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='NO')})() + router = EnterpriseRouter(SimpleNamespace(ROUTING_CONFIG_PATH=str(config), ENABLE_LLM_ROUTER=False, ENABLE_ROUTE_STICKINESS=False), llm=llm) + expected = {'allowed_values': ['YES', 'NO'], 'semantic_classifier': {'enabled': True, 'prompt': 'Use meaning: {{ user_input }}'}} + monkeypatch.setenv('DECISION_WORKFLOW_EXPECTED_INPUT_PROVIDER', 'jev') + monkeypatch.setattr(client, '_post', lambda body: answer(body['questions'])) + value, _ = await router._classify_expected_input_semantically(text='yes', expected_input=expected, pause_prompt='Confirm?') + assert value == 'YES' + value, _ = await router._classify_expected_input_semantically(text='no', expected_input=expected, pause_prompt='Confirm?', component_name='transaction.confirmation') + assert value == 'NO' + llm.ainvoke.assert_awaited_once() diff --git a/tests/test_jev_endpoint.py b/tests/test_jev_endpoint.py new file mode 100644 index 00000000..93d29e9b --- /dev/null +++ b/tests/test_jev_endpoint.py @@ -0,0 +1,33 @@ +"""Offline regression tests: python -m unittest discover -s tests -p test_jev_endpoint.py""" +import importlib.util +import os +from pathlib import Path +import unittest +from unittest.mock import patch + +CLIENT = Path(__file__).resolve().parents[1] / 'libs/agent_framework/src/agent_framework/decisions/client.py' +spec = importlib.util.spec_from_file_location('jev_client_patch', CLIENT) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + +class EndpointTests(unittest.TestCase): + def test_default(self): + with patch.dict(os.environ, {}, clear=True): + self.assertEqual(module._jev_endpoint(), 'https://ai-gateway.vercel.sh/v1/decisions') + + def test_typesafe_endpoint(self): + with patch.dict(os.environ, {'JEV_ENDPOINT': 'https://api.typesafe.ai/v1/systemone'}, clear=True): + self.assertEqual(module._jev_endpoint(), 'https://api.typesafe.ai/v1/systemone') + + def test_reject_untrusted_host(self): + with patch.dict(os.environ, {'JEV_ENDPOINT': 'https://evil.example/v1/decisions'}, clear=True): + with self.assertRaises(ValueError): + module._jev_endpoint() + + def test_reject_http(self): + with patch.dict(os.environ, {'JEV_ENDPOINT': 'http://api.typesafe.ai/v1/systemone'}, clear=True): + with self.assertRaises(ValueError): + module._jev_endpoint() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_jev_transport_standalone.py b/tests/test_jev_transport_standalone.py new file mode 100644 index 00000000..4b0d1f6b --- /dev/null +++ b/tests/test_jev_transport_standalone.py @@ -0,0 +1,210 @@ +"""Run without project dependencies: python3 tests/test_jev_transport_standalone.py.""" +import ast +import asyncio +import importlib.util +import json +import os +import logging +from types import SimpleNamespace +from pathlib import Path +import unittest +from unittest.mock import AsyncMock, patch + +ROOT = Path(__file__).resolve().parents[1] +MODULE = ROOT / 'libs/agent_framework/src/agent_framework/decisions/client.py' +spec = importlib.util.spec_from_file_location('jev_transport', MODULE) +client = importlib.util.module_from_spec(spec) +spec.loader.exec_module(client) + + +def response(questions, confidence=.95): + return {'model': 'typesafe-ai/jev', 'answers': [ + {'type': q['type'], 'name': q['name'], **( + {'choice': q['choices'][0]['value'], 'confidence': confidence} if q['type'] == 'choice' else + {'probability': confidence} if q['type'] == 'predicate' else + {'score': 8, 'confidence': confidence})} for q in questions]} + + +class DecisionTransportTests(unittest.IsolatedAsyncioTestCase): + def setUp(self): + self.env = patch.dict(os.environ, {k: v for k, v in os.environ.items() if not k.startswith(('DECISION_', 'JEV_'))}, clear=True) + self.env.start() + self.addCleanup(self.env.stop) + self.llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='legacy')})() + + async def call(self, **kwargs): + return await client.invoke(self.llm, [{'content': 'test context'}], decision_point='router', + decision_questions=[client.choice('intent', ['billing', 'orders'])], **kwargs) + + async def test_default_passes_original_kwargs_without_network(self): + with patch.object(client, '_post', side_effect=AssertionError('network')): + self.assertEqual(await self.call(profile_name='router'), 'legacy') + self.llm.ainvoke.assert_awaited_once_with([{'content': 'test context'}], profile_name='router') + + async def test_individual_override(self): + os.environ.update(DECISION_PROVIDER='jev', DECISION_ROUTER_PROVIDER='current') + self.assertFalse(client.selected('router')) + self.assertTrue(client.selected('route_continuity')) + os.environ.update(DECISION_PROVIDER='current', DECISION_ROUTER_PROVIDER='jev') + self.assertTrue(client.selected('router')) + self.assertFalse(client.selected('route_continuity')) + os.environ['DECISION_JUDGE_ALUC_PROVIDER'] = 'jev' + self.assertTrue(client.selected('judge.groundedness', 'judge_aluc')) + os.environ['DECISION_JUDGE_GROUNDEDNESS_PROVIDER'] = 'current' + self.assertFalse(client.selected('judge.groundedness', 'judge_aluc')) + + async def test_typed_choices_predicates_scores_shared_request(self): + os.environ['DECISION_ROUTER_PROVIDER'] = 'jev' + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])) as post: + result = json.loads(await client.invoke(self.llm, [], decision_point='router', + decision_questions=[client.choice('intent', ['billing']), client.predicate('allowed'), client.score('score')])) + self.assertEqual((result['intent'], result['allowed'], result['score']), ('billing', True, 8)) + self.assertEqual(post.call_count, 1) + self.llm.ainvoke.assert_not_awaited() + + async def test_invalid_response_falls_back_once(self): + os.environ['DECISION_ROUTER_PROVIDER'] = 'jev' + for failure in ['missing', 'duplicate', 'choice', 'nan', 'missing_confidence', 'low_confidence', 'type', 'network']: + with self.subTest(failure=failure): + self.llm.ainvoke.reset_mock() + def transport(body): + if failure == 'network': raise TimeoutError() + r = response(body['questions']); a = r['answers'][0] + if failure == 'missing': r['answers'] = [] + if failure == 'duplicate': r['answers'] *= 2 + if failure == 'choice': a['choice'] = 'unknown' + if failure == 'nan': a['confidence'] = float('nan') + if failure == 'missing_confidence': a.pop('confidence') + if failure == 'low_confidence': a['confidence'] = .1 + if failure == 'type': a['type'] = 'predicate' + return r + with patch.object(client, '_post', side_effect=transport): + self.assertEqual(await self.call(), 'legacy') + self.llm.ainvoke.assert_awaited_once() + + async def test_no_fallback_surfaces_error(self): + os.environ.update(DECISION_ROUTER_PROVIDER='jev', DECISION_FALLBACK_TO_CURRENT='false') + with patch.object(client, '_post', return_value={'answers': []}): + with self.assertRaises(ValueError): await self.call() + self.llm.ainvoke.assert_not_awaited() + + async def test_generative_bypass(self): + os.environ['DECISION_PROVIDER'] = 'jev' + with patch.object(client, '_post', side_effect=AssertionError('network')): + self.assertEqual(await self.call(decision_generative=True), 'legacy') + + async def test_invalid_provider_rejected(self): + os.environ['DECISION_ROUTER_PROVIDER'] = 'typo' + with self.assertRaises(ValueError): await self.call() + + async def test_custom_renderer_failure_falls_back(self): + os.environ['DECISION_ROUTER_PROVIDER'] = 'jev' + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])): + self.assertEqual(await self.call(decision_render=lambda d: d['missing']), 'legacy') + + async def test_threshold_override(self): + os.environ.update(DECISION_ROUTER_PROVIDER='jev', DECISION_ROUTER_MIN_CONFIDENCE='.99') + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])): + self.assertEqual(await self.call(), 'legacy') + + async def test_predicate_rejects_bool_and_out_of_range(self): + for probability in [True, -1, 2, float('inf'), None]: + with self.subTest(probability=probability): + with patch.object(client, '_post', return_value={'answers': [{'type': 'predicate', 'name': 'allowed', 'probability': probability}]}): + with self.assertRaises(ValueError): + await client.evaluate('guardrail', [], [client.predicate('allowed')]) + + async def test_transport_requires_key_before_request(self): + os.environ.pop('AI_GATEWAY_API_KEY', None) + with patch.object(client.urllib.request, 'urlopen') as request: + with self.assertRaises(ValueError): client._post({}) + request.assert_not_called() + + async def test_transport_endpoint_auth_and_timeout(self): + os.environ.update(AI_GATEWAY_API_KEY='test-key', JEV_TIMEOUT_SECONDS='3') + import io + with patch.object(client.urllib.request, 'urlopen', return_value=io.StringIO('{"answers": []}')) as request: + self.assertEqual(client._post({'model': 'typesafe-ai/jev'}), {'answers': []}) + req = request.call_args.args[0] + self.assertEqual(req.full_url, 'https://ai-gateway.vercel.sh/v1/decisions') + self.assertEqual(req.get_header('Authorization'), 'Bearer test-key') + self.assertEqual(request.call_args.kwargs['timeout'], 3) + + async def test_integrated_modules_compile(self): + for path in (ROOT / 'libs/agent_framework/src/agent_framework').rglob('*.py'): + with self.subTest(path=path): ast.parse(path.read_text(), filename=str(path)) + + + +def load_functions(relative_path, names, namespace): + """Execute actual function bodies in isolation from unavailable optional dependencies.""" + path = ROOT / 'libs/agent_framework/src/agent_framework' / relative_path + tree = ast.parse(path.read_text()) + nodes = [node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names] + module = ast.Module(body=[ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0)] + nodes, type_ignores=[]) + exec(compile(ast.fix_missing_locations(module), str(path), 'exec'), namespace) + return namespace + + +class DecisionCallsiteTests(unittest.IsolatedAsyncioTestCase): + setUp = DecisionTransportTests.setUp + + async def test_actual_continuity_classifier_jev(self): + os.environ['DECISION_ROUTE_CONTINUITY_PROVIDER'] = 'jev' + ns = load_functions('routing/continuity.py', {'_classify'}, { + 'json': json, 'decision_invoke': client.invoke, 'choice': client.choice, + 'ContinuityEvaluation': SimpleNamespace}) + self_obj = SimpleNamespace(llm=self.llm, profile_name='route_continuity', + _agent_capabilities=lambda intents: {'billing': ['invoices']}, + _compact_history=lambda history: history, _parse_json=json.loads) + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])): + result = await ns['_classify'](self_obj, {}, text='continue', active_agent='billing', intents=[]) + self.assertEqual(result.decision, 'CONTINUE') + self.assertGreater(result.confidence, .9) + self.llm.ainvoke.assert_not_awaited() + + async def test_actual_router_derives_agent_from_catalog(self): + os.environ['DECISION_ROUTER_PROVIDER'] = 'jev' + ns = load_functions('routing/enterprise_router.py', {'_route_by_llm'}, { + 'json': json, 'decision_invoke': client.invoke, 'choice': client.choice, 'RouteDecision': SimpleNamespace}) + intent = SimpleNamespace(enabled=True, name='invoice', agent='billing', description='Invoice', examples=[], mcp_tools=[], domain='telecom') + self_obj = SimpleNamespace(llm=self.llm, intents=[intent], fallback_agent='fallback', _parse_json=json.loads, + _agent_for_intent=lambda name: 'billing', _domain_for_intent=lambda name: 'telecom', _tools_for_intent=lambda name: ['read_invoice']) + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])): + result = await ns['_route_by_llm'](self_obj, 'invoice', {}) + self.assertEqual((result.intent, result.agent, result.mcp_tools), ('invoice', 'billing', ['read_invoice'])) + + async def test_actual_reconciliation_preserves_candidate_values(self): + os.environ['DECISION_TRANSACTION_PARAMETER_RECONCILIATION_PROVIDER'] = 'jev' + ns = load_functions('runtime/transaction_parameters.py', {'reconcile_transaction_parameters', '_response_text', '_coerce'}, { + 'json': json, 'logger': logging.getLogger('test'), 'parse_json_object': json.loads, + 'decision_invoke': client.invoke, 'selected': client.selected, 'choice': client.choice, + '_EMPTY_VALUES': (None, '', {}, [])}) + self.llm.ainvoke.return_value = json.dumps({'fields': { + 'id': {'value': '42', 'decision': 'resolved', 'source': 'current'}, + 'empty': {'value': None, 'decision': 'unresolved', 'source': ''}}}) + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])): + result = await ns['reconcile_transaction_parameters'](self.llm, text='42', tool_name='read', parameter_names=['id', 'empty']) + self.assertEqual(result['values'], {'id': '42'}) + self.assertEqual(result['decisions']['empty'], 'unresolved') + self.llm.ainvoke.assert_awaited_once() + + async def test_actual_calibrated_judge_and_generative_fallback(self): + os.environ['DECISION_JUDGE_RQLT_PROVIDER'] = 'jev' + ns = load_functions('judges/calibrated/llm_client.py', {'classify'}, { + 'decision_invoke': client.invoke, 'predicate': client.predicate, 'score': client.score, + 'choice': client.choice, '_parse_json': json.loads}) + self_obj = SimpleNamespace(llm=self.llm, default_profile='judge', _build_prompt=lambda task, payload: 'quality rubric') + with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])): + result = await ns['classify'](self_obj, 'RQLT', {}) + self.assertTrue(result['allowed']) + self.assertEqual(result['score'], 8) + self.llm.ainvoke.assert_not_awaited() + self.llm.ainvoke.return_value = '{"text":"fallback response"}' + os.environ['DECISION_PROVIDER'] = 'jev' + with patch.object(client, '_post', side_effect=AssertionError('no generative JEV')): + self.assertEqual(await ns['classify'](self_obj, 'FALLBACK', {}), {'text': 'fallback response'}) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/test_jev_typesafe_adapter.py b/tests/test_jev_typesafe_adapter.py new file mode 100644 index 00000000..074b3ef5 --- /dev/null +++ b/tests/test_jev_typesafe_adapter.py @@ -0,0 +1,176 @@ +import json +from unittest.mock import AsyncMock +from urllib.error import HTTPError +import pytest +from agent_framework.decisions import client +from agent_framework.decisions.adapters.typesafe import build_request, normalize_answers, TypeSafeContractError + +@pytest.mark.asyncio +async def test_default_provider_unchanged(monkeypatch): + monkeypatch.delenv("DECISION_PROVIDER", raising=False) + llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="legacy")})() + monkeypatch.setattr(client, "_post", lambda body: pytest.fail("JEV transport called")) + assert await client.invoke(llm, [{"content": "x"}], decision_point="route_continuity", + decision_questions=[], profile_name="route_continuity") == "legacy" + llm.ainvoke.assert_awaited_once_with([{"content": "x"}], profile_name="route_continuity") + +@pytest.mark.asyncio +async def test_typesafe_noul(monkeypatch): + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_MODEL", "jev-latest") + captured = [] + def transport(body): + captured.append(body) + return {"model": "jev-1.14.0", "answers": {"__jev_q0": {"type": "noul", "noul": .98}}} + monkeypatch.setattr(client, "_post", transport) + result = await client.evaluate("guardrail.pinj", [], [client.predicate("allowed")]) + assert result["allowed"] is True and result["confidence"] == pytest.approx(.98) + assert captured[0]["questions"]["__jev_q0"]["type"] == "noul" + assert "state" in captured[0] and "input" not in captured[0] + +@pytest.mark.asyncio +async def test_vercel_contract_not_modified(monkeypatch): + monkeypatch.setenv("JEV_ENDPOINT", "https://ai-gateway.vercel.sh/v1/decisions") + captured = [] + def transport(body): + captured.append(body) + return {"answers": [{"name": "allowed", "type": "predicate", "probability": .95}]} + monkeypatch.setattr(client, "_post", transport) + result = await client.evaluate("guardrail.pinj", [], [client.predicate("allowed")]) + assert result["allowed"] is True + assert "input" in captured[0] and "state" not in captured[0] + assert isinstance(captured[0]["questions"], list) + +@pytest.mark.asyncio +async def test_invalid_direct_response_falls_back_once(monkeypatch): + monkeypatch.setenv("DECISION_PROVIDER", "jev") + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setattr(client, "_post", lambda body: {"answers": {}}) + llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})() + assert await client.invoke(llm, [], decision_point="guardrail.revprec", + decision_questions=[client.predicate("allowed")]) == "original" + llm.ainvoke.assert_awaited_once() + +@pytest.mark.asyncio +async def test_http_error_diagnostic_without_body(monkeypatch, caplog): + monkeypatch.setenv("DECISION_PROVIDER", "jev") + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + def reject(body): + raise HTTPError("https://api.typesafe.ai/v1/systemone", 400, "SECRET_BODY", {}, None) + monkeypatch.setattr(client, "_post", reject) + llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})() + assert await client.invoke(llm, [], decision_point="route_continuity", + decision_questions=[client.choice("decision", ["CONTINUE", "ROUTE"])]) == "original" + assert "http_status=400" in caplog.text + assert "SECRET_BODY" not in caplog.text + +@pytest.mark.asyncio +async def test_fallback_disabled_raises(monkeypatch): + monkeypatch.setenv("DECISION_PROVIDER", "jev") + monkeypatch.setenv("DECISION_FALLBACK_TO_CURRENT", "false") + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setattr(client, "_post", lambda body: {"answers": {}}) + llm = type("LLM", (), {"ainvoke": AsyncMock()})() + with pytest.raises(TypeSafeContractError): + await client.invoke(llm, [], decision_point="guardrail.pinj", decision_questions=[client.predicate("allowed")]) + llm.ainvoke.assert_not_awaited() + +@pytest.mark.asyncio +async def test_http_422_schema_diagnostic_redacts_user_data(monkeypatch, caplog): + import io + monkeypatch.setenv("DECISION_PROVIDER", "jev") + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + def reject(body): + error = {"detail": [{"type": "missing", "loc": ["body", "questions", "private-user-input", "choices"], + "msg": "SECRET_PASSWORD", "input": "SECRET_PASSWORD"}]} + raise HTTPError("https://api.typesafe.ai/v1/systemone", 422, "invalid", {}, + io.BytesIO(json.dumps(error).encode())) + monkeypatch.setattr(client, "_post", reject) + llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})() + assert await client.invoke(llm, [], decision_point="route_continuity", + decision_questions=[client.choice("decision", ["CONTINUE", "ROUTE"])]) == "original" + assert "http_status=422" in caplog.text + assert "http_schema=missing:body.questions.choices" in caplog.text + assert "SECRET_PASSWORD" not in caplog.text + assert "private-user-input" not in caplog.text + +@pytest.mark.asyncio +async def test_low_confidence_has_specific_error_code(monkeypatch, caplog): + monkeypatch.setenv("DECISION_PROVIDER", "jev") + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.9") + monkeypatch.setattr(client, "_post", lambda body: {"answers": {"__jev_q0": {"type": "noul", "noul": 0.6}}}) + llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})() + assert await client.invoke(llm, [], decision_point="guardrail.revprec", + decision_questions=[client.predicate("allowed")]) == "original" + assert "error_code=CONFIDENCE_BELOW_THRESHOLD" in caplog.text + +@pytest.mark.asyncio +async def test_revprec_noul_probability_not_misread_as_confidence(monkeypatch, caplog): + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.7") + monkeypatch.setattr(client, "_post", lambda body: {"answers": {"__jev_q0": {"type": "noul", "noul": .61}}}) + with pytest.raises(ValueError, match="CONFIDENCE_BELOW_THRESHOLD"): + await client.evaluate("guardrail.revprec", [], [client.predicate("allowed")]) + assert "('allowed', 0.61)" in caplog.text + assert "confidence=0.6100" in caplog.text + +@pytest.mark.asyncio +async def test_revprec_negative_predicate_confidence(monkeypatch): + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.7") + monkeypatch.setattr(client, "_post", lambda body: {"answers": {"__jev_q0": {"type": "noul", "noul": .12}}}) + result = await client.evaluate("guardrail.revprec", [], [client.predicate("allowed")]) + assert result["allowed"] is False + assert result["confidence"] == pytest.approx(.88) + +@pytest.mark.asyncio +async def test_opt_in_choice_diagnostics_rejected_and_fallback(monkeypatch, caplog): + import logging + monkeypatch.setenv("DECISION_PROVIDER", "jev") + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_DECISION_DIAGNOSTICS", "true") + monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.60") + monkeypatch.setattr(client, "_post", lambda body: {"model": "jev-1.14.0", "answers": { + "__jev_q0_o0": {"type": "noul", "noul": .56}, + "__jev_q0_o1": {"type": "noul", "noul": .35}}}) + llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="current-result")})() + with caplog.at_level(logging.INFO, logger="agent_framework.decisions"): + output = await client.invoke(llm, [{"content": "PRIVATE_CUSTOMER_TEXT"}], + decision_point="transaction_intent_shift", + decision_questions=[client.choice("decision", ["CONTINUE", "SHIFT"])]) + assert output == "current-result" + assert "jev.choice.diagnostics" in caplog.text + assert '"option_probabilities":[0.56,0.35]' in caplog.text + assert "status=rejected_low_confidence" in caplog.text + assert "PRIVATE_CUSTOMER_TEXT" not in caplog.text + +@pytest.mark.asyncio +async def test_opt_in_choice_diagnostics_accepted(monkeypatch, caplog): + import logging + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_DECISION_DIAGNOSTICS", "true") + monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.60") + monkeypatch.setattr(client, "_post", lambda body: {"model": "jev-1.14.0", "answers": { + "__jev_q0_o0": {"type": "noul", "noul": .2}, + "__jev_q0_o1": {"type": "noul", "noul": .88}}}) + with caplog.at_level(logging.INFO, logger="agent_framework.decisions"): + result = await client.evaluate("transaction_intent_shift", [], + [client.choice("decision", ["CONTINUE", "SHIFT"])]) + assert result["decision"] == "SHIFT" + assert result["confidence"] == pytest.approx(.88) + assert '"selected_option_index":1' in caplog.text + assert "status=accepted" in caplog.text + +@pytest.mark.asyncio +async def test_diagnostics_off_by_default(monkeypatch, caplog): + import logging + monkeypatch.delenv("JEV_DECISION_DIAGNOSTICS", raising=False) + monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone") + monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.6") + monkeypatch.setattr(client, "_post", lambda body: {"answers": { + "__jev_q0_o0": {"noul": .9}, "__jev_q0_o1": {"noul": .1}}}) + with caplog.at_level(logging.INFO, logger="agent_framework.decisions"): + await client.evaluate("transaction_intent_shift", [], [client.choice("decision", ["A", "B"])]) + assert "jev.choice.diagnostics" not in caplog.text + assert "jev.decision.diagnostics" not in caplog.text diff --git a/tests/test_jev_typesafe_direct.py b/tests/test_jev_typesafe_direct.py new file mode 100644 index 00000000..741884fc --- /dev/null +++ b/tests/test_jev_typesafe_direct.py @@ -0,0 +1,28 @@ +import json +from unittest.mock import patch +import pytest +from agent_framework.decisions import client + +@pytest.mark.asyncio +async def test_direct_predicate_contract(monkeypatch): + monkeypatch.setenv('JEV_ENDPOINT','https://api.typesafe.ai/v1/systemone') + monkeypatch.setenv('JEV_MODEL','jev-latest') + monkeypatch.setenv('JEV_MIN_CONFIDENCE','0.7') + captured=[] + def fake_post(body): + captured.append(body) + return {'model':'jev-latest','answers':{'billing':{'type':'noul','noul':0.98}}} + monkeypatch.setattr(client,'_post',fake_post) + result=await client.evaluate('test',[{'role':'user','content':'fatura'}],[client.predicate('billing')]) + assert result['billing'] is True + assert result['confidence'] == pytest.approx(.98) + assert captured[0]['model']=='jev-latest' + assert 'state' in captured[0] and 'input' not in captured[0] + assert captured[0]['questions']['billing']['type']=='noul' + +@pytest.mark.asyncio +async def test_direct_missing_answers_rejected(monkeypatch): + monkeypatch.setenv('JEV_ENDPOINT','https://api.typesafe.ai/v1/systemone') + monkeypatch.setattr(client,'_post',lambda body:{'answers':{}}) + with pytest.raises(ValueError): + await client.evaluate('test',[],[client.predicate('billing')])