Guides
Tutoriel : une application derrière un point de terminaison HTTP
Évaluez un service que vous atteignez en HTTP, sans importer son code : pointez Oloproof vers l’URL, exécutez la suite, trouvez ce qu’elle rate, déployez un changement, et comparez. Un petit service local remplace le vôtre, si bien que tout s’exécute hors ligne.
Ce que vous allez construire
Un service de prise de commandes qui lit un message client et en extrait deux champs : une intent (where_is_order, cancel, return ou other) et un order_id (quatre chiffres, ou null). Vous vérifierez le format de chaque réponse, ce qui ne demande aucune référence, et mesurerez si chaque champ est juste, ce qui en demande une. Les termes comme cas, exécution, métrique et porte sont définis dans les Concepts.
Prérequis
- Python 3.11 ou ultérieur, et Oloproof dans un environnement virtuel :
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- Le projet d’exemple, livré avec le paquet. Copiez-le dans un nouveau répertoire et travaillez-y (httpx, qu’importe client.py, est installé avec Oloproof) :
oloproof init --example http order-intake
cd order-intake- Le port 8765 libre sur cette machine. S’il est pris, choisissez-en un autre et changez-le à la fois dans la commande du serveur et dans oloproof.yaml.
Rien ici n’utilise de fournisseur, de clé d’API ni Internet : le service écoute sur 127.0.0.1.
Les fichiers
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 casesLancez les commandes depuis order-intake/. Démarrez le service dans un second terminal et laissez-le tourner :
python server.py --port 8765intake service (v1) on http://127.0.0.1:8765/extractLe contrat HTTP
Pour chaque cas, Oloproof envoie une requête : l’input du cas comme corps JSON, avec la méthode que vous déclarez (POST par défaut). Il lit la réponse comme du JSON. Un statut de 400 ou plus, un délai dépassé ou une connexion refusée est enregistré comme une erreur d’exécution pour ce cas, jamais comme une mauvaise réponse.
Requête et réponse pour un cas :
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 indique à Oloproof de ne garder que result comme sortie du cas ; sans lui, le corps entier est la sortie. Un chemin pointé comme data.answer va plus en profondeur.
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_idUn système HTTP doit déclarer une version. Oloproof ne peut pas voir un déploiement : il met en cache la sortie de chaque cas sous l’URL, la méthode, le chemin de sortie et cette version, si bien que la version est la façon de lui dire que le service a changé. Oubliez de la changer et un nouveau déploiement n’est jamais appelé.
Les en-têtes et l’authentification ne peuvent pas encore être configurés sur system.http. La section sur les jetons ci-dessous montre le contournement.
Le jeu de données
{"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 est exactement le corps de la requête. expected contient la référence de chaque champ ; null est une vraie valeur de référence, qui signifie « il n’y a pas d’identifiant de commande dans ce message ».
Choisir les évaluateurs
| Critère | Évaluateur | Exige expected | Mesure |
|---|---|---|---|
| format_valid | json_schema sur la sortie | non | le format : une intention connue et un identifiant bien formé |
| intent_correct | exact_match sur intent | oui | la réussite de la tâche pour le premier champ |
| order_id_correct | exact_match sur order_id | oui | la réussite de la tâche pour le second champ |
Le schéma accepterait {"intent": "other", "order_id": null} pour chaque message : bien formé et inutile. Seules les vérifications par référence disent si le service a fait son travail. Noter les champs séparément montre lequel échoue, ce qu’une vérification combinée masquerait.
release.yaml n’admet aucun échec de format et demande que chaque champ soit juste au moins 70% du temps, jugé sur l’intervalle à 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.70Exécutez-la
oloproof runRun 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 missChaque réponse est bien formée. Chaque champ est juste 15 fois sur 20, mais 20 cas laissent un intervalle qui descend à 50.8%, si bien qu’aucun des deux planchers de 70% n’est démontré : INSUFFICIENT_EVIDENCE, et la porte bloque avec le code de sortie 3.
Examiner les échecs
oloproof inspect RUN_ID --failures8 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: failedLisez les entrées à côté (oloproof inspect RUN_ID --case m04) et deux défauts apparaissent : le motif de l’identifiant ne reconnaît que « order 1234 », pas « order #5120 », « order no. 8123 » ni un simple « #1673 » ; et des formulations comme « send back », « stop order » et « where’s my parcel » ne correspondent à aucune intention. C’est l’action suivante : élargir les deux règles.
Déployer un changement
Arrêtez le service et démarrez le candidat, qui porte ces corrections :
python server.py --port 8765 --rules v2Changez version: rules-v1 en version: rules-v2 dans oloproof.yaml, parce que l’URL n’a pas changé et qu’Oloproof réutiliserait sinon les anciennes sorties. Puis :
oloproof runGate: 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 │Les deux planchers passent et la commande sort avec 0.
Comparer les deux déploiements
compare.yaml demande si les intentions du candidat sont meilleures que celles de la référence :
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
- id: intent-better
kind: superiority
metric: intent_correctoloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yamlformat_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)Le candidat satisfait ses propres planchers, et pourtant la comparaison ne peut pas montrer qu’il est meilleur : cinq cas ont changé, et sur 20 cas appariés l’intervalle du gain inclut encore zéro. Les deux affirmations sont vraies en même temps. « Satisfait l’exigence » et « bat la référence » sont des questions distinctes, et une suite aussi petite ne répond à la seconde que pour de grands effets. Davantage de vrais messages est le remède.
Un service qui exige un jeton
Démarrez le service de façon qu’il exige un jeton bearer :
INTAKE_TOKEN=s3cret python server.py --port 8765 --rules v2 --require-tokensystem.http n’envoie aucun en-tête personnalisé, si bien qu’avec une nouvelle version (disons rules-v2-auth) chaque appel est refusé :
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 │et oloproof inspect RUN_ID --failures affiche execution ERROR: TransientError: system returned HTTP 401 sur chaque cas. Les erreurs d’exécution sont des preuves manquantes, pas des échecs : rien n’a été observé, si bien que chaque règle est INSUFFICIENT_EVIDENCE.
Le contournement est un appelable Python qui fait lui-même la requête. client.py ajoute l’en-tête à partir d’une variable d’environnement, si bien que le jeton n’entre jamais dans oloproof.yaml ni dans aucun enregistrement stocké :
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"]Remplacez le bloc http: de oloproof.yaml par :
system:
name: order-intake
version: rules-v2
callable: client:extractet exécutez avec le jeton dans l’environnement :
INTAKE_TOKEN=s3cret oloproof runLes 20 cas sont de nouveau observés et la porte autorise. Le cache de l’appelable suit son propre source et sa version, pas le service qui est derrière, si bien que la même règle s’applique : changez version quand vous déployez.
Dépannage
| Symptôme | Cause et correction |
|---|---|
| system connection failed (ConnectError) sur chaque cas | Le service ne tourne pas, ou écoute sur un autre port. |
| system connection failed (RemoteProtocolError) | Autre chose répond sur ce port. Choisissez-en un libre. |
| KeyError: "missing output path 'results'" | output_path nomme un champ que la réponse n’a pas. |
| system returned HTTP 401 ou 403 | Le point de terminaison exige des identifiants : utilisez le contournement par appelable. |
| Vous avez déployé un changement et la ligne du cache n’indique que des succès | La version n’a pas changé, si bien que les sorties stockées ont été réutilisées. |
| an HTTP system needs a declared version | Ajoutez version sous system ou system.http. |
Limites
- Pas d’en-têtes personnalisés, d’authentification, de paramètres de requête ni de gabarit de requête sur system.http : l’input du cas est le corps JSON tel quel. Utilisez un appelable pour tout le reste.
- Les réponses doivent être du JSON. Les réponses en flux ne sont pas lues comme un flux.
- Oloproof ne peut pas détecter un déploiement ; la version déclarée est toute l’identité de ce qui a répondu.
- Votre service possède son état : Oloproof envoie des requêtes et enregistre les réponses ; il ne réinitialise, n’isole ni n’annule rien de ce qu’une requête modifie.