mirror of
https://github.com/mcloh/decision-models-benchmark.git
synced 2026-11-06 06:14:11 +00:00
setup environment
This commit is contained in:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.venv/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Pesos e caches de modelos (ficam no bucket `artefatos`)
|
||||||
|
models/
|
||||||
|
.cache/
|
||||||
|
*.safetensors
|
||||||
|
*.bin
|
||||||
|
*.gguf
|
||||||
|
|
||||||
|
# Dados brutos e anotados não são versionados (ficam nos buckets OCI)
|
||||||
|
data/raw/*
|
||||||
|
data/annotated/*
|
||||||
|
!data/**/.gitkeep
|
||||||
|
|
||||||
|
# Resultados de execução (ficam no bucket `resultados`)
|
||||||
|
runs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Credenciais
|
||||||
|
.secrets/
|
||||||
|
.oci/
|
||||||
|
*.pem
|
||||||
|
.env
|
||||||
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Benchmark de modelos de decisão em PT-BR para roteamento em orquestradores multiagente
|
||||||
|
|
||||||
|
Benchmark reprodutível de modelos de decisão *open source*, do tipo "System 1" e semelhantes ao JEV (`POST /v1/systemone`), aplicados ao **roteamento e à orquestração de enunciados de usuário** (*user utterances*) num sistema multiagente com **A2A Gateway** e **MCP Gateway**.
|
||||||
|
|
||||||
|
O modelo de decisão é avaliado como um componente do **Agente Orquestrador**. A cada turno, ele toma decisões rápidas e estruturadas: identifica a intenção, escolhe o agente de destino, decide entre continuar, alternar ou retomar uma sessão e decide quando é preciso desambiguar. Essas decisões substituem ou complementam chamadas a LLMs generalistas.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Comparar os candidatos em duas dimensões, nas mesmas condições:
|
||||||
|
|
||||||
|
- **Acurácia e calibração** das decisões de roteamento em PT-BR, incluindo casos ambíguos, adversariais e com várias intenções.
|
||||||
|
- **Desempenho operacional**: latência por turno (p50/p95/p99), vazão, memória, tempo de carga e custo por mil decisões, em CPU e GPU na OCI.
|
||||||
|
|
||||||
|
O resultado é um relatório técnico com uma recomendação. Todos os resultados podem ser reproduzidos a partir dos artefatos versionados.
|
||||||
|
|
||||||
|
## Participantes
|
||||||
|
|
||||||
|
| Candidato | Artefato |
|
||||||
|
|-------------|----------------------------------------------|
|
||||||
|
| Laya | `convaiinnovations/laya-multilingual` |
|
||||||
|
| SemIf | `SemIf-OpenJev` com `Qwen/Qwen3.5-4B` (modo `direct`) |
|
||||||
|
| GLiNER | `fastino/GLiNER2.5-multi-Decide` (`classify_text`) |
|
||||||
|
| Rizzo Flow | `rizzoaiacademy/rizzo-flow` (4B) |
|
||||||
|
|
||||||
|
Cada candidato é exposto por um adaptador local com o mesmo contrato JEV (`POST /v1/systemone`). Assim, o *harness* não sabe qual modelo está respondendo.
|
||||||
|
|
||||||
|
## Documentação
|
||||||
|
|
||||||
|
| Documento | Conteúdo |
|
||||||
|
|-----------|----------|
|
||||||
|
| [docs/01-contexto-arquitetural.md](docs/01-contexto-arquitetural.md) | Arquitetura de referência do orquestrador e onde o modelo de decisão atua |
|
||||||
|
| [docs/02-desenho-experimental.md](docs/02-desenho-experimental.md) | Tarefas de decisão, formato dos dados, partições, métricas e regras de isolamento |
|
||||||
|
| [docs/03-plano-de-tarefas.md](docs/03-plano-de-tarefas.md) | As 78 microtarefas organizadas em fases, com dependências e adaptações ao contexto de roteamento |
|
||||||
|
| [docs/04-questoes-em-aberto.md](docs/04-questoes-em-aberto.md) | Decisões de projeto (Q1–Q13) e os mecanismos automatizados que as aplicam |
|
||||||
|
| [config/benchmark.yaml](config/benchmark.yaml) | Parâmetros e critérios pré-registrados, congelados na tarefa 62 |
|
||||||
|
|
||||||
|
## Estrutura do repositório
|
||||||
|
|
||||||
|
```
|
||||||
|
config/ Configuração pré-registrada (benchmark.yaml)
|
||||||
|
infra/oci/ Compartment, tags, orçamento, IAM, VCN, buckets, Data Science (tarefas 1–10, 17, 78)
|
||||||
|
env/ Imagem de contêiner e manifesto de versões (11–12)
|
||||||
|
contracts/jev/ Contrato JEV jev-compat-v1: esquemas, mapeamento de tipos, fixtures de conformidade (36–41)
|
||||||
|
data/taxonomy/ Taxonomia de intenções/agentes e definição das tarefas de decisão (20–23)
|
||||||
|
data/raw/ Enunciados coletados e anonimizados — não versionar dados sensíveis (24–25)
|
||||||
|
data/annotated/ Anotações, adjudicação e referência (29–32)
|
||||||
|
data/splits/ Partições dev / calibração / teste congeladas (33–35)
|
||||||
|
adapters/<modelo>/ Adaptadores JEV locais por candidato (42–51)
|
||||||
|
harness/ Execução, controle de seeds/lotes/truncamento, coleta de medições (52–66)
|
||||||
|
analysis/ Métricas, cortes, análise de erros e gráficos (67–74)
|
||||||
|
reports/ Relatório técnico e anexos de reprodução (75–76)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Princípios
|
||||||
|
|
||||||
|
1. **Isolamento de rede.** Na inferência, os modelos usam só artefatos locais com hash conhecido. Qualquer tentativa de download faz o job falhar.
|
||||||
|
2. **Contrato único.** Todos os candidatos recebem a mesma requisição JEV e devolvem o mesmo formato de resposta.
|
||||||
|
3. **Teste final congelado.** Prompts, opções, pesos e limiares não mudam depois que o conjunto de teste final é aberto.
|
||||||
|
4. **Partição por conversa.** Turnos de uma mesma conversa ou caso nunca aparecem em mais de uma partição.
|
||||||
|
5. **Reprodutibilidade.** Versões, seeds, hashes, configurações e comandos ficam registrados em manifestos anexados ao relatório.
|
||||||
0
adapters/gliner/.gitkeep
Normal file
0
adapters/gliner/.gitkeep
Normal file
0
adapters/laya/.gitkeep
Normal file
0
adapters/laya/.gitkeep
Normal file
0
adapters/rizzo_flow/.gitkeep
Normal file
0
adapters/rizzo_flow/.gitkeep
Normal file
0
adapters/semif/.gitkeep
Normal file
0
adapters/semif/.gitkeep
Normal file
0
analysis/.gitkeep
Normal file
0
analysis/.gitkeep
Normal file
101
config/benchmark.yaml
Normal file
101
config/benchmark.yaml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Configuração pré-registrada do benchmark.
|
||||||
|
# Decisões e justificativas: docs/04-questoes-em-aberto.md
|
||||||
|
# Este arquivo é congelado com hash no marco da tarefa 62.
|
||||||
|
|
||||||
|
contract:
|
||||||
|
version: jev-compat-v1 # Q1
|
||||||
|
endpoint: /v1/systemone
|
||||||
|
max_options: 20 # Q2: inclui sem_correspondencia
|
||||||
|
fallback_option: sem_correspondencia
|
||||||
|
require_zero_output_tokens: true
|
||||||
|
|
||||||
|
candidates:
|
||||||
|
laya:
|
||||||
|
model: convaiinnovations/laya-multilingual
|
||||||
|
revision: null # preenchido pela aquisição (tarefa 13)
|
||||||
|
declared_context: 1024
|
||||||
|
semif:
|
||||||
|
model: Qwen/Qwen3.5-4B
|
||||||
|
runtime: SemIf-OpenJev
|
||||||
|
mode: direct
|
||||||
|
revision: null
|
||||||
|
declared_context: 8192
|
||||||
|
gliner:
|
||||||
|
model: fastino/GLiNER2.5-multi-Decide
|
||||||
|
interface: classify_text
|
||||||
|
revision: null
|
||||||
|
declared_context: null # medido (Q4)
|
||||||
|
rizzo_flow:
|
||||||
|
model: rizzoaiacademy/rizzo-flow
|
||||||
|
prompt_version: spark-decisions-v3
|
||||||
|
revision: null
|
||||||
|
declared_context: 2048
|
||||||
|
|
||||||
|
baseline: # Q9: fora do ranking principal
|
||||||
|
enabled: true
|
||||||
|
model: null # fixado no manifesto
|
||||||
|
network: gateway_only
|
||||||
|
|
||||||
|
licenses: # Q4
|
||||||
|
allowlist: [apache-2.0, mit]
|
||||||
|
|
||||||
|
network: # Q3
|
||||||
|
egress_probe_hosts: [huggingface.co, pypi.org]
|
||||||
|
require_probe_failure: true
|
||||||
|
|
||||||
|
input: # Q8
|
||||||
|
state_turns_candidates: [0, 1, 2, 4]
|
||||||
|
fit_ratio_min: 0.95
|
||||||
|
token_counter: most_verbose_tokenizer
|
||||||
|
truncation: drop_oldest_turns_then_tail_truncate_utterance
|
||||||
|
|
||||||
|
execution:
|
||||||
|
seed: 20261007
|
||||||
|
batch_sweep: [1, 2, 4, 8, 16, 32] # Q6
|
||||||
|
production_batch: auto # maior lote com p95 <= slo
|
||||||
|
repetitions: 3
|
||||||
|
cpu: # Q11
|
||||||
|
timeout_ms: 10000
|
||||||
|
max_timeout_rate: 0.05
|
||||||
|
gpu:
|
||||||
|
timeout_ms: 2000
|
||||||
|
unavailable_retries: 3
|
||||||
|
|
||||||
|
calibration: # Q10
|
||||||
|
method: temperature_scaling
|
||||||
|
per: [candidate, task]
|
||||||
|
cv_folds: 5
|
||||||
|
accept_if: ece_decreases
|
||||||
|
|
||||||
|
criteria: # Q5
|
||||||
|
latency_gpu_batch1:
|
||||||
|
p95_ms_per_decision: 150
|
||||||
|
p95_ms_per_turn: 300
|
||||||
|
accuracy:
|
||||||
|
tasks: [D1, D2]
|
||||||
|
non_inferiority_margin_pp: 3.0
|
||||||
|
ci: 0.95
|
||||||
|
bootstrap_unit: group_id
|
||||||
|
fallback_macro_f1_min: 0.80
|
||||||
|
risk:
|
||||||
|
max_confident_wrong_route_rate: 0.02
|
||||||
|
min_coverage: 0.80
|
||||||
|
|
||||||
|
sample_size: # Q12
|
||||||
|
task: D1
|
||||||
|
mde_pp: 3.0
|
||||||
|
alpha: 0.05
|
||||||
|
power: 0.80
|
||||||
|
expected_discordance: 0.15
|
||||||
|
prior_icc: 0.05
|
||||||
|
min_test_decisions: 1300
|
||||||
|
|
||||||
|
data: # Q7
|
||||||
|
sources: [real, synthetic]
|
||||||
|
human_audit_rate: 0.05
|
||||||
|
dedup: minhash_lsh
|
||||||
|
|
||||||
|
budget: # Q13: em aberto (cota interna); não provisionado
|
||||||
|
enabled: false
|
||||||
|
contingency_factor: 1.3
|
||||||
|
alerts_pct: [50, 80, 100]
|
||||||
0
contracts/jev/fixtures/.gitkeep
Normal file
0
contracts/jev/fixtures/.gitkeep
Normal file
79
contracts/jev/schemas/request.schema.json
Normal file
79
contracts/jev/schemas/request.schema.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "jev-compat-v1/request.schema.json",
|
||||||
|
"title": "JEV systemone request (jev-compat-v1)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["model", "state", "questions"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"model": {"type": "string", "minLength": 1},
|
||||||
|
"state": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string", "minLength": 1},
|
||||||
|
{"type": "object", "minProperties": 1},
|
||||||
|
{"type": "array", "minItems": 1}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"questions": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"additionalProperties": {"$ref": "#/$defs/question"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"question": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "instructions", "criteria"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"enum": ["choice", "noul", "score"]},
|
||||||
|
"instructions": {"type": "string", "minLength": 1},
|
||||||
|
"criteria": {}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {"properties": {"type": {"const": "choice"}}},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"criteria": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 2,
|
||||||
|
"maxProperties": 20,
|
||||||
|
"additionalProperties": {"type": "string", "minLength": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {"properties": {"type": {"const": "noul"}}},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"criteria": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["true", "false"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"true": {"type": "string", "minLength": 1},
|
||||||
|
"false": {"type": "string", "minLength": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {"properties": {"type": {"const": "score"}}},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"criteria": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 20,
|
||||||
|
"items": {"type": "string", "minLength": 1}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
contracts/jev/schemas/response.schema.json
Normal file
74
contracts/jev/schemas/response.schema.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "jev-compat-v1/response.schema.json",
|
||||||
|
"title": "JEV systemone response (jev-compat-v1)",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["model", "answers", "usage"],
|
||||||
|
"properties": {
|
||||||
|
"model": {"type": "string", "minLength": 1},
|
||||||
|
"answers": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"additionalProperties": {
|
||||||
|
"oneOf": [
|
||||||
|
{"$ref": "#/$defs/choice"},
|
||||||
|
{"$ref": "#/$defs/noul"},
|
||||||
|
{"$ref": "#/$defs/score"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usage": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["input_tokens", "output_tokens"],
|
||||||
|
"properties": {
|
||||||
|
"input_tokens": {"type": "integer", "minimum": 0},
|
||||||
|
"output_tokens": {"const": 0}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Extensão do benchmark (tarefa 48); ignorada por clientes JEV.",
|
||||||
|
"properties": {
|
||||||
|
"latency_ms": {"type": "number", "minimum": 0},
|
||||||
|
"model_id": {"type": "string"},
|
||||||
|
"run_id": {"type": "string"},
|
||||||
|
"adapter_version": {"type": "string"},
|
||||||
|
"probabilities_source": {"enum": ["native", "normalized_scores", "logprobs", "none"]},
|
||||||
|
"truncated": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"prob": {"type": "number", "minimum": 0, "maximum": 1},
|
||||||
|
"probs": {"type": "object", "minProperties": 1, "additionalProperties": {"$ref": "#/$defs/prob"}},
|
||||||
|
"choice": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "choice", "probabilities"],
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "choice"},
|
||||||
|
"choice": {"type": "string"},
|
||||||
|
"confidence": {"$ref": "#/$defs/prob"},
|
||||||
|
"probabilities": {"$ref": "#/$defs/probs"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"noul": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "noul"],
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "noul"},
|
||||||
|
"noul": {"$ref": "#/$defs/prob"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "score", "probabilities", "legend"],
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "score"},
|
||||||
|
"score": {"type": "number", "minimum": 0},
|
||||||
|
"confidence": {"$ref": "#/$defs/prob"},
|
||||||
|
"probabilities": {"$ref": "#/$defs/probs"},
|
||||||
|
"legend": {"type": "object", "additionalProperties": {"type": "string"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
data/annotated/.gitkeep
Normal file
0
data/annotated/.gitkeep
Normal file
0
data/raw/.gitkeep
Normal file
0
data/raw/.gitkeep
Normal file
0
data/splits/.gitkeep
Normal file
0
data/splits/.gitkeep
Normal file
0
data/taxonomy/.gitkeep
Normal file
0
data/taxonomy/.gitkeep
Normal file
74
docs/01-contexto-arquitetural.md
Normal file
74
docs/01-contexto-arquitetural.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# 01 — Contexto arquitetural
|
||||||
|
|
||||||
|
Este documento descreve a arquitetura de referência em termos genéricos e só com os conceitos necessários ao experimento. O objetivo é deixar claro **onde** o modelo de decisão atua e **quais informações de contexto** ele recebe.
|
||||||
|
|
||||||
|
## Componentes da arquitetura de referência
|
||||||
|
|
||||||
|
| Componente | Papel | Relevância para o benchmark |
|
||||||
|
|------------|-------|-----------------------------|
|
||||||
|
| **Adaptador de canal** | Integra canais de interação (chat, mensageria, voz) e normaliza a entrada | Define o formato do enunciado que chega ao orquestrador |
|
||||||
|
| **Agente Orquestrador** | Supervisiona cada turno: identifica a intenção, aplica políticas, controla sessões e seleciona o agente | **Hospeda o modelo de decisão avaliado** |
|
||||||
|
| **A2A Gateway** | Registra agentes (cards) e faz autenticação, mediação de tarefas A2A, ciclo de vida de tarefas (IDs, estado, retomada, cancelamento), além de erros e timeouts | Fonte do catálogo de agentes, que vira o conjunto de opções de destino |
|
||||||
|
| **A2A Adapter** | Permite que agentes sem A2A nativo conversem com o orquestrador de forma padronizada | Fora de escopo, exceto como opção de destino |
|
||||||
|
| **Agentes especializados** | Agentes de domínio, um agente de propósito geral (por exemplo, com busca web) e outros | São as **classes de destino** do roteamento |
|
||||||
|
| **MCP Gateway / Server** | Expõe ferramentas e fontes de dados com contrato padronizado, *discovery*, autenticação e limites | Fonte de dados para pré-requisitos e para o **serviço de elegibilidade de intenções** |
|
||||||
|
| **Serviço de elegibilidade de intenções** | A partir das intenções mapeadas, identifica as que são elegíveis para um dado usuário | **Restringe as opções** apresentadas ao modelo de decisão |
|
||||||
|
| **LLM Gateway** | Centraliza o acesso a modelos (LLM, busca e modelos System 1), além de políticas, limites e telemetria | Ponto de integração natural do modelo de decisão em produção |
|
||||||
|
| **Memória curta** | Estado operacional da conversa e de cada sessão de agente (`conversation_id`, `session_id`/`task_id`, `agent_id`) | Fornece o **estado** (`state`) da requisição de decisão |
|
||||||
|
| **Memória longa** | Fatos e preferências duradouros entre sessões | Contexto opcional; avaliado só se fizer parte do `state` |
|
||||||
|
| **Base de personalização** | Atributos declarados e inferidos do usuário, com acesso governado | Fora de escopo no primeiro ciclo |
|
||||||
|
| **Observabilidade** | OpenTelemetry Collector, Pub/Sub, armazenamento de traces e logs, dashboards, avaliação de LLM e data lake | Modelo de telemetria usado pelo harness (latência, `run_id`, `model_id`) |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
U[Usuário] --> CA[Adaptador de canal]
|
||||||
|
CA --> ORQ
|
||||||
|
subgraph ORQ[Agente Orquestrador]
|
||||||
|
DM[[Modelo de decisão<br/>POST /v1/systemone]]
|
||||||
|
end
|
||||||
|
MC[(Memória curta)] --> ORQ
|
||||||
|
ML[(Memória longa)] --> ORQ
|
||||||
|
ORQ <--> MCP[MCP Gateway] --> EL[Serviço de elegibilidade]
|
||||||
|
ORQ <--> LLMG[LLM Gateway]
|
||||||
|
ORQ <--> A2A[A2A Gateway]
|
||||||
|
A2A <--> AG1[Agente especializado A]
|
||||||
|
A2A <--> AG2[Agente especializado B]
|
||||||
|
A2A <--> AGG[Agente de propósito geral]
|
||||||
|
ORQ -.-> OT[OTel Collector]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fluxo de um turno no orquestrador
|
||||||
|
|
||||||
|
1. Registrar a mensagem recebida, carregar o estado da conversa e carregar os atributos de contexto.
|
||||||
|
2. Aplicar os **guardrails de entrada**.
|
||||||
|
3. **Classificar e analisar a intenção.**
|
||||||
|
4. Seguir um destes ramos:
|
||||||
|
- **rotear para o agente ativo** via A2A;
|
||||||
|
- **executar os pré-requisitos** e **iniciar uma sessão com um novo agente** via A2A;
|
||||||
|
- **preparar perguntas de pré-requisito**;
|
||||||
|
- **preparar uma pergunta de desambiguação de intenção**.
|
||||||
|
5. Validar o retorno do agente.
|
||||||
|
6. Preparar a resposta e aplicar os **guardrails de saída**.
|
||||||
|
7. Registrar a mensagem e os estados.
|
||||||
|
|
||||||
|
## Pontos de decisão avaliados
|
||||||
|
|
||||||
|
O benchmark avalia os pontos de decisão em que um modelo System 1 substitui ou antecede uma chamada a um LLM generalista. Cada ponto vira uma **tarefa de decisão** com pergunta, opções e regra de decisão próprias. A definição completa está em [02-desenho-experimental.md](02-desenho-experimental.md).
|
||||||
|
|
||||||
|
| ID | Ponto de decisão | Etapa do fluxo | Tipo JEV previsto |
|
||||||
|
|----|------------------|----------------|-------------------|
|
||||||
|
| D1 | Classificação de intenção entre as intenções **elegíveis** | Passo 3 | `choice` |
|
||||||
|
| D2 | Continuidade de sessão: manter o agente ativo, retomar uma sessão suspensa, iniciar um novo agente ou desambiguar | Passo 4 | `choice` |
|
||||||
|
| D3 | Necessidade de desambiguação: a intenção está clara o suficiente para rotear? | Passos 3–4 | `noul` ou `score`, a confirmar (ver questões em aberto) |
|
||||||
|
| D4 | Várias intenções no mesmo enunciado: quais intenções estão presentes | Passo 3 | várias decisões sobre o mesmo texto (tarefa 65) |
|
||||||
|
| D5 | *(opcional)* Guardrail de entrada: dentro do escopo, fora do escopo ou bloqueado | Passo 2 | `choice` |
|
||||||
|
|
||||||
|
Ficam **fora de escopo**: geração de resposta, validação do retorno dos agentes, guardrails de saída, extração de atributos de personalização e o próprio protocolo A2A/MCP. A elegibilidade de intenções é **simulada**: o conjunto de opções elegíveis já vem pronto em cada exemplo.
|
||||||
|
|
||||||
|
## Requisitos não funcionais que derivam da arquitetura
|
||||||
|
|
||||||
|
- **Latência por turno.** O modelo roda em todos os turnos, no caminho síncrono do usuário. Por isso, p95 e p99 com lote 1 são a métrica operacional principal.
|
||||||
|
- **Contexto curto.** A decisão usa o enunciado e um resumo do estado (agente ativo, sessões abertas, últimos turnos), não o histórico completo.
|
||||||
|
- **Opções dinâmicas.** O conjunto de opções muda a cada usuário e a cada turno, por causa da elegibilidade e das sessões abertas. Os modelos precisam aceitar opções descritas em tempo de inferência, sem retreino.
|
||||||
|
- **Abstenção útil.** Quando a confiança é baixa, o melhor caminho é desambiguar, não errar a rota. A calibração e a acurácia seletiva são métricas de primeira classe.
|
||||||
|
- **Telemetria.** Cada decisão gera um registro compatível com OpenTelemetry (`run_id`, `model_id`, `latency_ms`, versão do adaptador).
|
||||||
131
docs/02-desenho-experimental.md
Normal file
131
docs/02-desenho-experimental.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# 02 — Desenho experimental
|
||||||
|
|
||||||
|
## 1. Hipóteses
|
||||||
|
|
||||||
|
- **H1 (acurácia).** Pelo menos um candidato atinge, em PT-BR, uma acurácia de roteamento (D1/D2) compatível com o uso no orquestrador. O critério é a não inferioridade em relação ao baseline LLM (margem de 3 p.p.); ver [04, Q5](04-questoes-em-aberto.md#q5--critérios-de-sucesso-pré-registrados).
|
||||||
|
- **H2 (calibração).** As probabilidades e a confiança devolvidas permitem uma política de abstenção (encaminhar para desambiguação) que melhora a acurácia seletiva sem derrubar demais a cobertura.
|
||||||
|
- **H3 (desempenho).** O candidato cabe no orçamento de latência por turno (p95 com lote 1) com um custo por mil decisões menor que o de uma chamada a um LLM generalista.
|
||||||
|
|
||||||
|
## 2. Tarefas de decisão
|
||||||
|
|
||||||
|
Cada tarefa tem **pergunta**, **opções com descrição** e **regra de decisão** (tarefa 21). No primeiro ciclo, cada questão tem **no máximo 20 opções**, incluindo `sem_correspondencia` (tarefas 22–23).
|
||||||
|
|
||||||
|
### D1 — Classificação de intenção
|
||||||
|
|
||||||
|
- **Pergunta:** "Qual intenção do usuário este enunciado expressa?"
|
||||||
|
- **Opções:** as intenções elegíveis do exemplo, de 2 a 19, mais `sem_correspondencia`. Cada opção tem uma descrição curta, do mesmo jeito que viria do catálogo de agentes ou intenções.
|
||||||
|
- **Regra:** escolher a intenção principal que o próximo agente precisa atender. Se nenhuma se aplica, escolher `sem_correspondencia`.
|
||||||
|
|
||||||
|
### D2 — Continuidade de sessão
|
||||||
|
|
||||||
|
- **Pergunta:** "Como o orquestrador deve tratar este turno, considerando o estado da conversa?"
|
||||||
|
- **Opções:** `continuar_agente_ativo`, `retomar_sessao:<agent_id>` (uma opção para cada sessão suspensa), `iniciar_novo_agente`, `desambiguar`.
|
||||||
|
- **Regra:** `continuar_agente_ativo` quando o enunciado dá sequência à tarefa em curso, mesmo que seja curto ("sim", "o segundo", "pode ser"). `retomar_sessao` quando o usuário volta a um assunto anterior. `iniciar_novo_agente` quando aparece uma intenção nova. `desambiguar` quando não dá para decidir com segurança.
|
||||||
|
|
||||||
|
### D3 — Necessidade de desambiguação
|
||||||
|
|
||||||
|
- **Pergunta:** "O enunciado tem informação suficiente para rotear sem perguntar ao usuário?"
|
||||||
|
- **Tipo:** `noul` (P(sim) = "dá para rotear sem perguntar").
|
||||||
|
- **Uso:** comparar a abstenção explícita (D3) com a abstenção derivada da confiança de D1 (tarefa 60).
|
||||||
|
|
||||||
|
### D4 — Várias intenções
|
||||||
|
|
||||||
|
- Aplica-se só a enunciados anotados com mais de uma intenção.
|
||||||
|
- Executa várias perguntas `noul` sobre o mesmo texto, uma por intenção candidata ("O enunciado pede X?"), na mesma requisição JEV (tarefa 65).
|
||||||
|
- Métrica: F1 por conjunto de intenções e custo total, que soma as latências.
|
||||||
|
|
||||||
|
### D5 — Guardrail de entrada *(opcional)*
|
||||||
|
|
||||||
|
- Opções: `no_escopo`, `fora_do_escopo`, `requer_bloqueio`. Só entra se houver dados anotados suficientes.
|
||||||
|
|
||||||
|
## 3. Formato canônico de entrada (tarefa 36)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "conv-000123-t04-D2",
|
||||||
|
"group_id": "conv-000123",
|
||||||
|
"task": "D2",
|
||||||
|
"state": {
|
||||||
|
"active_agent": "agente_a",
|
||||||
|
"suspended_sessions": ["agente_b"],
|
||||||
|
"recent_turns": [
|
||||||
|
{"role": "user", "text": "..."},
|
||||||
|
{"role": "agent", "agent_id": "agente_a", "text": "..."}
|
||||||
|
],
|
||||||
|
"eligible_intents": ["intencao_1", "intencao_2"]
|
||||||
|
},
|
||||||
|
"utterance": "...",
|
||||||
|
"question": "Como o orquestrador deve tratar este turno?",
|
||||||
|
"question_type": "choice",
|
||||||
|
"options": {
|
||||||
|
"continuar_agente_ativo": "...",
|
||||||
|
"retomar_sessao:agente_b": "...",
|
||||||
|
"iniciar_novo_agente": "...",
|
||||||
|
"desambiguar": "..."
|
||||||
|
},
|
||||||
|
"gold_label": "retomar_sessao:agente_b",
|
||||||
|
"metadata": {
|
||||||
|
"taxonomy_version": "v0.1",
|
||||||
|
"domain": "...",
|
||||||
|
"length_bucket": "curto",
|
||||||
|
"ambiguity": "media",
|
||||||
|
"turn_index": 4,
|
||||||
|
"adversarial": ["abreviacao"],
|
||||||
|
"n_options": 4,
|
||||||
|
"n_tokens": {"laya": 0, "semif": 0, "gliner": 0, "rizzo_flow": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conversão para JEV.** `question` vira `questions.<task>.instructions` e `options` vira `criteria` (contrato `jev-compat-v1`, em [contracts/jev/schemas/](../contracts/jev/schemas/)). O `state` enviado é sempre o **texto serializado** descrito abaixo, nunca o objeto estruturado.
|
||||||
|
|
||||||
|
**Serialização do estado.** Alguns candidatos aceitam só texto, e não `state` estruturado. Por isso, uma **única regra de serialização** de `state` + `utterance` em texto vale para todos os adaptadores (um bloco "Contexto:" com o agente ativo, as sessões suspensas e os N últimos turnos, seguido de "Mensagem:"; N calculado como em [04, Q8](04-questoes-em-aberto.md#q8--turnos-no-state-e-orçamento-de-tokens)). A regra é versionada junto com a política de truncamento (tarefa 53). Um adaptador não pode usar informação de `state` que não esteja nessa serialização.
|
||||||
|
|
||||||
|
## 4. Dados
|
||||||
|
|
||||||
|
- **Unidade de amostragem:** conversa (`group_id`). Cada turno anotado gera um ou mais exemplos (um por tarefa de decisão).
|
||||||
|
- **Fontes:** enunciados representativos do domínio de destino, anonimizados (tarefas 24–25). Se houver dados sintéticos, eles ficam identificados em `metadata` e são reportados em separado.
|
||||||
|
- **Estratificação (tarefa 26):** domínio, comprimento, ambiguidade, classe esperada, posição do turno e presença de agente ativo ou de sessões suspensas.
|
||||||
|
- **Adversariais (tarefa 27):** abreviações, erros de ortografia, regionalismos, enunciados muito curtos e dependentes de contexto ("sim", "o outro"), várias intenções, troca de assunto no meio da conversa, retorno a um assunto anterior e pedidos fora de escopo.
|
||||||
|
- **Anotação (tarefas 29–32):** dois anotadores independentes e um terceiro para adjudicação. Para cada exemplo se registram o rótulo, a justificativa e a versão da taxonomia. A concordância (kappa de Cohen ou alfa de Krippendorff) vai para o relatório.
|
||||||
|
- **Partições (tarefas 33–35):** desenvolvimento, calibração e teste final, separadas por `group_id`. O teste final é congelado (hash registrado) antes de qualquer ajuste.
|
||||||
|
|
||||||
|
## 5. Protocolo de execução
|
||||||
|
|
||||||
|
| Etapa | Partição | O que pode mudar | Tarefas |
|
||||||
|
|-------|----------|------------------|---------|
|
||||||
|
| Conformidade e sanidade | fixtures | adaptadores | 41, 49–51 |
|
||||||
|
| Piloto CPU/GPU (50 ex.) | dev | só parâmetros operacionais | 55–58 |
|
||||||
|
| Desenvolvimento | dev | nada além do operacional; sem calibração | 59 |
|
||||||
|
| Calibração | calibração | limiares de confiança, regra de abstenção, calibradores por candidato | 60–61 |
|
||||||
|
| **Congelamento** | — | **nada** daqui em diante | 62 |
|
||||||
|
| Teste final | teste | — | 63–66 |
|
||||||
|
|
||||||
|
- Seeds, ordem dos exemplos, lote, precisão numérica (fp32, bf16 ou fp16 por candidato, registrada) e limite de tokens fixos por execução (tarefa 52).
|
||||||
|
- As medições de desempenho são feitas depois do aquecimento, com no mínimo três execuções independentes (tarefa 66).
|
||||||
|
- Cenários de execução: CPU com lote 1, GPU com lote 1 e GPU com lote de produção (escolhido automaticamente, ver Q6). O lote 1 representa o caminho síncrono do orquestrador; o lote maior mede a vazão.
|
||||||
|
|
||||||
|
## 6. Métricas
|
||||||
|
|
||||||
|
**Qualidade (tarefas 67–68)**
|
||||||
|
- Accuracy, macro F1, precision e recall por classe, matriz de confusão.
|
||||||
|
- Taxa de **rota errada com confiança alta** (erro que não seria desambiguado): é a métrica de maior impacto operacional.
|
||||||
|
- Brier score, ECE, curva cobertura × acurácia seletiva e AURC, calculados sobre `max(probabilities)` calibrado. A `confidence` nativa do JEV não é calibrada e só é registrada.
|
||||||
|
- D4: F1 por conjunto de intenções.
|
||||||
|
|
||||||
|
**Desempenho (tarefas 69–70)**
|
||||||
|
- Latência p50, p95 e p99 por decisão e por turno (soma das decisões do turno).
|
||||||
|
- Vazão, uso de GPU, RAM, VRAM, tempo de carga, tempo de aquecimento e armazenamento dos artefatos.
|
||||||
|
- Custo estimado por mil decisões em cada cenário, a partir do preço do *shape* OCI e da vazão medida.
|
||||||
|
|
||||||
|
**Cortes (tarefa 71):** tarefa de decisão, número de opções, comprimento, ambiguidade, posição do turno, com ou sem agente ativo e categoria adversarial.
|
||||||
|
|
||||||
|
**Análise de erros (tarefas 72–73):** taxonomia, contexto insuficiente, ambiguidade, linguagem, truncamento, instrução ou desempenho do modelo.
|
||||||
|
|
||||||
|
## 7. Ameaças à validade
|
||||||
|
|
||||||
|
- **Vantagem de formato.** Cada modelo pode ter sido treinado com um estilo de prompt diferente. Por isso há um contrato único e uma serialização única, e os desvios ficam documentados por adaptador.
|
||||||
|
- **Vazamento entre partições.** É controlado pela partição por `group_id` e pelo congelamento com hash.
|
||||||
|
- **Truncamento desigual.** Todos usam a mesma política, baseada no menor contexto efetivo, e a contagem de tokens de cada entrada é registrada.
|
||||||
|
- **Representatividade.** Dados sintéticos e reais são reportados em separado.
|
||||||
|
- **Elegibilidade simulada.** Em produção, o conjunto de opções vem de um serviço. Aqui ele é fixado por exemplo.
|
||||||
175
docs/03-plano-de-tarefas.md
Normal file
175
docs/03-plano-de-tarefas.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# 03 — Plano de tarefas
|
||||||
|
|
||||||
|
As 78 microtarefas mantêm a numeração original. As linhas **↳** registram a adaptação de cada tarefa ao contexto de roteamento e orquestração ou uma dependência importante. As referências *Qn* apontam para as decisões de projeto em [04-questoes-em-aberto.md](04-questoes-em-aberto.md).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
F0[F0 Infra OCI] --> F1[F1 Ambiente e artefatos]
|
||||||
|
F2[F2 Tarefas de decisão] --> F3[F3 Dados e anotação]
|
||||||
|
F2 --> F4[F4 Contrato e adaptadores]
|
||||||
|
F1 --> F4
|
||||||
|
F3 --> F5[F5 Controle de execução]
|
||||||
|
F4 --> F5
|
||||||
|
F5 --> F6[F6 Piloto]
|
||||||
|
F6 --> F7[F7 Dev e calibração]
|
||||||
|
F7 --> F8[F8 Teste final]
|
||||||
|
F8 --> F9[F9 Métricas e análise]
|
||||||
|
F9 --> F10[F10 Relatório e encerramento]
|
||||||
|
```
|
||||||
|
|
||||||
|
As fases F0–F1 (infra) e F2–F3 (dados) podem andar em paralelo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F0 — Infraestrutura OCI
|
||||||
|
|
||||||
|
- [x] **1.** Criar um compartment OCI dedicado ao benchmark.
|
||||||
|
- ↳ `root/<compartimento-pai>/decision-models`. Recursos em `us-chicago-1`. OCIDs em `.secrets/artefatos-OCI.md` (fora do Git).
|
||||||
|
- [ ] **2.** Aplicar as tags `projeto=benchmark-decisoes-ptbr`, `ambiente=benchmark` e `centro_custo=<definir>` aos recursos do projeto. *(Q13)*
|
||||||
|
- ↳ Parcial: `projeto` e `ambiente` aplicadas como tags livres por `infra/oci/provision.py`. Falta `centro_custo`.
|
||||||
|
- [ ] **3.** Criar um orçamento para o compartment.
|
||||||
|
- ↳ **Em aberto por decisão (2026-10-07):** o uso está coberto por cota interna de engenharia. Não será criado no primeiro ciclo.
|
||||||
|
- [ ] **4.** Configurar alertas de orçamento em 50%, 80% e 100% do valor aprovado.
|
||||||
|
- ↳ Em aberto, porque depende da tarefa 3.
|
||||||
|
- [x] **5.** Criar um projeto no OCI Data Science.
|
||||||
|
- ↳ `benchmark-decisoes-ptbr`.
|
||||||
|
- [ ] **6.** Criar buckets no Object Storage para `entrada`, `artefatos`, `resultados` e `logs-imutaveis`.
|
||||||
|
- ↳ Criados com o prefixo `dmb-` (nomes de bucket são únicos no namespace), privados e com versionamento.
|
||||||
|
- ↳ Pendente: a regra de retenção (WORM) de `dmb-logs-imutaveis`, que depende do prazo de retenção (tarefa 77).
|
||||||
|
- [x] **7.** Definir políticas IAM de menor privilégio para leitura dos dados, escrita dos resultados e execução dos jobs.
|
||||||
|
- ↳ Sem acesso de administrador à tenancy, as políticas ficam no próprio compartment e não usam grupos nem dynamic groups.
|
||||||
|
- ↳ `dmb-usuario-manager`: o usuário (filtrado por `request.user.id`) é *manager* das famílias data-science, object, virtual-network, logging, repos e generative-ai.
|
||||||
|
- ↳ `dmb-jobs-menor-privilegio`: os jobs (`request.principal.type='datasciencejob'`) leem `dmb-entrada`/`dmb-artefatos`, escrevem em `dmb-resultados`/`dmb-logs-imutaveis`, usam logs e repos. Inclui a política de serviço do Data Science para a VCN.
|
||||||
|
- [x] **8.** Definir uma VCN privada para os jobs, com acesso ao Object Storage por Service Gateway.
|
||||||
|
- ↳ `dmb-vcn` 10.20.0.0/16, sub-rede privada `dmb-subnet-jobs`. A rota e a security list só permitem tráfego para serviços OCI pelo Service Gateway.
|
||||||
|
- [ ] **9.** Criar uma rota temporária de saída por NAT Gateway, só para a aquisição inicial de dependências e pesos.
|
||||||
|
- ↳ Remover a rota logo após a tarefa 16 e passar pelo gate de egresso antes de qualquer execução. *(Q3)*
|
||||||
|
- ↳ Criar e ativar **só quando necessário**, com `infra/oci/nat.py on`. Para desligar: `nat.py off`, que fecha a rota e bloqueia o NAT. Na tarefa 78: `nat.py off --delete`.
|
||||||
|
- [ ] **10.** Validar que os jobs acessam os buckets sem internet pública.
|
||||||
|
- ↳ Adiado para quando o primeiro job existir (depois da tarefa 11).
|
||||||
|
|
||||||
|
## F1 — Ambiente e artefatos
|
||||||
|
|
||||||
|
- [ ] **11.** Criar uma imagem de contêiner base com Python, CUDA, PyTorch, Transformers, Hugging Face Hub e ferramentas de medição.
|
||||||
|
- ↳ Incluir um exportador OpenTelemetry, para que a telemetria do harness tenha o mesmo formato da observabilidade do orquestrador.
|
||||||
|
- [ ] **12.** Fixar num manifesto as versões do sistema, dos drivers CUDA, do Python, das bibliotecas e da imagem de contêiner.
|
||||||
|
- [ ] **13.** Baixar os pesos, tokenizadores, revisões e licenças aprovados de cada candidato. *(Q4)*
|
||||||
|
- ↳ A aquisição falha se a licença estiver fora da allowlist de `config/benchmark.yaml`. O contexto efetivo de cada candidato é medido e registrado.
|
||||||
|
- ↳ Incluir as dependências específicas de cada candidato (pacote GLiNER, runtime SemIf, interface Rizzo Flow) e a revisão fixada de `Qwen/Qwen3.5-4B`.
|
||||||
|
- [ ] **14.** Calcular o SHA-256 de cada artefato baixado.
|
||||||
|
- [ ] **15.** Copiar os pesos e dependências aprovados para o bucket `artefatos`.
|
||||||
|
- [ ] **16.** Registrar no manifesto a origem, a revisão, a licença, o hash, a data de aquisição e o tamanho de cada artefato.
|
||||||
|
- [ ] **17.** Desabilitar a saída pública dos jobs de benchmark.
|
||||||
|
- [ ] **18.** Carregar cada candidato só a partir dos artefatos locais.
|
||||||
|
- ↳ Usar `HF_HUB_OFFLINE=1`, `TRANSFORMERS_OFFLINE=1` e um cache apontado para os artefatos verificados.
|
||||||
|
- [ ] **19.** Fazer o job falhar se algum candidato tentar baixar arquivos durante a inferência.
|
||||||
|
- ↳ Duas camadas: as variáveis de ambiente offline e o bloqueio de rede da VCN. Um teste negativo prova que a falha ocorre.
|
||||||
|
|
||||||
|
## F2 — Tarefas de decisão e taxonomia
|
||||||
|
|
||||||
|
- [ ] **20.** Definir a taxonomia de decisões a avaliar em PT-BR.
|
||||||
|
- ↳ Taxonomia de **intenções** (D1), ligadas aos **agentes de destino** do catálogo A2A, mais as decisões de **continuidade de sessão** (D2), **desambiguação** (D3) e **várias intenções** (D4). D5 é opcional. Ver [02 §2](02-desenho-experimental.md#2-tarefas-de-decisão).
|
||||||
|
- [ ] **21.** Escrever a pergunta, as opções, as descrições das opções e a regra de decisão de cada tarefa.
|
||||||
|
- ↳ As descrições das opções seguem o estilo de um *agent card*: curtas e orientadas à capacidade do agente.
|
||||||
|
- [ ] **22.** Limitar o primeiro ciclo a no máximo 20 opções por questão, para garantir compatibilidade com o Laya. *(Q2)*
|
||||||
|
- ↳ Os subconjuntos de opções por exemplo simulam o serviço de elegibilidade de intenções.
|
||||||
|
- [ ] **23.** Incluir uma opção explícita de `outro` ou `sem correspondência` quando a taxonomia não for exaustiva.
|
||||||
|
|
||||||
|
## F3 — Dados e anotação
|
||||||
|
|
||||||
|
- [ ] **24.** Coletar textos PT-BR representativos do domínio de destino. *(Q7)*
|
||||||
|
- ↳ A unidade é a **conversa com vários turnos**, não o enunciado isolado. Cada turno guarda o estado (agente ativo e sessões suspensas).
|
||||||
|
- [ ] **25.** Remover ou mascarar dados pessoais, segredos e identificadores que o benchmark não precisa.
|
||||||
|
- [ ] **26.** Separar os textos por domínio, comprimento, grau de ambiguidade e classe esperada.
|
||||||
|
- ↳ Acrescentar a posição do turno e a presença de agente ativo ou de sessões suspensas.
|
||||||
|
- [ ] **27.** Criar exemplos adversariais com abreviações, erros de ortografia, regionalismos, textos curtos e várias intenções.
|
||||||
|
- ↳ Acrescentar respostas curtas que dependem do contexto ("sim", "o segundo"), troca de assunto, retorno a um assunto anterior e pedidos fora de escopo.
|
||||||
|
- [ ] **28.** Definir os critérios de inclusão e exclusão dos exemplos.
|
||||||
|
- [ ] **29.** Produzir o guia de anotação humana.
|
||||||
|
- ↳ O guia cobre D1–D4 com exemplos-limite (por exemplo, quando um enunciado curto continua a tarefa do agente ativo e quando inicia um novo agente).
|
||||||
|
- [ ] **30.** Anotar cada exemplo com dois avaliadores independentes.
|
||||||
|
- [ ] **31.** Resolver as discordâncias com a revisão de um terceiro avaliador.
|
||||||
|
- [ ] **32.** Registrar, para cada exemplo, a resposta de referência, a justificativa e a versão da taxonomia.
|
||||||
|
- ↳ Reportar também a concordância entre anotadores.
|
||||||
|
- [ ] **33.** Separar os dados em desenvolvimento, calibração e teste final por grupo de origem. *(Q12)*
|
||||||
|
- ↳ O grupo é `group_id`, ou seja, a conversa.
|
||||||
|
- [ ] **34.** Impedir que textos derivados do mesmo caso apareçam em mais de uma partição.
|
||||||
|
- [ ] **35.** Congelar o conjunto de teste final antes de qualquer ajuste de parâmetros.
|
||||||
|
- ↳ Registrar o hash do arquivo congelado e guardar uma cópia em `logs-imutaveis`.
|
||||||
|
|
||||||
|
## F4 — Contrato JEV e adaptadores
|
||||||
|
|
||||||
|
- [ ] **36.** Criar um formato canônico de entrada com `id`, `state`, `question`, `options`, `gold_label` e metadados.
|
||||||
|
- ↳ Ver [02 §3](02-desenho-experimental.md#3-formato-canônico-de-entrada-tarefa-36), que inclui a **regra única de serialização** de `state`.
|
||||||
|
- [ ] **37.** Definir a versão do contrato JEV compatível a usar no benchmark: `POST /v1/systemone`, esquema de requisição, esquema de resposta e códigos de erro. *(Q1)*
|
||||||
|
- ↳ Versão `jev-compat-v1`. Os esquemas já estão em `contracts/jev/schemas/`; falta revisar os códigos de erro no código dos adaptadores.
|
||||||
|
- [ ] **38.** Definir o mapeamento obrigatório entre o contrato JEV e os tipos internos `choice`, `noul` e `score`.
|
||||||
|
- [ ] **39.** Definir a regra de conversão de probabilidades, confiança, níveis ordinais, opções descritas e campos de uso.
|
||||||
|
- [ ] **40.** Definir o comportamento padronizado para entradas inválidas, limite de contexto, timeout, opção desconhecida e indisponibilidade do modelo.
|
||||||
|
- ↳ Na prática, uma falha do modelo de decisão vira desambiguação ou fallback no orquestrador. No benchmark, essas falhas contam como erro e são reportadas à parte.
|
||||||
|
- [ ] **41.** Criar testes de conformidade com requisições e respostas JEV congeladas.
|
||||||
|
- [ ] **42.** Criar um adaptador JEV local para o Laya com o checkpoint multilíngue fixado.
|
||||||
|
- [ ] **43.** Criar um adaptador JEV local para o SemIf em modo `direct`, com a revisão do Qwen fixada.
|
||||||
|
- [ ] **44.** Criar um adaptador JEV local para o GLiNER Decide usando `classify_text`.
|
||||||
|
- [ ] **45.** Criar um adaptador JEV local para o Rizzo Flow usando sua interface de decisões. *(Q4)*
|
||||||
|
- [ ] **46.** Expor cada adaptador no mesmo caminho local `POST /v1/systemone`, sem dependência de rede externa.
|
||||||
|
- [ ] **47.** Garantir que cada adaptador devolva a resposta JEV completa, incluindo escolha, distribuição de probabilidades, confiança e metadados de uso, quando disponíveis.
|
||||||
|
- [ ] **48.** Normalizar as saídas adicionais de execução num registro interno com `latency_ms`, `model_id`, `run_id` e versão do adaptador.
|
||||||
|
- ↳ O registro é emitido como span OpenTelemetry e também gravado em arquivo de resultados.
|
||||||
|
- [ ] **49.** Validar que os adaptadores preservam a mesma ordem e a mesma descrição das opções.
|
||||||
|
- [ ] **50.** Validar que trocar de candidato não altera a requisição JEV nem o formato da resposta JEV.
|
||||||
|
- [ ] **51.** Implementar testes de sanidade com decisões triviais, opções permutadas e os três tipos de pergunta.
|
||||||
|
- ↳ Incluir um teste de invariância à permutação das opções em D1 e D2 (a mesma escolha com outra ordem).
|
||||||
|
|
||||||
|
## F5 — Controle de execução
|
||||||
|
|
||||||
|
- [ ] **52.** Fixar seeds, ordem dos exemplos, tamanho de lote, precisão numérica e limites de tokens de cada execução.
|
||||||
|
- [ ] **53.** Definir uma política de truncamento comum para textos maiores que o menor contexto efetivamente suportado. *(Q8)*
|
||||||
|
- ↳ Truncar primeiro os turnos mais antigos do `state`. O enunciado atual nunca é truncado.
|
||||||
|
- [ ] **54.** Registrar o número de tokens de cada entrada antes de executar os modelos.
|
||||||
|
|
||||||
|
## F6 — Piloto
|
||||||
|
|
||||||
|
- [ ] **55.** Executar um piloto de 50 exemplos por candidato em CPU. *(Q11)*
|
||||||
|
- [ ] **56.** Executar um piloto de 50 exemplos por candidato em GPU.
|
||||||
|
- [ ] **57.** Medir memória de GPU, memória RAM, tempo de carga, tempo de aquecimento e falhas do piloto.
|
||||||
|
- [ ] **58.** Ajustar apenas os parâmetros operacionais necessários para eliminar falhas de execução.
|
||||||
|
|
||||||
|
## F7 — Desenvolvimento e calibração
|
||||||
|
|
||||||
|
- [ ] **59.** Executar os candidatos no conjunto de desenvolvimento, sem calibração adicional.
|
||||||
|
- [ ] **60.** Escolher os limites de confiança e as regras de abstenção só com a partição de calibração.
|
||||||
|
- ↳ Abster-se significa encaminhar para desambiguação. A regra é escolhida por meta de cobertura ou de risco, definida antes. *(Q5)*
|
||||||
|
- [ ] **61.** Calibrar as probabilidades separadamente por candidato, quando aplicável. *(Q10)*
|
||||||
|
- [ ] **62.** Não alterar prompts, opções, pesos ou limiares depois da abertura do conjunto de teste final.
|
||||||
|
- ↳ Marco de congelamento: registrar os hashes da configuração, dos calibradores e dos adaptadores.
|
||||||
|
|
||||||
|
## F8 — Teste final
|
||||||
|
|
||||||
|
- [ ] **63.** Executar uma rodada de acurácia no teste final com lote 1.
|
||||||
|
- [ ] **64.** Executar uma rodada de acurácia no teste final com o lote de produção definido. *(Q6)*
|
||||||
|
- [ ] **65.** Executar uma rodada de várias decisões sobre o mesmo texto, quando esse padrão existir no caso de uso.
|
||||||
|
- ↳ Corresponde a D4 e a combinações por turno (por exemplo, D2 + D1). Medir a latência somada por turno.
|
||||||
|
- [ ] **66.** Repetir cada medição de desempenho depois do aquecimento, em pelo menos três execuções independentes.
|
||||||
|
|
||||||
|
## F9 — Métricas e análise
|
||||||
|
|
||||||
|
- [ ] **67.** Coletar accuracy, macro F1, precision, recall e matriz de confusão.
|
||||||
|
- ↳ Acrescentar a taxa de rota errada com confiança alta.
|
||||||
|
- [ ] **68.** Coletar Brier score, ECE, cobertura e acurácia seletiva quando houver probabilidades ou abstenção.
|
||||||
|
- [ ] **69.** Coletar latência p50, p95 e p99, vazão, uso de GPU, uso de RAM e consumo de armazenamento.
|
||||||
|
- ↳ Reportar a latência por decisão e por turno.
|
||||||
|
- [ ] **70.** Calcular o custo estimado por mil decisões em cada cenário de execução.
|
||||||
|
- [ ] **71.** Criar cortes dos resultados por domínio, número de opções, comprimento e dificuldade do texto.
|
||||||
|
- ↳ Acrescentar os cortes por tarefa de decisão, posição do turno, com ou sem agente ativo e categoria adversarial.
|
||||||
|
- [ ] **72.** Inspecionar manualmente os erros de alto impacto e as discordâncias entre os modelos.
|
||||||
|
- [ ] **73.** Classificar os erros em: taxonomia, contexto insuficiente, ambiguidade, linguagem, truncamento, instrução ou desempenho do modelo.
|
||||||
|
- [ ] **74.** Gerar tabelas comparativas e gráficos a partir dos arquivos de resultados versionados. *(Q9)*
|
||||||
|
- ↳ Incluir intervalos de confiança (bootstrap) e testes pareados entre candidatos.
|
||||||
|
|
||||||
|
## F10 — Relatório e encerramento
|
||||||
|
|
||||||
|
- [ ] **75.** Elaborar o relatório técnico com configuração, limitações, resultados e recomendação.
|
||||||
|
- [ ] **76.** Anexar ao relatório os manifestos de artefatos, hashes, configurações, logs, testes de conformidade e comandos de reprodução.
|
||||||
|
- [ ] **77.** Arquivar os conjuntos congelados e os resultados no Object Storage com retenção definida.
|
||||||
|
- [ ] **78.** Desligar notebooks, jobs, endpoints temporários e o NAT Gateway que não sejam mais necessários.
|
||||||
167
docs/04-questoes-em-aberto.md
Normal file
167
docs/04-questoes-em-aberto.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# 04 — Decisões de projeto (ex-questões em aberto)
|
||||||
|
|
||||||
|
Todas as questões foram resolvidas em 2026-10-07. O critério foi preferir a **abordagem automatizada e verificável**: regras determinísticas em código, gates que fazem o pipeline falhar, valores calculados a partir de medições e parâmetros pré-registrados em [config/benchmark.yaml](../config/benchmark.yaml). Ninguém precisa decidir nada caso a caso durante a execução. Os valores numéricos pré-registrados ficam congelados com hash no marco da tarefa 62.
|
||||||
|
|
||||||
|
## Resumo
|
||||||
|
|
||||||
|
| # | Questão | Decisão | Mecanismo automatizado |
|
||||||
|
|---|---------|---------|------------------------|
|
||||||
|
| Q1 | Contrato JEV | Esquema público do `POST /v1/systemone` (`state` + `questions` tipadas → `answers` + `usage`), congelado como `jev-compat-v1` | JSON Schema em `contracts/jev/schemas/` e testes de conformidade |
|
||||||
|
| Q2 | 20 opções incluem o fallback? | Sim: no máximo 19 opções de domínio + `sem_correspondencia` | Validação de esquema (`maxProperties: 20`) e lint do dataset |
|
||||||
|
| Q3 | NAT × isolamento | Desassociar a rota NAT logo após a tarefa 16 | Gate de egresso: o pipeline só segue se a sonda de saída **falhar** |
|
||||||
|
| Q4 | Interfaces e licenças | Interfaces mapeadas por candidato (abaixo). Só licenças da allowlist | O script de aquisição lê a licença nos metadados do Hub e falha fora da allowlist |
|
||||||
|
| Q5 | Critérios de sucesso | SLO de latência fixo + não inferioridade relativa ao baseline | Avaliação automática dos critérios em `config/benchmark.yaml` |
|
||||||
|
| Q6 | Lote de produção | Escolhido por candidato: o maior lote com p95 dentro do SLO | Varredura de lotes no piloto |
|
||||||
|
| Q7 | Fonte dos dados | Reais anonimizados + sintéticos, com origem marcada | Geração sintética por template e LLM, anonimização automática, deduplicação |
|
||||||
|
| Q8 | Turnos no `state` | O maior N ∈ {0,1,2,4} que cabe no orçamento comum de tokens | Cálculo sobre a partição de desenvolvimento |
|
||||||
|
| Q9 | Baseline LLM | Incluído, fora do ranking principal | Adaptador JEV sobre o LLM Gateway |
|
||||||
|
| Q10 | Calibração | Temperature scaling por candidato × tarefa, só na partição de calibração | Ajuste por NLL e congelamento com hash |
|
||||||
|
| Q11 | Piloto em CPU | Mantido, com timeout. Inviabilidade declarada automaticamente | Regra p95 > timeout → "inviável em CPU" |
|
||||||
|
| Q12 | Tamanho do teste | ≥ 1.300 decisões D1 no teste final (análise de poder) | `analysis/sample_size.py`, com recálculo do MDE |
|
||||||
|
| Q13 | Centro de custo e orçamento | **Orçamento em aberto**: o uso está coberto por cota interna de engenharia. A estimativa de custo continua no relatório | A estimativa é calculada por script (tarefa 70) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q1 — Contrato JEV `jev-compat-v1`
|
||||||
|
|
||||||
|
Base: a forma pública do `POST /v1/systemone`, a mesma reproduzida pelas implementações abertas (OpenJev/SemIf).
|
||||||
|
|
||||||
|
**Requisição**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "<model_id>",
|
||||||
|
"state": "<texto> | <objeto JSON>",
|
||||||
|
"questions": {
|
||||||
|
"<nome>": {"type": "choice", "instructions": "...", "criteria": {"<opcao>": "<descrição>"}},
|
||||||
|
"<nome>": {"type": "noul", "instructions": "...", "criteria": {"true": "...", "false": "..."}},
|
||||||
|
"<nome>": {"type": "score", "instructions": "...", "criteria": ["<nível 0>", "<nível 1>", "..."]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resposta**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "<model_id>",
|
||||||
|
"answers": {
|
||||||
|
"<nome>": {"type": "choice", "choice": "<opcao>", "confidence": 0.0, "probabilities": {"<opcao>": 0.0}},
|
||||||
|
"<nome>": {"type": "noul", "noul": 0.0},
|
||||||
|
"<nome>": {"type": "score", "score": 0.0, "confidence": 0.0,
|
||||||
|
"probabilities": {"0": 0.0}, "legend": {"0": "<nível 0>"}}
|
||||||
|
},
|
||||||
|
"usage": {"input_tokens": 0, "output_tokens": 0}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Erros:** `422` para requisição malformada, modelo desconhecido ou estado acima do contexto. `401` para autenticação, que não se aplica aos adaptadores locais. Comportamentos padronizados do benchmark (tarefa 40):
|
||||||
|
|
||||||
|
| Situação | Código | Contabilização |
|
||||||
|
|----------|--------|----------------|
|
||||||
|
| Requisição inválida | `422` | Erro do harness: falha o job |
|
||||||
|
| Estado acima do contexto, depois do truncamento comum | `422` | Erro de decisão (`error_type=context`) |
|
||||||
|
| Timeout (`timeout_ms` do cenário) | `504` | Erro de decisão (`error_type=timeout`) |
|
||||||
|
| Opção devolvida fora de `criteria` | `502` | Erro de decisão (`error_type=unknown_option`) |
|
||||||
|
| Modelo indisponível | `503` | Erro de execução: refaz até 3 vezes e depois falha o job |
|
||||||
|
|
||||||
|
**Mapeamento para os tipos internos (tarefas 38–39)**
|
||||||
|
|
||||||
|
- `choice` → D1, D2 e D5. A predição é `choice`; a distribuição é `probabilities`.
|
||||||
|
- `noul` → D3 e as perguntas por intenção de D4. P(sim) = `noul`.
|
||||||
|
- `score` → reservado. Não é usado no primeiro ciclo.
|
||||||
|
- **`confidence` não é calibrada** em nenhuma implementação. Por isso, todas as métricas de calibração e abstenção usam `max(probabilities)` (ou `max(noul, 1−noul)`) **depois** da calibração da tarefa 61. A `confidence` nativa é só registrada.
|
||||||
|
- Quando um candidato não expõe uma distribuição normalizada, o adaptador aplica softmax sobre os escores das opções e marca `metadata.probabilities_source = "normalized_scores"`.
|
||||||
|
- `output_tokens` deve ser sempre 0, porque não há geração. Um valor diferente de 0 reprova o teste de conformidade.
|
||||||
|
|
||||||
|
## Q2 — Limite de opções
|
||||||
|
|
||||||
|
O limite de 20 opções inclui `sem_correspondencia`: no máximo 19 opções de domínio + 1. O dataset é validado por esquema. Um exemplo com mais de 20 opções reprova o lint e impede o congelamento (tarefa 35).
|
||||||
|
|
||||||
|
## Q3 — NAT e isolamento de rede
|
||||||
|
|
||||||
|
Sequência automatizada, em que cada passo falha o pipeline se não for cumprido:
|
||||||
|
|
||||||
|
1. **9 → 16:** a rota NAT existe só durante a aquisição.
|
||||||
|
2. **Fim da tarefa 16:** `infra/oci/nat.py off` remove a rota e o egresso `0.0.0.0/0` e bloqueia o NAT. O NAT só é criado ou ativado com `nat.py on`, quando for necessário.
|
||||||
|
3. **Gate de egresso (tarefas 17 e 19):** um job de sonda tenta resolver e abrir conexão com hosts públicos (Hugging Face Hub, PyPI). O gate passa só se **todas** as tentativas falharem e se o acesso ao Object Storage pelo Service Gateway funcionar.
|
||||||
|
4. **Toda execução de benchmark** roda o gate antes da inferência e grava o resultado no manifesto da execução.
|
||||||
|
5. **Tarefa 78:** o NAT Gateway é destruído, e o gate confirma que nenhum recurso do compartment tem rota de saída.
|
||||||
|
|
||||||
|
## Q4 — Interfaces e licenças dos candidatos
|
||||||
|
|
||||||
|
| Candidato | Licença | Interface usada pelo adaptador | Contexto declarado | Observação |
|
||||||
|
|-----------|---------|--------------------------------|--------------------|------------|
|
||||||
|
| Laya multilingual | Apache-2.0 | `laya.load(...).predict(state, questions)`, nativamente no formato JEV | 1.024 tokens (default), até 8.192 | Recomenda até ~20 opções. Vem descalibrado: a calibração da tarefa 61 é obrigatória |
|
||||||
|
| SemIf + Qwen3.5-4B | MIT (SemIf); Qwen: verificar no Hub | Modo `direct`: um forward pass lê os logits das opções declaradas | ~8k tokens | Revisão do Qwen fixada no manifesto |
|
||||||
|
| GLiNER2.5-multi-Decide | Apache-2.0 | `classify_text(text, {"<q>": {"labels": {opcao: descrição}}})` | Não declarado: medir | Não tem `noul`/`score` nativos. `noul` vira o rótulo binário `{true,false}`. As probabilidades são normalizadas a partir dos escores |
|
||||||
|
| Rizzo Flow 4B | Apache-2.0 (base); dados de treino com componentes CC-BY-SA/CC-BY | Prompt `spark-decisions-v3`, com leitura dos logits das letras de resposta | 2.048 tokens | **O formato de prompt é obrigatório**: o adaptador monta `spark-decisions-v3` a partir da serialização comum. Documentação só em inglês: o risco de idioma vai para o relatório |
|
||||||
|
|
||||||
|
Regras automatizadas:
|
||||||
|
- A **allowlist de licenças** (`Apache-2.0`, `MIT`) fica em `config/benchmark.yaml`. O script da tarefa 13 lê `cardData.license` de cada repositório no Hub. Licença ausente ou fora da lista falha a aquisição, até haver aprovação explícita registrada no manifesto.
|
||||||
|
- O **contexto efetivo** de cada candidato é medido por código: o mínimo entre `tokenizer.model_max_length`, `config.max_position_embeddings` e o limite declarado no adaptador. O resultado vai para o manifesto. Esse valor alimenta Q8.
|
||||||
|
|
||||||
|
## Q5 — Critérios de sucesso pré-registrados
|
||||||
|
|
||||||
|
Avaliados automaticamente pelo `analysis/` a partir de `config/benchmark.yaml`:
|
||||||
|
|
||||||
|
- **Desempenho (H3):** em GPU com lote 1, p95 ≤ **150 ms por decisão** e ≤ **300 ms por turno**. Em CPU os valores são só reportados.
|
||||||
|
- **Acurácia (H1):** em D1 e D2, **não inferioridade em relação ao baseline LLM** com margem de **3 p.p.**: o limite inferior do IC 95% (bootstrap pareado por conversa) da diferença precisa ser > −3 p.p. Sem baseline disponível, vale o critério absoluto: macro F1 ≥ 0,80.
|
||||||
|
- **Risco (H2):** no limiar escolhido na calibração, a taxa de **rota errada com confiança alta** precisa ser ≤ **2%**, com cobertura ≥ **80%**.
|
||||||
|
|
||||||
|
O critério relativo ao baseline dispensa um limiar absoluto arbitrário de acurácia, que dependeria da dificuldade do dataset.
|
||||||
|
|
||||||
|
## Q6 — Lote de produção
|
||||||
|
|
||||||
|
O piloto em GPU (tarefa 56) varre os lotes {1, 2, 4, 8, 16, 32}. O lote de produção de cada candidato é **o maior lote com p95 por decisão ≤ SLO**. O valor escolhido entra no manifesto e é congelado na tarefa 62. Lote 1 continua sendo o cenário principal. O lote de produção serve para reportar vazão e custo.
|
||||||
|
|
||||||
|
## Q7 — Fonte e preparação dos dados
|
||||||
|
|
||||||
|
- **Reais:** enunciados de conversas anonimizadas, se houver. **Sintéticos:** gerados a partir da taxonomia e dos templates adversariais (tarefa 27), com um LLM via gateway que **não** seja nenhum dos candidatos nem o baseline.
|
||||||
|
- **Anonimização automática (tarefa 25):** regras para CPF, CNPJ, telefone, e-mail, CEP, números de cartão e protocolo, além de NER de pessoas e lugares, substituindo os achados por marcadores tipados (`<CPF>`, `<NOME>`). Uma amostra aleatória de 5% passa por verificação humana. Qualquer vazamento encontrado obriga a reprocessar o lote inteiro.
|
||||||
|
- **Deduplicação:** MinHash/LSH para quase-duplicatas, dentro de cada fonte e entre fontes. Antes da partição, duplicatas recebem o mesmo `group_id`.
|
||||||
|
- O campo `metadata.source` ∈ {`real`, `synthetic`} existe em todos os exemplos. As métricas saem por fonte e no agregado.
|
||||||
|
- A anotação humana (tarefas 30–31) se mantém para todos os exemplos, inclusive os sintéticos. O rótulo de geração serve só como sugestão e é escondido dos anotadores.
|
||||||
|
|
||||||
|
## Q8 — Turnos no `state` e orçamento de tokens
|
||||||
|
|
||||||
|
1. **Orçamento comum** = o menor contexto efetivo medido entre os candidatos (Q4) − a reserva para a pergunta e as opções do maior exemplo.
|
||||||
|
2. **N** = o maior valor em {0, 1, 2, 4} em que ≥ 95% dos exemplos de desenvolvimento cabem no orçamento sem truncar o enunciado atual. A contagem usa o tokenizador **mais verboso** entre os candidatos.
|
||||||
|
3. A regra de truncamento (tarefa 53) remove primeiro os turnos mais antigos e nunca o enunciado atual. Se o enunciado sozinho estoura o orçamento, ele é truncado no fim e o exemplo recebe `truncated=true`.
|
||||||
|
4. Uma ablação com N ∈ {0, 1, 2, 4} roda **só no desenvolvimento** e serve só para o relatório. Ela não altera a configuração congelada.
|
||||||
|
|
||||||
|
## Q9 — Baseline LLM
|
||||||
|
|
||||||
|
- Um LLM generalista disponível via LLM Gateway ou serviço gerenciado na OCI, com a versão fixada no manifesto.
|
||||||
|
- Adaptador JEV próprio com saída restrita às opções. Quando houver logprobs, as probabilidades vêm deles. Quando não houver, a distribuição é one-hot com `probabilities_source="none"`, e o baseline fica fora das métricas de calibração.
|
||||||
|
- Tem a mesma serialização, o mesmo truncamento e os mesmos dados. Fica **fora do ranking principal** e serve de referência para H1 e para o custo.
|
||||||
|
- Por usar rede, é a **única exceção** ao isolamento. Roda num job separado, com egresso só para o endpoint do gateway, sem acesso aos artefatos dos candidatos.
|
||||||
|
|
||||||
|
## Q10 — Calibração
|
||||||
|
|
||||||
|
- **Temperature scaling** por candidato × tarefa de decisão: um único parâmetro, ajustado por minimização de NLL na partição de calibração. Para `noul`, o mesmo método é aplicado ao logit de P(sim).
|
||||||
|
- O calibrador só é aceito se reduzir o ECE na própria partição de calibração (validação cruzada em 5 dobras por `group_id`). Se não reduzir, fica a identidade (T = 1).
|
||||||
|
- Em seguida, os limiares de abstenção (tarefa 60) são escolhidos sobre as probabilidades calibradas, pelo critério de Q5: maximizar a cobertura com taxa de rota errada com confiança alta ≤ 2%.
|
||||||
|
- Os parâmetros T e os limiares são versionados e congelados com hash na tarefa 62.
|
||||||
|
|
||||||
|
## Q11 — Piloto em CPU
|
||||||
|
|
||||||
|
- Timeout de **10 s por decisão** em CPU.
|
||||||
|
- Regra automática: candidato com p95 > timeout ou com taxa de timeout > 5% no piloto recebe o status `inviavel_cpu` no manifesto e é excluído das rodadas finais em CPU. Os resultados em CPU dos demais são reportados como viabilidade, não como ranking.
|
||||||
|
|
||||||
|
## Q12 — Tamanho do conjunto de teste final
|
||||||
|
|
||||||
|
Análise de poder para a comparação pareada (teste de McNemar) entre dois candidatos em D1:
|
||||||
|
|
||||||
|
- Diferença mínima detectável d = 3 p.p., α = 0,05 (bilateral), poder = 0,80, proporção de discordância esperada p_d = 0,15:
|
||||||
|
n ≈ (z₀.₉₇₅·√p_d + z₀.₈·√(p_d − d²))² / d² ≈ **1.300 decisões**.
|
||||||
|
- Correção de agrupamento: n_final = n × (1 + (m − 1)·ICC), em que m é o número médio de turnos por conversa e o ICC é estimado na partição de desenvolvimento. Antes de medir, usar ICC = 0,05.
|
||||||
|
- `analysis/sample_size.py` (a implementar) recalcula n com os valores observados. Se o teste congelado ficar abaixo do n necessário, o relatório troca a afirmação de significância pelo **MDE efetivo** do conjunto disponível.
|
||||||
|
- D2–D4 usam o que estiver disponível, com IC por bootstrap por conversa.
|
||||||
|
|
||||||
|
## Q13 — Centro de custo e orçamento
|
||||||
|
|
||||||
|
> **Revisão (2026-10-07):** o orçamento e os alertas (tarefas 3–4) ficam **em aberto**, porque o uso está coberto por cota interna de engenharia. A tag `centro_custo` não é aplicada por enquanto. O texto abaixo vale só se for necessário criar um orçamento depois.
|
||||||
|
|
||||||
|
- `centro_custo` é variável Terraform **obrigatória e sem default**. O `terraform plan` falha enquanto ela não for informada, o que impede criar recursos sem a tag.
|
||||||
|
- O valor do orçamento é calculado pelo script de estimativa, que multiplica as horas previstas por *shape* (piloto, dev, calibração, teste e repetições) pelo preço público da OCI e aplica um fator de contingência de 1,3. O resultado vai para `infra/oci/` como `budget_amount`. Os alertas de 50%, 80% e 100% (tarefa 4) são derivados desse valor.
|
||||||
0
env/.gitkeep
vendored
Normal file
0
env/.gitkeep
vendored
Normal file
0
harness/.gitkeep
Normal file
0
harness/.gitkeep
Normal file
93
infra/oci/nat.py
Normal file
93
infra/oci/nat.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""Liga e desliga a saída temporária por NAT Gateway (tarefas 9, 17 e 78; decisão Q3).
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
PYTHONPATH=infra/oci .venv/bin/python infra/oci/nat.py on # cria/desbloqueia o NAT e abre a rota
|
||||||
|
PYTHONPATH=infra/oci .venv/bin/python infra/oci/nat.py off # fecha a rota e bloqueia o NAT
|
||||||
|
PYTHONPATH=infra/oci .venv/bin/python infra/oci/nat.py off --delete # fecha a rota e apaga o NAT (tarefa 78)
|
||||||
|
|
||||||
|
O NAT só deve ficar ativo durante a aquisição de dependências e pesos.
|
||||||
|
Ao final, regrava .secrets/artefatos-OCI.md.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import oci
|
||||||
|
|
||||||
|
import provision
|
||||||
|
from session import load_config
|
||||||
|
|
||||||
|
NAT_NAME = "dmb-nat-temporario"
|
||||||
|
ANYWHERE = "0.0.0.0/0"
|
||||||
|
|
||||||
|
|
||||||
|
def lookup(vn, cid):
|
||||||
|
vcn = next(v for v in vn.list_vcns(cid, display_name="dmb-vcn").data if v.lifecycle_state == "AVAILABLE")
|
||||||
|
rt = next(r for r in vn.list_route_tables(cid, vcn_id=vcn.id, display_name="dmb-rt-privada").data
|
||||||
|
if r.lifecycle_state == "AVAILABLE")
|
||||||
|
sl = next(s for s in vn.list_security_lists(cid, vcn_id=vcn.id, display_name="dmb-sl-privada").data
|
||||||
|
if s.lifecycle_state == "AVAILABLE")
|
||||||
|
nat = next((n for n in vn.list_nat_gateways(cid, vcn_id=vcn.id, display_name=NAT_NAME).data
|
||||||
|
if n.lifecycle_state == "AVAILABLE"), None)
|
||||||
|
return vcn, rt, sl, nat
|
||||||
|
|
||||||
|
|
||||||
|
def nat_on(vn, cid):
|
||||||
|
vcn, rt, sl, nat = lookup(vn, cid)
|
||||||
|
if nat is None:
|
||||||
|
nat = vn.create_nat_gateway(oci.core.models.CreateNatGatewayDetails(
|
||||||
|
compartment_id=cid, vcn_id=vcn.id, display_name=NAT_NAME,
|
||||||
|
freeform_tags=provision.TAGS)).data
|
||||||
|
nat = oci.wait_until(vn, vn.get_nat_gateway(nat.id), "lifecycle_state", "AVAILABLE").data
|
||||||
|
elif nat.block_traffic:
|
||||||
|
vn.update_nat_gateway(nat.id, oci.core.models.UpdateNatGatewayDetails(block_traffic=False))
|
||||||
|
|
||||||
|
if not any(r.destination == ANYWHERE for r in rt.route_rules):
|
||||||
|
vn.update_route_table(rt.id, oci.core.models.UpdateRouteTableDetails(route_rules=rt.route_rules + [
|
||||||
|
oci.core.models.RouteRule(destination=ANYWHERE, destination_type="CIDR_BLOCK",
|
||||||
|
network_entity_id=nat.id, description="TEMPORÁRIO: aquisição de artefatos")]))
|
||||||
|
if not any(r.destination == ANYWHERE for r in sl.egress_security_rules):
|
||||||
|
vn.update_security_list(sl.id, oci.core.models.UpdateSecurityListDetails(
|
||||||
|
ingress_security_rules=sl.ingress_security_rules,
|
||||||
|
egress_security_rules=sl.egress_security_rules + [oci.core.models.EgressSecurityRule(
|
||||||
|
destination=ANYWHERE, protocol="6", description="TEMPORÁRIO: HTTPS para aquisição",
|
||||||
|
tcp_options=oci.core.models.TcpOptions(
|
||||||
|
destination_port_range=oci.core.models.PortRange(min=443, max=443)))]))
|
||||||
|
print(f"NAT ativo: {nat.id}")
|
||||||
|
|
||||||
|
|
||||||
|
def nat_off(vn, cid, delete):
|
||||||
|
_, rt, sl, nat = lookup(vn, cid)
|
||||||
|
rules = [r for r in rt.route_rules if r.destination != ANYWHERE]
|
||||||
|
if len(rules) != len(rt.route_rules):
|
||||||
|
vn.update_route_table(rt.id, oci.core.models.UpdateRouteTableDetails(route_rules=rules))
|
||||||
|
egress = [r for r in sl.egress_security_rules if r.destination != ANYWHERE]
|
||||||
|
if len(egress) != len(sl.egress_security_rules):
|
||||||
|
vn.update_security_list(sl.id, oci.core.models.UpdateSecurityListDetails(
|
||||||
|
ingress_security_rules=sl.ingress_security_rules, egress_security_rules=egress))
|
||||||
|
if nat is None:
|
||||||
|
print("NAT inexistente; rota e egresso fechados")
|
||||||
|
return
|
||||||
|
if delete:
|
||||||
|
vn.delete_nat_gateway(nat.id)
|
||||||
|
print(f"NAT apagado: {nat.id}")
|
||||||
|
else:
|
||||||
|
vn.update_nat_gateway(nat.id, oci.core.models.UpdateNatGatewayDetails(block_traffic=True))
|
||||||
|
print(f"NAT bloqueado: {nat.id}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2 or sys.argv[1] not in ("on", "off"):
|
||||||
|
sys.exit(__doc__)
|
||||||
|
cfg = load_config()
|
||||||
|
home = load_config(region="us-ashburn-1")
|
||||||
|
cid = provision.find_compartment(oci.identity.IdentityClient(home), home["tenancy"])
|
||||||
|
vn = oci.core.VirtualNetworkClient(cfg)
|
||||||
|
if sys.argv[1] == "on":
|
||||||
|
nat_on(vn, cid)
|
||||||
|
else:
|
||||||
|
nat_off(vn, cid, delete="--delete" in sys.argv)
|
||||||
|
provision.artifacts.clear()
|
||||||
|
provision.main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
231
infra/oci/provision.py
Normal file
231
infra/oci/provision.py
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
"""Provisionamento idempotente da fase F0 (tarefas 1, 2, 5, 6, 7, 8).
|
||||||
|
|
||||||
|
Uso: PYTHONPATH=infra/oci .venv/bin/python infra/oci/provision.py
|
||||||
|
Registra os artefatos criados em .secrets/artefatos-OCI.md.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import oci
|
||||||
|
|
||||||
|
from session import CRED_DIR, REGION, load_config
|
||||||
|
|
||||||
|
PARENT_PATH = "root/<compartimento-pai>"
|
||||||
|
COMPARTMENT_NAME = "decision-models"
|
||||||
|
TAGS = {"projeto": "benchmark-decisoes-ptbr", "ambiente": "benchmark"} # centro_custo pendente (Q13)
|
||||||
|
BUCKETS = ["entrada", "artefatos", "resultados", "logs-imutaveis"]
|
||||||
|
VCN_CIDR = "10.20.0.0/16"
|
||||||
|
SUBNET_CIDR = "10.20.1.0/24"
|
||||||
|
ARTIFACTS_FILE = CRED_DIR.parent / "artefatos-OCI.md"
|
||||||
|
|
||||||
|
artifacts = []
|
||||||
|
|
||||||
|
|
||||||
|
def record(kind, name, ocid, region):
|
||||||
|
artifacts.append((kind, name, ocid, region))
|
||||||
|
|
||||||
|
|
||||||
|
def find_compartment(idn, tenancy):
|
||||||
|
allc = oci.pagination.list_call_get_all_results(
|
||||||
|
idn.list_compartments, tenancy, compartment_id_in_subtree=True,
|
||||||
|
access_level="ACCESSIBLE", lifecycle_state="ACTIVE").data
|
||||||
|
byid = {c.id: c for c in allc}
|
||||||
|
|
||||||
|
def path(c):
|
||||||
|
parts = []
|
||||||
|
while c:
|
||||||
|
parts.append(c.name)
|
||||||
|
c = byid.get(c.compartment_id)
|
||||||
|
return "root/" + "/".join(reversed(parts))
|
||||||
|
|
||||||
|
parent = next(c for c in allc if path(c) == PARENT_PATH)
|
||||||
|
comp = next((c for c in allc if c.compartment_id == parent.id and c.name == COMPARTMENT_NAME), None)
|
||||||
|
if comp is None:
|
||||||
|
comp = idn.create_compartment(oci.identity.models.CreateCompartmentDetails(
|
||||||
|
compartment_id=parent.id, name=COMPARTMENT_NAME, freeform_tags=TAGS,
|
||||||
|
description="Benchmark de modelos de decisão PT-BR para roteamento em orquestrador multiagente")).data
|
||||||
|
for _ in range(30):
|
||||||
|
try:
|
||||||
|
if idn.get_compartment(comp.id).data.lifecycle_state == "ACTIVE":
|
||||||
|
break
|
||||||
|
except oci.exceptions.ServiceError as e:
|
||||||
|
if e.status != 404:
|
||||||
|
raise
|
||||||
|
time.sleep(10)
|
||||||
|
record("Compartment", f"{PARENT_PATH}/{COMPARTMENT_NAME}", comp.id, "global (home us-ashburn-1)")
|
||||||
|
return comp.id
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ds_project(cfg, cid):
|
||||||
|
ds = oci.data_science.DataScienceClient(cfg)
|
||||||
|
name = "benchmark-decisoes-ptbr"
|
||||||
|
proj = next((p for p in ds.list_projects(cid, lifecycle_state="ACTIVE").data if p.display_name == name), None)
|
||||||
|
if proj is None:
|
||||||
|
proj = ds.create_project(oci.data_science.models.CreateProjectDetails(
|
||||||
|
compartment_id=cid, display_name=name, freeform_tags=TAGS,
|
||||||
|
description="Benchmark de modelos de decisão PT-BR")).data
|
||||||
|
record("Data Science Project", name, proj.id, REGION)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_buckets(cfg, cid):
|
||||||
|
os_ = oci.object_storage.ObjectStorageClient(cfg)
|
||||||
|
ns = os_.get_namespace().data
|
||||||
|
existing = {b.name for b in oci.pagination.list_call_get_all_results(os_.list_buckets, ns, cid).data}
|
||||||
|
for short in BUCKETS:
|
||||||
|
name = f"dmb-{short}"
|
||||||
|
if name not in existing:
|
||||||
|
os_.create_bucket(ns, oci.object_storage.models.CreateBucketDetails(
|
||||||
|
name=name, compartment_id=cid, public_access_type="NoPublicAccess",
|
||||||
|
storage_tier="Standard", versioning="Enabled", freeform_tags=TAGS))
|
||||||
|
b = os_.get_bucket(ns, name).data
|
||||||
|
record("Object Storage Bucket", f"{name} (namespace {ns})", b.id, REGION)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_network(cfg, cid):
|
||||||
|
vn = oci.core.VirtualNetworkClient(cfg)
|
||||||
|
comp = oci.wait_until # alias curto
|
||||||
|
|
||||||
|
vcn = next((v for v in vn.list_vcns(cid, display_name="dmb-vcn").data
|
||||||
|
if v.lifecycle_state == "AVAILABLE"), None)
|
||||||
|
if vcn is None:
|
||||||
|
vcn = vn.create_vcn(oci.core.models.CreateVcnDetails(
|
||||||
|
compartment_id=cid, display_name="dmb-vcn", cidr_blocks=[VCN_CIDR],
|
||||||
|
dns_label="dmbvcn", freeform_tags=TAGS)).data
|
||||||
|
vcn = comp(vn, vn.get_vcn(vcn.id), "lifecycle_state", "AVAILABLE").data
|
||||||
|
record("VCN", f"dmb-vcn ({VCN_CIDR})", vcn.id, REGION)
|
||||||
|
|
||||||
|
svc = next(s for s in vn.list_services().data if s.cidr_block.startswith("all-") and "services-in-oracle-services-network" in s.cidr_block)
|
||||||
|
sgw = next((g for g in vn.list_service_gateways(cid, vcn_id=vcn.id).data
|
||||||
|
if g.lifecycle_state == "AVAILABLE"), None)
|
||||||
|
if sgw is None:
|
||||||
|
sgw = vn.create_service_gateway(oci.core.models.CreateServiceGatewayDetails(
|
||||||
|
compartment_id=cid, vcn_id=vcn.id, display_name="dmb-sgw", freeform_tags=TAGS,
|
||||||
|
services=[oci.core.models.ServiceIdRequestDetails(service_id=svc.id)])).data
|
||||||
|
sgw = comp(vn, vn.get_service_gateway(sgw.id), "lifecycle_state", "AVAILABLE").data
|
||||||
|
record("Service Gateway", f"dmb-sgw ({svc.cidr_block})", sgw.id, REGION)
|
||||||
|
|
||||||
|
rt = next((r for r in vn.list_route_tables(cid, vcn_id=vcn.id, display_name="dmb-rt-privada").data
|
||||||
|
if r.lifecycle_state == "AVAILABLE"), None)
|
||||||
|
if rt is None:
|
||||||
|
rt = vn.create_route_table(oci.core.models.CreateRouteTableDetails(
|
||||||
|
compartment_id=cid, vcn_id=vcn.id, display_name="dmb-rt-privada", freeform_tags=TAGS,
|
||||||
|
route_rules=[oci.core.models.RouteRule(
|
||||||
|
destination=svc.cidr_block, destination_type="SERVICE_CIDR_BLOCK",
|
||||||
|
network_entity_id=sgw.id, description="Object Storage e serviços OCI via Service Gateway")])).data
|
||||||
|
rt = comp(vn, vn.get_route_table(rt.id), "lifecycle_state", "AVAILABLE").data
|
||||||
|
record("Route Table", "dmb-rt-privada (somente Service Gateway)", rt.id, REGION)
|
||||||
|
|
||||||
|
sl = next((s for s in vn.list_security_lists(cid, vcn_id=vcn.id, display_name="dmb-sl-privada").data
|
||||||
|
if s.lifecycle_state == "AVAILABLE"), None)
|
||||||
|
if sl is None:
|
||||||
|
sl = vn.create_security_list(oci.core.models.CreateSecurityListDetails(
|
||||||
|
compartment_id=cid, vcn_id=vcn.id, display_name="dmb-sl-privada", freeform_tags=TAGS,
|
||||||
|
egress_security_rules=[
|
||||||
|
oci.core.models.EgressSecurityRule(
|
||||||
|
destination=svc.cidr_block, destination_type="SERVICE_CIDR_BLOCK", protocol="6",
|
||||||
|
tcp_options=oci.core.models.TcpOptions(destination_port_range=oci.core.models.PortRange(min=443, max=443)),
|
||||||
|
description="HTTPS para serviços OCI"),
|
||||||
|
oci.core.models.EgressSecurityRule(destination=VCN_CIDR, protocol="all", description="Tráfego interno da VCN"),
|
||||||
|
],
|
||||||
|
ingress_security_rules=[
|
||||||
|
oci.core.models.IngressSecurityRule(source=VCN_CIDR, protocol="all", description="Tráfego interno da VCN"),
|
||||||
|
])).data
|
||||||
|
sl = comp(vn, vn.get_security_list(sl.id), "lifecycle_state", "AVAILABLE").data
|
||||||
|
record("Security List", "dmb-sl-privada (egresso só para serviços OCI e VCN)", sl.id, REGION)
|
||||||
|
|
||||||
|
sub = next((s for s in vn.list_subnets(cid, vcn_id=vcn.id, display_name="dmb-subnet-jobs").data
|
||||||
|
if s.lifecycle_state == "AVAILABLE"), None)
|
||||||
|
if sub is None:
|
||||||
|
sub = vn.create_subnet(oci.core.models.CreateSubnetDetails(
|
||||||
|
compartment_id=cid, vcn_id=vcn.id, display_name="dmb-subnet-jobs", cidr_block=SUBNET_CIDR,
|
||||||
|
dns_label="jobs", prohibit_public_ip_on_vnic=True, route_table_id=rt.id,
|
||||||
|
security_list_ids=[sl.id], freeform_tags=TAGS)).data
|
||||||
|
sub = comp(vn, vn.get_subnet(sub.id), "lifecycle_state", "AVAILABLE").data
|
||||||
|
record("Subnet (privada)", f"dmb-subnet-jobs ({SUBNET_CIDR})", sub.id, REGION)
|
||||||
|
|
||||||
|
# O NAT é criado só por nat.py; aqui ele só é registrado, se existir.
|
||||||
|
for nat in vn.list_nat_gateways(cid, vcn_id=vcn.id).data:
|
||||||
|
if nat.lifecycle_state == "AVAILABLE":
|
||||||
|
status = "bloqueado" if nat.block_traffic else "ATIVO"
|
||||||
|
record("NAT Gateway (temporário)", f"{nat.display_name} ({status})", nat.id, REGION)
|
||||||
|
|
||||||
|
|
||||||
|
USER_FAMILIES = [
|
||||||
|
"data-science-family",
|
||||||
|
"object-family",
|
||||||
|
"virtual-network-family",
|
||||||
|
"logging-family",
|
||||||
|
"repos",
|
||||||
|
"generative-ai-family",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_policy(idn, cid, name, description, statements):
|
||||||
|
pol = next((p for p in idn.list_policies(cid).data if p.name == name), None)
|
||||||
|
if pol is None:
|
||||||
|
pol = idn.create_policy(oci.identity.models.CreatePolicyDetails(
|
||||||
|
compartment_id=cid, name=name, description=description,
|
||||||
|
statements=statements, freeform_tags=TAGS)).data
|
||||||
|
elif sorted(pol.statements) != sorted(statements):
|
||||||
|
pol = idn.update_policy(pol.id, oci.identity.models.UpdatePolicyDetails(
|
||||||
|
description=description, statements=statements)).data
|
||||||
|
record("IAM Policy", f"{name} ({len(statements)} declarações)", pol.id, "global (home us-ashburn-1)")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_policies(idn, cid, user_id):
|
||||||
|
# Sem acesso de administrador à tenancy: nada de grupos nem dynamic groups.
|
||||||
|
# O usuário é identificado pelo OCID e os jobs pelo tipo de principal.
|
||||||
|
comp = f"compartment id {cid}"
|
||||||
|
user_stmts = [f"Allow any-user to manage {fam} in {comp} where request.user.id = '{user_id}'"
|
||||||
|
for fam in USER_FAMILIES]
|
||||||
|
ensure_policy(idn, cid, "dmb-usuario-manager",
|
||||||
|
"Usuário do benchmark como manager das famílias usadas", user_stmts)
|
||||||
|
|
||||||
|
job = f"request.principal.type = 'datasciencejob', request.principal.compartment.id = '{cid}'"
|
||||||
|
read_buckets = "any {target.bucket.name = 'dmb-entrada', target.bucket.name = 'dmb-artefatos'}"
|
||||||
|
write_buckets = "any {target.bucket.name = 'dmb-resultados', target.bucket.name = 'dmb-logs-imutaveis'}"
|
||||||
|
job_stmts = [
|
||||||
|
f"Allow any-user to read buckets in {comp} where all {{{job}}}",
|
||||||
|
f"Allow any-user to read objects in {comp} where all {{{job}, {read_buckets}}}",
|
||||||
|
f"Allow any-user to manage objects in {comp} where all {{{job}, {write_buckets}, "
|
||||||
|
"any {request.permission = 'OBJECT_CREATE', request.permission = 'OBJECT_INSPECT', request.permission = 'OBJECT_READ'}}",
|
||||||
|
f"Allow any-user to use log-content in {comp} where all {{{job}}}",
|
||||||
|
f"Allow any-user to read repos in {comp} where all {{{job}}}",
|
||||||
|
f"Allow service datascience to use virtual-network-family in {comp}",
|
||||||
|
]
|
||||||
|
ensure_policy(idn, cid, "dmb-jobs-menor-privilegio",
|
||||||
|
"Jobs de benchmark: leitura de entrada/artefatos e escrita de resultados/logs (tarefa 7)",
|
||||||
|
job_stmts)
|
||||||
|
|
||||||
|
|
||||||
|
def write_artifacts():
|
||||||
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||||
|
lines = [
|
||||||
|
"# Artefatos OCI — benchmark de modelos de decisão",
|
||||||
|
"",
|
||||||
|
f"Atualizado em {now} por `infra/oci/provision.py`. Região dos recursos: `{REGION}`.",
|
||||||
|
f"Tags livres aplicadas: {', '.join(f'`{k}={v}`' for k, v in TAGS.items())} (`centro_custo` pendente, Q13).",
|
||||||
|
"",
|
||||||
|
"| Tipo | Nome | OCID | Região |",
|
||||||
|
"|------|------|------|--------|",
|
||||||
|
]
|
||||||
|
lines += [f"| {k} | {n} | `{o}` | {r} |" for k, n, o, r in artifacts]
|
||||||
|
ARTIFACTS_FILE.write_text("\n".join(lines) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
home = load_config(region="us-ashburn-1")
|
||||||
|
cfg = load_config()
|
||||||
|
idn = oci.identity.IdentityClient(home)
|
||||||
|
cid = find_compartment(idn, home["tenancy"])
|
||||||
|
ensure_policies(idn, cid, home["user"])
|
||||||
|
ensure_ds_project(cfg, cid)
|
||||||
|
ensure_buckets(cfg, cid)
|
||||||
|
ensure_network(cfg, cid)
|
||||||
|
write_artifacts()
|
||||||
|
for a in artifacts:
|
||||||
|
print(" | ".join(a))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
25
infra/oci/session.py
Normal file
25
infra/oci/session.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Sessão OCI do benchmark a partir das credenciais em .secrets/oci-credentials/."""
|
||||||
|
import configparser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import oci
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
CRED_DIR = ROOT / ".secrets" / "oci-credentials"
|
||||||
|
REGION = "us-chicago-1"
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(region: str = REGION, profile: str = "DEFAULT") -> dict:
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
parser.read(CRED_DIR / "profile.config")
|
||||||
|
cfg = dict(parser[profile])
|
||||||
|
key_file = cfg.get("key_file", "").split("#")[0].strip()
|
||||||
|
if not key_file or not Path(key_file).expanduser().is_file():
|
||||||
|
keys = [p for p in CRED_DIR.glob("*.pem") if not p.name.endswith("_public.pem")]
|
||||||
|
if len(keys) != 1:
|
||||||
|
raise RuntimeError(f"esperada 1 chave privada em {CRED_DIR}, encontradas {len(keys)}")
|
||||||
|
key_file = str(keys[0])
|
||||||
|
cfg["key_file"] = key_file
|
||||||
|
cfg["region"] = region
|
||||||
|
oci.config.validate_config(cfg)
|
||||||
|
return cfg
|
||||||
0
reports/.gitkeep
Normal file
0
reports/.gitkeep
Normal file
Reference in New Issue
Block a user