Vai al contenuto

Guide

Tutorial: un'applicazione dietro un endpoint HTTP

Valuta un servizio che raggiungi via HTTP, senza importarne il codice: punta Oloproof all'URL, esegui la suite, trova che cosa sbaglia, distribuisci una modifica e confronta. Un piccolo servizio locale fa le veci del tuo, quindi tutto gira offline.

Che cosa costruirai

Un servizio di acquisizione ordini che legge il messaggio di un cliente ed estrae due campi: un intent (where_is_order, cancel, return o other) e un order_id (quattro cifre, oppure null). Controllerai il formato di ogni risposta, che non richiede un riferimento, e misurerai se ogni campo è corretto, che invece lo richiede. Termini come caso, esecuzione, metrica e gate sono definiti in Concetti.

Prerequisiti

  • Python 3.11 o successivo, e Oloproof in un ambiente virtuale:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • Il progetto di esempio, incluso nel pacchetto. Copialo in una nuova directory e lavora lì (httpx, che client.py importa, viene installato con Oloproof):
oloproof init --example http order-intake
cd order-intake
  • La porta 8765 libera su questa macchina. Se è occupata, scegline un'altra e cambiala sia nel comando del server sia in oloproof.yaml.

Nulla qui usa un provider, una chiave API o internet: il servizio ascolta su 127.0.0.1.

I file

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

Esegui i comandi da order-intake/. Avvia il servizio in un secondo terminale e lascialo in esecuzione:

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

Il contratto HTTP

Per ogni caso Oloproof invia una richiesta: l'input del caso come corpo JSON, con il metodo che dichiari (POST per impostazione predefinita). Legge la risposta come JSON. Uno stato pari o superiore a 400, un timeout o una connessione rifiutata viene registrato come errore di esecuzione per quel caso, mai come risposta sbagliata.

Richiesta e risposta per un 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 dice a Oloproof di tenere solo result come output del caso; senza di esso l'output è l'intero corpo. Un percorso puntato come data.answer arriva più in profondità.

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

Un sistema HTTP deve dichiarare una version. Oloproof non può vedere un deployment: mette in cache l'output di ogni caso in base all'URL, al metodo, al percorso dell'output e a quella versione, quindi la versione è il modo in cui gli dici che il servizio è cambiato. Se dimentichi di cambiarla, un nuovo deployment non viene mai chiamato.

Header e autenticazione non si possono ancora configurare su system.http. La sezione sui token qui sotto mostra il ripiego.

Il 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 è esattamente il corpo della richiesta. expected contiene il riferimento per ogni campo; null è un vero valore di riferimento, che significa "in questo messaggio non c'è un id d'ordine".

Scegliere i valutatori

CriterioValutatoreRichiede expectedMisura
format_validjson_schema sull'outputnoformato: un intent noto e un id ben formato
intent_correctexact_match su intentsìsuccesso del compito per il primo campo
order_id_correctexact_match su order_idsìsuccesso del compito per il secondo campo

Lo schema farebbe passare {"intent": "other", "order_id": null} per ogni messaggio: ben formato e inutile. Solo i controlli sul riferimento dicono se il servizio ha fatto il suo lavoro. Valutare i campi separatamente mostra quale fallisce, cosa che un unico controllo combinato nasconderebbe.

release.yaml non ammette fallimenti di formato e chiede che ogni campo sia corretto almeno il 70% delle volte, giudicato sull'intervallo al 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

Eseguilo

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

Ogni risposta è ben formata. Ogni campo è corretto 15 volte su 20, ma 20 casi lasciano un intervallo che scende fino al 50,8%, quindi nessuna delle due soglie del 70% è dimostrata: INSUFFICIENT_EVIDENCE, e il gate blocca con uscita 3.

Esamina i fallimenti

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

Leggi gli input accanto a essi (oloproof inspect RUN_ID --case m04) e compaiono due difetti: il pattern dell'id riconosce solo "order 1234", non "order #5120", "order no. 8123" né un "#1673" isolato; e formulazioni come "send back", "stop order" e "where's my parcel" non corrispondono ad alcun intent. Questa è l'azione successiva: ampliare entrambe le regole.

Distribuisci una modifica

Ferma il servizio e avvia il candidato, che contiene quelle correzioni:

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

Cambia version: rules-v1 in version: rules-v2 in oloproof.yaml, perché l'URL non è cambiato e altrimenti Oloproof riutilizzerebbe i vecchi output. Poi:

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 │

Entrambe le soglie passano e il comando esce con 0.

Confronta i due deployment

compare.yaml chiede se gli intent del candidato sono migliori di quelli della baseline:

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)

Il candidato supera le proprie soglie, eppure il confronto non può mostrare che sia migliore: sono cambiati cinque casi, e su 20 casi appaiati l'intervallo del guadagno include ancora lo zero. Entrambe le affermazioni sono vere allo stesso tempo. "Soddisfa il requisito" e "batte la baseline" sono domande separate, e una suite così piccola risponde alla seconda solo per effetti grandi. Il rimedio sono più messaggi reali.

Un servizio che richiede un token

Avvia il servizio in modo che richieda un bearer token:

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

system.http non invia header personalizzati, quindi con una nuova version (per esempio rules-v2-auth) ogni chiamata viene rifiutata:

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 su ogni caso. Gli errori di esecuzione sono evidenze mancanti, non fallimenti: non è stato osservato nulla, quindi ogni regola è INSUFFICIENT_EVIDENCE.

Il ripiego è un callable Python che fa la richiesta da sé. client.py aggiunge l'header da una variabile d'ambiente, così il token non entra mai in oloproof.yaml né in alcun record memorizzato:

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"]

Sostituisci il blocco http: in oloproof.yaml con:

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

ed esegui con il token nell'ambiente:

INTAKE_TOKEN=s3cret oloproof run

Tutti i 20 casi vengono di nuovo osservati e il gate lascia passare. La cache del callable segue il suo sorgente e la sua version, non il servizio dietro di esso, quindi vale la stessa regola: cambia version quando distribuisci.

Risoluzione dei problemi

SintomoCausa e rimedio
system connection failed (ConnectError) su ogni casoIl servizio non è in esecuzione, o ascolta su un'altra porta.
system connection failed (RemoteProtocolError)Qualcos'altro risponde su quella porta. Scegline una libera.
KeyError: "missing output path 'results'"output_path nomina un campo che la risposta non ha.
system returned HTTP 401 o 403L'endpoint richiede credenziali: usa il ripiego con il callable.
Hai distribuito una modifica e la riga della cache riporta solo hitLa version non è cambiata, quindi sono stati riutilizzati gli output memorizzati.
an HTTP system needs a declared versionAggiungi version sotto system o system.http.

Limitazioni

  • Niente header personalizzati, autenticazione, parametri di query o template di richiesta su system.http: l'input del caso è il corpo JSON così com'è. Usa un callable per qualsiasi altra cosa.
  • Le risposte devono essere JSON. Le risposte in streaming non vengono lette come stream.
  • Oloproof non può rilevare un deployment; la version dichiarata è l'intera identità di ciò che ha risposto.
  • Il tuo servizio gestisce il proprio stato: Oloproof invia richieste e registra risposte; non reimposta, non isola e non annulla nulla di ciò che una richiesta modifica.