本文へスキップ

ガイド

チュートリアル:HTTP エンドポイントの背後にあるアプリケーション

HTTP で到達するサービスを、そのコードをインポートせずに評価します。Oloproof に URL を指定し、スイートを実行し、取りこぼしを見つけ、変更をデプロイして比較します。小さなローカルサービスがあなたのサービスの代わりを務めるので、すべてオフラインで動作します。

作るもの

顧客のメッセージを読み、2 つのフィールドを抽出する注文受付サービスです。intent(where_is_order、cancel、return、other のいずれか)と order_id(4 桁の数字、または null)です。すべての応答の形式を確認し(これには参照は不要です)、各フィールドが正しいかを測定します(これには参照が必要です)。ケース、実行、メトリクス、ゲートなどの用語は 基本概念 で定義しています。

前提条件

  • Python 3.11 以降と、仮想環境にインストールした Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • パッケージに同梱されているサンプルプロジェクト。新しいディレクトリにコピーし、そこで作業します(client.py がインポートする httpx は Oloproof と一緒にインストールされます):
oloproof init --example http order-intake
cd order-intake
  • このマシンでポート 8765 が空いていること。使われている場合は別のポートを選び、サーバーのコマンドと oloproof.yaml の両方で変更してください。

ここではプロバイダーも、API キーも、インターネットも使いません。サービスは 127.0.0.1 で待ち受けます。

ファイル

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

コマンドは order-intake/ から実行してください。2 つ目のターミナルでサービスを起動し、動かしたままにします:

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

HTTP の契約

各ケースについて、Oloproof はリクエストを 1 つ送ります。ケースの input を JSON ボディとし、宣言したメソッド(既定は POST)を使います。応答は JSON として読みます。400 以上のステータス、タイムアウト、接続拒否は、そのケースの実行エラーとして記録され、誤った回答として記録されることはありません。

1 ケースのリクエストと応答:

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 は、ケースの出力として result だけを残すよう Oloproof に指示します。これがなければボディ全体が出力になります。data.answer のようなドット区切りのパスでより深い位置に到達できます。

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

HTTP システムは version を宣言しなければなりません。Oloproof はデプロイを見ることができません。各ケースの出力を URL、メソッド、出力パス、そしてそのバージョンをキーとしてキャッシュするので、サービスが変わったことを伝える手段がバージョンです。変更し忘れると、新しいデプロイは決して呼び出されません。

ヘッダーと認証はまだ system.http で設定できません。下のトークンに関する節で回避策を示します。

データセット

{"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 はリクエストボディそのものです。expected は各フィールドの参照を保持します。null は実際の参照値で、「このメッセージには注文 ID がない」ことを意味します。

評価器を選ぶ

基準評価器expected が必要か測るもの
format_valid出力に対する json_schemaいいえ形式:既知のインテントと正しい形の ID
intent_correctintent に対する exact_matchはい1 つ目のフィールドのタスク成功
order_id_correctorder_id に対する exact_matchはい2 つ目のフィールドのタスク成功

スキーマは、どのメッセージに対しても {"intent": "other", "order_id": null} を合格にしてしまいます。形は正しくても役に立ちません。サービスが仕事をしたかどうかを示すのは参照チェックだけです。フィールドを別々に採点すると、どちらが失敗しているかがわかります。1 つにまとめたチェックではそれが隠れてしまいます。

release.yaml は形式の失敗を一切許さず、各フィールドが少なくとも 70% の割合で正しいことを、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

実行する

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

すべての応答は正しい形をしています。各フィールドは 20 回中 15 回正しいですが、20 ケースでは区間が 50.8% まで下がるため、どちらの 70% の下限も示されません。INSUFFICIENT_EVIDENCE となり、ゲートは終了コード 3 でブロックします。

失敗を調べる

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

横に入力を並べて読むと(oloproof inspect RUN_ID --case m04)、2 つの欠陥が現れます。ID のパターンは「order 1234」にしかマッチせず、「order #5120」「order no. 8123」や単独の「#1673」にはマッチしません。また、「send back」「stop order」「where's my parcel」といった言い回しはどのインテントにも対応しません。次の行動は、両方のルールを広げることです。

変更をデプロイする

サービスを停止し、それらの修正を含む候補を起動します:

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

oloproof.yaml の version: rules-v1 を version: rules-v2 に変更してください。URL は変わっていないので、そうしないと Oloproof は古い出力を再利用します。続いて:

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 │

両方の下限が合格し、コマンドは 0 で終了します。

2 つのデプロイを比較する

compare.yaml は、候補のインテントがベースラインより良いかを問います:

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)

候補は自身の下限には合格しますが、比較ではより良いことを示せません。変わったのは 5 ケースで、20 個の対応のあるケースでは、改善幅の区間はまだゼロを含みます。どちらの記述も同時に真です。「要件を満たす」と「ベースラインに勝る」は別の問いであり、これほど小さなスイートが 2 つ目に答えられるのは大きな効果の場合だけです。実際のメッセージを増やすことが対処法です。

トークンを必要とするサービス

ベアラートークンを要求するようにサービスを起動します:

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

system.http はカスタムヘッダーを送らないので、新しい version(たとえば rules-v2-auth)では、すべての呼び出しが拒否されます:

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 │

そして oloproof inspect RUN_ID --failures は各ケースで execution ERROR: TransientError: system returned HTTP 401 を表示します。実行エラーは失敗ではなく、欠けたエビデンスです。何も観測されていないので、すべてのルールは INSUFFICIENT_EVIDENCE になります。

回避策は、自らリクエストを行う Python の callable です。client.py は環境変数からヘッダーを付けるので、トークンが oloproof.yaml や保存されるレコードに入ることはありません:

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

oloproof.yaml の http: ブロックを次で置き換えます:

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

そして、環境にトークンを入れて実行します:

INTAKE_TOKEN=s3cret oloproof run

20 ケースすべてが再び観測され、ゲートは許可します。callable のキャッシュは背後のサービスではなく、callable 自身のソースと version に従うので、同じルールが当てはまります。デプロイしたら version を変更してください。

トラブルシューティング

症状原因と対処
すべてのケースで system connection failed (ConnectError)サービスが動いていないか、別のポートで待ち受けています。
system connection failed (RemoteProtocolError)そのポートで別の何かが応答しています。空いているポートを選んでください。
KeyError: "missing output path 'results'"output_path が、応答にないフィールドを指定しています。
system returned HTTP 401 または 403エンドポイントが認証情報を必要としています。callable による回避策を使ってください。
変更をデプロイしたのに、キャッシュの行がすべてヒットになるversion が変わっていないため、保存された出力が再利用されました。
an HTTP system needs a declared versionsystem または system.http の下に version を追加してください。

制限事項

  • system.http にはカスタムヘッダー、認証、クエリパラメーター、リクエストのテンプレート化がありません。ケースの input がそのまま JSON ボディになります。それ以外のことには callable を使ってください。
  • 応答は JSON でなければなりません。ストリーミング応答はストリームとしては読まれません。
  • Oloproof はデプロイを検出できません。宣言された version が、応答したものの同一性のすべてです。
  • 状態はサービス自身が管理します。Oloproof はリクエストを送って回答を記録しますが、リクエストが変更したものをリセットも、サンドボックス化も、ロールバックもしません。