Pular para o conteúdo

Guias

Tutorial: uma aplicação por trás de um endpoint HTTP

Avalie um serviço que você acessa por HTTP, sem importar o código dele: aponte o Oloproof para a URL, execute a suíte, descubra o que ele erra, implante uma mudança e compare. Um pequeno serviço local faz o papel do seu, então tudo roda offline.

O que você vai construir

Um serviço de recepção de pedidos que lê a mensagem de um cliente e extrai dois campos: um intent (where_is_order, cancel, return ou other) e um order_id (quatro dígitos, ou null). Você vai verificar o formato de toda resposta, o que não precisa de referência, e medir se cada campo está certo, o que precisa. Termos como caso, execução, métrica e gate são definidos em Conceitos.

Pré-requisitos

  • Python 3.11 ou posterior, e o Oloproof em um ambiente virtual:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • O projeto de exemplo, que vem com o pacote. Copie-o para um novo diretório e trabalhe nele (httpx, que client.py importa, é instalado com o Oloproof):
oloproof init --example http order-intake
cd order-intake
  • A porta 8765 livre nesta máquina. Se estiver ocupada, escolha outra e mude-a tanto no comando do servidor quanto em oloproof.yaml.

Nada aqui usa um provedor, uma chave de API ou a internet: o serviço escuta em 127.0.0.1.

Os arquivos

order-intake/
  server.py             the stand-in service (Python standard library only)
  client.py             a callable that calls the service with a token (used near the end)
  oloproof.yaml         the suite: dataset, HTTP system, evaluators
  release.yaml          rules for a run
  compare.yaml          a rule for a comparison
  data/messages.jsonl   20 cases

Execute os comandos a partir de order-intake/. Inicie o serviço em um segundo terminal e deixe-o rodando:

python server.py --port 8765
intake service (v1) on http://127.0.0.1:8765/extract

O contrato HTTP

Para cada caso, o Oloproof envia uma requisição: o input do caso como corpo JSON, com o método que você declarar (POST por padrão). Ele lê a resposta como JSON. Um status 400 ou acima, um timeout ou uma conexão recusada é registrado como erro de execução para aquele caso, nunca como resposta errada.

Requisição e resposta de um caso:

POST /extract
{"message": "Where is order 1042? It has not arrived."}

200 OK
{"result": {"intent": "where_is_order", "order_id": "1042"}, "service": {"rules": "v1"}}

output_path: result diz ao Oloproof para manter só result como saída do caso; sem isso, a saída é o corpo inteiro. Um caminho com pontos como data.answer vai mais fundo.

version: 1
project: order-intake
dataset: data/messages.jsonl
system:
  name: order-intake
  http:
    url: http://127.0.0.1:8765/extract
    method: POST
    version: rules-v1
    output_path: result
    timeout_s: 30
evaluators:
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [intent, order_id]
      properties:
        intent: {enum: [where_is_order, cancel, return, other]}
        order_id: {type: [string, "null"], pattern: '^\d{4}$'}
      additionalProperties: false
  - type: exact_match
    criterion: intent_correct
    field: intent
  - type: exact_match
    criterion: order_id_correct
    field: order_id

Um sistema HTTP precisa declarar uma version. O Oloproof não consegue ver uma implantação: ele coloca em cache a saída de cada caso pela URL, pelo método, pelo caminho da saída e por essa versão, então a versão é a forma de dizer a ele que o serviço mudou. Se você esquecer de mudá-la, uma nova implantação nunca é chamada.

Cabeçalhos e autenticação ainda não podem ser configurados em system.http. A seção sobre tokens abaixo mostra a alternativa.

O dataset

{"id":"m04","input":{"message":"Has order #5120 shipped yet?"},"expected":{"intent":"where_is_order","order_id":"5120"}}
{"id":"m07","input":{"message":"Do you ship to Canada?"},"expected":{"intent":"other","order_id":null}}
{"id":"m16","input":{"message":"Please refund and take back the lamp from order #1560."},"expected":{"intent":"return","order_id":"1560"}}

input é exatamente o corpo da requisição. expected contém a referência de cada campo; null é um valor de referência real, que significa "não há id de pedido nesta mensagem".

Escolhendo os avaliadores

CritérioAvaliadorPrecisa de expectedMede
format_validjson_schema sobre a saídanãoformato: um intent conhecido e um id bem formado
intent_correctexact_match em intentsimsucesso da tarefa no primeiro campo
order_id_correctexact_match em order_idsimsucesso da tarefa no segundo campo

O schema aprovaria {"intent": "other", "order_id": null} para toda mensagem: bem formado e inútil. Só as verificações contra a referência dizem se o serviço fez o seu trabalho. Avaliar os campos separadamente mostra qual deles falha, o que uma única verificação combinada esconderia.

release.yaml não permite nenhuma falha de formato e pede que cada campo esteja certo em pelo menos 70% das vezes, julgado pelo intervalo de 95%:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: intent-floor
    metric: intent_correct
    min: 0.70
  - id: order-id-floor
    metric: order_id_correct
    min: 0.70

Execute

oloproof run
Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ valid-format   │ format_valid     │ PASS                  │ observed_failures_within_limit │
│ intent-floor   │ intent_correct   │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ order-id-floor │ order_id_correct │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ format_valid     │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ intent_correct   │ 75.0%    │ [50.8%, 91.4%]  │ 15 / 20 observed · 0 missing · 0 excluded │
│ order_id_correct │ 75.0%    │ [50.8%, 91.4%]  │ 15 / 20 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/20 miss; judgment 0 hit/60 miss

Toda resposta é bem formada. Cada campo está certo 15 vezes em 20, mas 20 casos deixam um intervalo que desce até 50,8%, então nenhum dos pisos de 70% é demonstrado: INSUFFICIENT_EVIDENCE, e o gate bloqueia com saída 3.

Inspecione as falhas

oloproof inspect RUN_ID --failures
8 of 20 cases failed, errored or did not finish

m04
  output: {"intent": "where_is_order", "order_id": null}
  order_id_correct: failed

m06
  output: {"intent": "other", "order_id": "7011"}
  intent_correct: failed
...
m18
  output: {"intent": "other", "order_id": null}
  intent_correct: failed
  order_id_correct: failed

Leia as entradas ao lado delas (oloproof inspect RUN_ID --case m04) e aparecem dois defeitos: o padrão do id só reconhece "order 1234", não "order #5120", "order no. 8123" nem um "#1673" sozinho; e formulações como "send back", "stop order" e "where's my parcel" não correspondem a nenhum intent. Essa é a próxima ação: ampliar as duas regras.

Implante uma mudança

Pare o serviço e inicie o candidato, que traz essas correções:

python server.py --port 8765 --rules v2

Mude version: rules-v1 para version: rules-v2 em oloproof.yaml, porque a URL não mudou e, sem isso, o Oloproof reaproveitaria as saídas antigas. Depois:

oloproof run
Gate: ALLOW (exit 0)
│ intent_correct   │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ order_id_correct │ 100.0%   │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │

Os dois pisos passam e o comando sai com 0.

Compare as duas implantações

compare.yaml pergunta se os intents do candidato são melhores do que os da linha de base:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: intent-better
    kind: superiority
    metric: intent_correct
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
format_valid: +0.0 points [-23.6, +23.6] · 20 paired · 0 missing · 0 excluded
intent_correct: +25.0 points [-8.5, +58.3] · 20 paired · 0 missing · 0 excluded
order_id_correct: +25.0 points [-8.5, +58.3] · 20 paired · 0 missing · 0 excluded
Decisions
  intent-better  intent_correct  superiority  INSUFFICIENT_EVIDENCE  interval_overlaps_zero
Gate: BLOCK (exit 3)

O candidato passa nos seus próprios pisos, mas a comparação não consegue mostrar que ele é melhor: cinco casos mudaram, e sobre 20 casos pareados o intervalo do ganho ainda inclui o zero. As duas afirmações são verdadeiras ao mesmo tempo. "Atende ao requisito" e "supera a linha de base" são perguntas separadas, e uma suíte tão pequena só responde à segunda para efeitos grandes. O remédio é ter mais mensagens reais.

Um serviço que exige um token

Inicie o serviço de forma que ele exija um bearer token:

INTAKE_TOKEN=s3cret python server.py --port 8765 --rules v2 --require-token

system.http não envia cabeçalhos personalizados, então com uma nova version (digamos rules-v2-auth) toda chamada é recusada:

Gate: BLOCK (exit 3)
│ valid-format   │ format_valid     │ INSUFFICIENT_EVIDENCE │ no_observations │
│ intent_correct   │          │ [0.0%, 100.0%] │ 0 / 0 observed · 20 missing · 0 excluded │

e oloproof inspect RUN_ID --failures mostra execution ERROR: TransientError: system returned HTTP 401 em cada caso. Erros de execução são evidência faltante, não falhas: nada foi observado, então toda regra é INSUFFICIENT_EVIDENCE.

A alternativa é um callable Python que faz a requisição por conta própria. client.py acrescenta o cabeçalho a partir de uma variável de ambiente, então o token nunca entra em oloproof.yaml nem em nenhum registro armazenado:

URL = os.environ.get("INTAKE_URL", "http://127.0.0.1:8765/extract")


def extract(case: dict[str, Any]) -> dict[str, Any]:
    headers = {"Authorization": f"Bearer {os.environ['INTAKE_TOKEN']}"}
    response = httpx.post(URL, json=case, headers=headers, timeout=30)
    response.raise_for_status()
    return response.json()["result"]

Substitua o bloco http: em oloproof.yaml por:

system:
  name: order-intake
  version: rules-v2
  callable: client:extract

e execute com o token no ambiente:

INTAKE_TOKEN=s3cret oloproof run

Os 20 casos são observados de novo e o gate libera. O cache do callable segue o seu próprio código-fonte e a sua version, não o serviço por trás dele, então a mesma regra vale: mude version quando implantar.

Solução de problemas

SintomaCausa e correção
system connection failed (ConnectError) em todo casoO serviço não está rodando, ou escuta em outra porta.
system connection failed (RemoteProtocolError)Outra coisa responde nessa porta. Escolha uma livre.
KeyError: "missing output path 'results'"output_path nomeia um campo que a resposta não tem.
system returned HTTP 401 ou 403O endpoint precisa de credenciais: use a alternativa com o callable.
Você implantou uma mudança e a linha do cache mostra só hitsA version não mudou, então as saídas armazenadas foram reaproveitadas.
an HTTP system needs a declared versionAcrescente version em system ou system.http.

Limitações

  • Nada de cabeçalhos personalizados, autenticação, parâmetros de query ou templates de requisição em system.http: o input do caso é o corpo JSON como está. Use um callable para qualquer outra coisa.
  • As respostas precisam ser JSON. Respostas em streaming não são lidas como stream.
  • O Oloproof não consegue detectar uma implantação; a version declarada é toda a identidade do que respondeu.
  • O seu serviço cuida do próprio estado: o Oloproof envia requisições e registra respostas; ele não reinicia, não isola e não desfaz nada que uma requisição altere.