Saltar al contenido

Guías

Tutorial: una aplicación detrás de un endpoint HTTP

Evalúe un servicio al que accede por HTTP, sin importar su código: apunte Oloproof a la URL, ejecute la suite, encuentre lo que falla, despliegue un cambio y compare. Un pequeño servicio local ocupa el lugar del suyo, así que todo funciona sin conexión.

Lo que va a construir

Un servicio de recepción de pedidos que lee el mensaje de un cliente y extrae dos campos: un intent (where_is_order, cancel, return u other) y un order_id (cuatro dígitos, o null). Comprobará el formato de cada respuesta, lo que no necesita referencia, y medirá si cada campo es correcto, lo que sí la necesita. Términos como caso, ejecución, métrica y gate se definen en Conceptos.

Requisitos previos

  • Python 3.11 o posterior, y Oloproof en un entorno virtual:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • El proyecto de ejemplo, que viene con el paquete. Cópielo en un directorio nuevo y trabaje allí (httpx, que importa client.py, se instala con Oloproof):
oloproof init --example http order-intake
cd order-intake
  • El puerto 8765 libre en esta máquina. Si está ocupado, elija otro y cámbielo tanto en el comando del servidor como en oloproof.yaml.

Nada de esto usa un proveedor, una clave de API ni internet: el servicio escucha en 127.0.0.1.

Los archivos

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

Ejecute los comandos desde order-intake/. Inicie el servicio en un segundo terminal y déjelo en marcha:

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

El contrato HTTP

Para cada caso, Oloproof envía una petición: el input del caso como cuerpo JSON, con el método que usted declare (POST por defecto). Lee la respuesta como JSON. Un estado de 400 o superior, un tiempo de espera agotado o una conexión rechazada se registran como un error de ejecución de ese caso, nunca como una respuesta incorrecta.

Petición y respuesta para 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 indica a Oloproof que conserve solo result como salida del caso; sin él, la salida es el cuerpo entero. Una ruta con puntos como data.answer llega más hondo.

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 debe declarar una version. Oloproof no puede ver un despliegue: guarda en caché la salida de cada caso bajo la URL, el método, la ruta de salida y esa versión, así que la versión es la forma de decirle que el servicio cambió. Si olvida cambiarla, nunca se llama a un despliegue nuevo.

Las cabeceras y la autenticación todavía no se pueden configurar en system.http. La sección sobre tokens, más abajo, muestra la alternativa.

El conjunto de datos

{"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 es exactamente el cuerpo de la petición. expected contiene la referencia de cada campo; null es un valor de referencia real, que significa "no hay ningún id de pedido en este mensaje".

Elegir los evaluadores

CriterioEvaluadorNecesita expectedMide
format_validjson_schema sobre la salidanoformato: un intent conocido y un id bien formado
intent_correctexact_match sobre intentsíéxito de la tarea para el primer campo
order_id_correctexact_match sobre order_idsíéxito de la tarea para el segundo campo

El esquema aprobaría {"intent": "other", "order_id": null} para cualquier mensaje: bien formado e inútil. Solo las comprobaciones con referencia dicen si el servicio hizo su trabajo. Puntuar los campos por separado muestra cuál falla, algo que una comprobación combinada ocultaría.

release.yaml no admite ningún fallo de formato y pide que cada campo sea correcto al menos el 70% de las veces, juzgado sobre el intervalo 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

Ejecútelo

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

Todas las respuestas están bien formadas. Cada campo es correcto 15 veces de 20, pero 20 casos dejan un intervalo que baja hasta el 50.8%, así que ninguno de los dos mínimos del 70% queda demostrado: INSUFFICIENT_EVIDENCE, y el gate bloquea con salida 3.

Inspeccionar los fallos

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

Lea las entradas al lado (oloproof inspect RUN_ID --case m04) y aparecen dos defectos: el patrón del id solo reconoce "order 1234", no "order #5120", "order no. 8123" ni un "#1673" suelto; y expresiones como "send back", "stop order" y "where's my parcel" no corresponden a ningún intent. Esa es la siguiente acción: ampliar ambas reglas.

Desplegar un cambio

Detenga el servicio e inicie el candidato, que incluye esas correcciones:

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

Cambie version: rules-v1 por version: rules-v2 en oloproof.yaml, porque la URL no cambió y Oloproof reutilizaría si no las salidas antiguas. Después:

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 │

Ambos mínimos se cumplen y el comando termina con 0.

Comparar los dos despliegues

compare.yaml pregunta si los intents del candidato son mejores que los de la línea 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)

El candidato cumple sus propios mínimos y, sin embargo, la comparación no puede demostrar que sea mejor: cambiaron cinco casos y, sobre 20 casos emparejados, el intervalo de la mejora todavía incluye el cero. Ambas afirmaciones son ciertas a la vez. "Cumple el requisito" y "supera a la línea base" son preguntas distintas, y una suite tan pequeña solo responde a la segunda para efectos grandes. El remedio es tener más mensajes reales.

Un servicio que necesita un token

Inicie el servicio de modo que exija un token bearer:

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

system.http no envía cabeceras personalizadas, así que con una version nueva (por ejemplo rules-v2-auth) se rechazan todas las llamadas:

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 │

y oloproof inspect RUN_ID --failures muestra execution ERROR: TransientError: system returned HTTP 401 en cada caso. Los errores de ejecución son evidencia que falta, no fallos: no se observó nada, así que cada regla queda en INSUFFICIENT_EVIDENCE.

La alternativa es un callable de Python que haga la petición por sí mismo. client.py añade la cabecera a partir de una variable de entorno, así que el token nunca entra en oloproof.yaml ni en ningún registro almacenado:

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

Sustituya el bloque http: de oloproof.yaml por:

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

y ejecute con el token en el entorno:

INTAKE_TOKEN=s3cret oloproof run

Los 20 casos vuelven a observarse y el gate permite la publicación. La caché del callable sigue su propio código fuente y su version, no el servicio que hay detrás, así que se aplica la misma regla: cambie version cuando despliegue.

Solución de problemas

SíntomaCausa y solución
system connection failed (ConnectError) en todos los casosEl servicio no está en marcha, o escucha en otro puerto.
system connection failed (RemoteProtocolError)Otra cosa responde en ese puerto. Elija uno libre.
KeyError: "missing output path 'results'"output_path nombra un campo que la respuesta no tiene.
system returned HTTP 401 o 403El endpoint necesita credenciales: use la alternativa del callable.
Desplegó un cambio y la línea de caché indica todo aciertosLa version no cambió, así que se reutilizaron las salidas almacenadas.
an HTTP system needs a declared versionAñada version bajo system o system.http.

Limitaciones

  • Sin cabeceras personalizadas, autenticación, parámetros de consulta ni plantillas de petición en system.http: el input del caso es el cuerpo JSON tal cual. Use un callable para cualquier otra cosa.
  • Las respuestas deben ser JSON. Las respuestas en streaming no se leen como un flujo.
  • Oloproof no puede detectar un despliegue; la version declarada es toda la identidad de lo que respondió.
  • Su servicio es dueño de su estado: Oloproof envía peticiones y registra respuestas; no restablece, aísla ni revierte nada de lo que una petición cambie.