Files
wehub-resource-sync 5a558eb09e
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / render-equality (push) Has been cancelled
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Has been cancelled
Python BE E2E Tests / Python BE E2E (push) Has been cancelled
Python Backend Tests / run-python-backend-tests (push) Has been cancelled
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
SDK E2E Libraries Integration Tests / Check Secrets (push) Has been cancelled
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Has been cancelled
SDK E2E Libraries Integration Tests / build-opik (push) Has been cancelled
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Has been cancelled
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Has been cancelled
TypeScript SDK Build & Publish / build-and-publish (push) Has been cancelled
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Has been cancelled
Backend Tests / discover-tests (push) Has been cancelled
Backend Tests / ${{ matrix.name }} (push) Has been cancelled
Build and Publish SDK / build-and-publish (push) Has been cancelled
Build Opik Docker Images / set-version (push) Has been cancelled
Build Opik Docker Images / build-backend (push) Has been cancelled
Build Opik Docker Images / build-sandbox-executor-python (push) Has been cancelled
Build Opik Docker Images / build-python-backend (push) Has been cancelled
Build Opik Docker Images / build-frontend (push) Has been cancelled
Build Opik Docker Images / create-git-tag (push) Has been cancelled
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Has been cancelled
Docs - Publish / run (push) Has been cancelled
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Has been cancelled
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Has been cancelled
Frontend Unit Tests / Test on Node 20 (push) Has been cancelled
Guardrails E2E Tests / Select Python version matrix (push) Has been cancelled
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Guardrails E2E Tests / 📢 Slack Notification (push) Has been cancelled
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Has been cancelled
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Has been cancelled
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Has been cancelled
Lint Opik Helm Chart / unittest-helm-chart (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:44 +08:00

1143 lines
41 KiB
Python

"""
E2E tests for ``opik.evaluate_resume`` against a real Opik backend.
Each test follows the same narrative, top to bottom:
1. Build a dataset.
2. Run ``opik.evaluate()`` — sometimes with a task that crashes mid-way
to simulate an interruption.
3. Verify the original run's outcome via the ``EvaluationResult`` it
returned, or — when the run raised — via the experiment record.
4. Run ``opik.evaluate_resume()`` with a working task and the same
metrics + scoring_key_mapping the user originally supplied.
5. Verify that resume re-ran only the missing items, and that the
experiment converged to the expected final state.
All assertions go through user-facing API: the ``EvaluationResult``
returned by ``evaluate`` / ``evaluate_resume``, ``verify_experiment(...)``,
and ``verify_experiment_items_completed(...)``. Local checkpoint files and
internal resume state are implementation details and are never inspected
directly.
"""
from typing import Any, Dict, Set
import pytest
import opik
from opik import id_helpers
from opik.evaluation import metrics, samplers
from opik.evaluation.metrics import base_metric, score_result
from .. import verifiers
from ...testlib import generate_project_name
PROJECT_NAME = generate_project_name("e2e", __name__)
# --- helpers --------------------------------------------------------------
def _items_with_labels(labels):
"""
Build dataset.insert payload + label↔uuid maps.
The backend requires dataset item ``id`` to be a real UUID. Tests need
stable labels (``item-0``, ``item-3``, ...) for readable assertions
about which items crashed / got resumed / etc. This helper bridges the
two: each label gets a generated UUID stored under ``id``, and the
label travels alongside as part of the item content so tasks can
reference it.
"""
ids_by_label = {label: id_helpers.generate_id() for label in labels}
labels_by_id = {uid: label for label, uid in ids_by_label.items()}
payload = [
{
"id": ids_by_label[label],
"input": {"text": label},
"expected_output": label,
}
for label in labels
]
return payload, ids_by_label, labels_by_id
def _experiment_id_after_failed_evaluate(opik_client, experiment_name) -> str:
"""
Recover the experiment id when the original ``evaluate()`` raised — the
engine re-raises the first task exception, so the ``EvaluationResult``
is unavailable. The experiment record itself is created before task
execution, so it exists even when the run crashed mid-way.
"""
experiments = opik_client.get_experiments_by_name(
experiment_name, project_name=PROJECT_NAME
)
assert len(experiments) == 1, (
f"Expected 1 experiment named {experiment_name}, got {len(experiments)}"
)
return experiments[0].id
# === Core scenarios =======================================================
def test_evaluate_resume__happy_path__metrics_and_mapping_round_trip(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Original ``evaluate()`` completes every item with an ``Equals`` metric
and a ``scoring_key_mapping`` that renames ``expected_output`` to
``reference``. ``evaluate_resume()`` finds nothing pending — the task
is never invoked, and the experiment is unchanged.
"""
# 1. Dataset: 3 items whose `expected_output` matches what `echo_task`
# will return — every Equals score is 1.0.
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(
[
{"input": {"text": "hello"}, "expected_output": "hello"},
{"input": {"text": "world"}, "expected_output": "world"},
{"input": {"text": "test"}, "expected_output": "test"},
]
)
def echo_task(item: Dict[str, Any]):
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
expected_all_ids: Set[str] = {item["id"] for item in dataset.get_items()}
# 2. Original evaluate — every item runs to completion.
result = opik.evaluate(
dataset=dataset,
task=echo_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
# 3. Verify: 3 test results, each with an Equals score of 1.0.
assert len(result.test_results) == 3
for test_result in result.test_results:
assert len(test_result.score_results) == 1
assert test_result.score_results[0].value == 1.0
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=expected_all_ids,
)
# 4. Resume — re-supply the metrics and mapping (the framework cannot
# persist them: they are user-side Python objects).
resume_invocations = []
def resume_task(item: Dict[str, Any]):
resume_invocations.append(item["id"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=result.experiment_id,
task=resume_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. Verify: resume was a no-op on the task side, but the returned
# EvaluationResult describes the full experiment — all 3 items are
# present (reconstructed from their stored scores), each still 1.0.
assert resume_invocations == []
assert len(resume_result.test_results) == 3
for test_result in resume_result.test_results:
assert test_result.score_results[0].value == 1.0
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=expected_all_ids,
)
def test_evaluate_resume__failure_during_evaluate__continue_works(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Flow 1: original ``evaluate()`` crashes on a subset of items. A single
``evaluate_resume()`` call completes the missing items and the
experiment converges to "all items completed".
"""
# 1. 5-item dataset. Labels (``item-N``) double as the input text so
# tasks can pick them out without touching the UUID ``id`` field.
labels = [f"item-{i}" for i in range(5)]
items, ids_by_label, labels_by_id = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
all_uuids = set(ids_by_label.values())
failed_labels = {"item-3", "item-4"}
failed_uuids = {ids_by_label[label] for label in failed_labels}
def crashing_task(item: Dict[str, Any]):
if item["input"]["text"] in failed_labels:
raise RuntimeError(f"simulated crash on {item['input']['text']}")
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate — expected to raise after the first crash.
try:
opik.evaluate(
dataset=dataset,
task=crashing_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except Exception:
pass # see _experiment_id_after_failed_evaluate docstring
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# 3. Verify partial state: only the 3 non-crashing items completed.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=all_uuids - failed_uuids,
)
# 4. Resume with a working task.
resume_invocations = []
def working_task(item: Dict[str, Any]):
resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=experiment_id,
task=working_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. Verify: only the failed items were re-invoked by the task, but the
# returned EvaluationResult describes the full experiment — all 5
# items appear, every score is 1.0.
assert set(resume_invocations) == failed_labels
assert len(resume_result.test_results) == 5
for test_result in resume_result.test_results:
assert test_result.score_results[0].value == 1.0
assert {tr.test_case.dataset_item_id for tr in resume_result.test_results} == (
all_uuids
)
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=all_uuids,
)
def test_evaluate_resume__failure_during_continue__second_continue_works(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Flow 2: original ``evaluate()`` fails on two items; the first
``evaluate_resume()`` fixes one of them but crashes on the other; a
second ``evaluate_resume()`` finishes the remaining item.
This verifies that resume reads its state fresh from the experiment on
every call — there is no in-memory "we already tried this" state that
would prevent a second resume from picking up the still-pending item.
"""
# 1. 5-item dataset; labels stand in for ids in task-side logic.
labels = [f"item-{i}" for i in range(5)]
items, ids_by_label, labels_by_id = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
all_uuids = set(ids_by_label.values())
def uuids_of(label_set):
return {ids_by_label[label] for label in label_set}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate — items 3 and 4 crash.
def original_task(item: Dict[str, Any]):
label = item["input"]["text"]
if label in {"item-3", "item-4"}:
raise RuntimeError(f"original crash on {label}")
return {"output": label}
try:
opik.evaluate(
dataset=dataset,
task=original_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except Exception:
pass
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# 3. After the original run: items 0, 1, 2 are done; items 3 and 4 are pending.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=uuids_of({"item-0", "item-1", "item-2"}),
)
# 4a. First resume — fixes item-3, but a different bug crashes item-4.
first_resume_invocations = []
def first_resume_task(item: Dict[str, Any]):
label = item["input"]["text"]
first_resume_invocations.append(label)
if label == "item-4":
raise RuntimeError("still flaky on item-4")
return {"output": label}
try:
opik.evaluate_resume(
experiment_id=experiment_id,
task=first_resume_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
except Exception:
pass
# First resume saw exactly the two previously-pending items; item-3
# finished, item-4 still pending.
assert set(first_resume_invocations) == {"item-3", "item-4"}
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=uuids_of(
{"item-0", "item-1", "item-2", "item-3"}
),
)
# 4b. Second resume — bug fixed, item-4 completes.
second_resume_invocations = []
def second_resume_task(item: Dict[str, Any]):
second_resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
opik.evaluate_resume(
experiment_id=experiment_id,
task=second_resume_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. Verify: second resume only touched the still-pending item-4, and
# the experiment now shows every item completed.
assert second_resume_invocations == ["item-4"]
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=all_uuids,
)
def test_evaluate_resume__nonexistent_experiment__raises(
opik_client: opik.Opik,
):
"""Clean error path: resuming an id that does not exist raises."""
with pytest.raises(opik.exceptions.ExperimentNotFound):
opik.evaluate_resume(
experiment_id=id_helpers.generate_id(),
task=lambda _item: {"output": "x"},
verbose=0,
)
# === Iteration config variants ============================================
def test_evaluate_resume__dataset_filter_string__filter_replayed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
The original run filtered to ``category = "geo"``. Resume must replay
the same filter; items outside the filter must stay out of scope.
"""
# 1. 4 items in two categories.
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(
[
{"input": {"text": "q1"}, "expected_output": "q1", "category": "geo"},
{"input": {"text": "q2"}, "expected_output": "q2", "category": "math"},
{"input": {"text": "q3"}, "expected_output": "q3", "category": "geo"},
{"input": {"text": "q4"}, "expected_output": "q4", "category": "math"},
]
)
def echo_task(item: Dict[str, Any]):
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate — filter selects 2 of 4 items.
result = opik.evaluate(
dataset=dataset,
task=echo_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
dataset_filter_string='data.category = "geo"',
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
# 3. Verify exactly 2 items processed; capture their ids for the
# converged-state check.
assert len(result.test_results) == 2
selected_ids = {tr.test_case.dataset_item_id for tr in result.test_results}
assert len(selected_ids) == 2
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=selected_ids,
)
# 4. Resume — same filter is replayed; both items already done.
resume_invocations = []
def task_for_resume(item: Dict[str, Any]):
resume_invocations.append(item["id"])
return {"output": item["input"]["text"]}
opik.evaluate_resume(
experiment_id=result.experiment_id,
task=task_for_resume,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. The 2 math items must never reach the task; the completed set is
# unchanged.
assert resume_invocations == []
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=selected_ids,
)
def test_evaluate_resume__dataset_item_ids__only_selected_items_resumed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
When the original run passed explicit ``dataset_item_ids``, resume must
iterate the same ids — items outside the selection must stay out of
scope even though they exist in the dataset.
"""
# 1. 4 items; we'll select the first two by id.
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
ids = [id_helpers.generate_id() for _ in range(4)]
dataset.insert(
[
{"id": ids[i], "input": {"text": f"v{i}"}, "expected_output": f"v{i}"}
for i in range(4)
]
)
selected_ids = ids[:2]
failed_id = ids[1]
successful_selected_id = ids[0]
def crashing_task(item: Dict[str, Any]):
if item["id"] == failed_id:
raise RuntimeError("crash on selected id")
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate runs only the selected ids; one crashes.
try:
opik.evaluate(
dataset=dataset,
task=crashing_task,
dataset_item_ids=selected_ids,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except Exception:
pass
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# 3. Only the non-failing selected id is completed so far.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids={successful_selected_id},
)
# 4. Resume — only the failed selected id should run again; the two
# unselected items must never reach the task.
resume_invocations = []
def working_task(item: Dict[str, Any]):
resume_invocations.append(item["id"])
return {"output": item["input"]["text"]}
opik.evaluate_resume(
experiment_id=experiment_id,
task=working_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. Verify: only the failed selected id was re-run; both selected ids
# are now completed; the two unselected ids never entered scope.
assert resume_invocations == [failed_id]
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=set(selected_ids),
)
def test_evaluate_resume__random_sampler__only_sampled_items_resumed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Original run sampled 3 items out of 10 with a ``RandomDatasetSampler``.
Resume must iterate the exact same 3 sampled items.
"""
# 1. 10-item dataset.
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
ids = [id_helpers.generate_id() for _ in range(10)]
dataset.insert(
[
{"id": ids[i], "input": {"text": f"v{i}"}, "expected_output": f"v{i}"}
for i in range(10)
]
)
def echo_task(item: Dict[str, Any]):
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate samples 3 of 10.
result = opik.evaluate(
dataset=dataset,
task=echo_task,
dataset_sampler=samplers.RandomDatasetSampler(max_samples=3, seed=42),
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
# 3. Verify only 3 items processed; capture their ids.
assert len(result.test_results) == 3
sampled_ids = {tr.test_case.dataset_item_id for tr in result.test_results}
assert len(sampled_ids) == 3
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=sampled_ids,
)
# 4. Resume — same 3 sampled ids replayed; all already done.
resume_invocations = []
def task_for_resume(item: Dict[str, Any]):
resume_invocations.append(item["id"])
return {"output": item["input"]["text"]}
opik.evaluate_resume(
experiment_id=result.experiment_id,
task=task_for_resume,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. The 7 unsampled items must never reach the task; converged set
# remains the same 3.
assert resume_invocations == []
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=sampled_ids,
)
def test_evaluate_resume__nb_samples__only_sampled_count_replayed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Original run capped iteration at ``nb_samples=3`` against a 5-item
dataset. Resume must replay the same cap against the same
(version-pinned) dataset; the unsampled items must stay out of scope.
"""
# 1. 5-item dataset (labels carried in input.text for readability).
labels = [f"item-{i}" for i in range(5)]
items, _ids_by_label, _labels_by_id = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
def echo_task(item: Dict[str, Any]):
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate limits to 3 items.
result = opik.evaluate(
dataset=dataset,
task=echo_task,
nb_samples=3,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
# 3. Verify only 3 items processed.
assert len(result.test_results) == 3
capped_ids = {tr.test_case.dataset_item_id for tr in result.test_results}
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=capped_ids,
)
# 4. Resume — nb_samples=3 replayed against the pinned version; same 3
# items returned by the stream; all already done.
resume_invocations = []
def task_for_resume(item: Dict[str, Any]):
resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
opik.evaluate_resume(
experiment_id=result.experiment_id,
task=task_for_resume,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. No re-runs — the 2 unsampled items must stay out of scope.
assert resume_invocations == []
verifiers.verify_experiment_items_completed(
opik_client,
result.experiment_id,
expected_completed_dataset_item_ids=capped_ids,
)
# === Trials ===============================================================
def test_evaluate_resume__trial_count__partial_item_replays_only_missing_runs(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Trials of the same item are independent: with ``trial_count=3``, the
original task succeeds on the first run then crashes on the second
(item ends up 1-of-3 completed). Resume must replay **only the 2
missing runs** — the one completed run is reconstructed alongside,
so the merged result has 3 runs total.
"""
# 1. Single-item dataset (keeps the trial bookkeeping simple). Backend
# requires UUIDs for the ``id`` field, so we generate one upfront
# and pin the verifier to it.
the_item_id = id_helpers.generate_id()
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(
[{"id": the_item_id, "input": {"text": "value"}, "expected_output": "value"}]
)
# Task that succeeds on its first invocation and crashes thereafter.
call_counter = {"count": 0}
def flaky_task(item: Dict[str, Any]):
call_counter["count"] += 1
if call_counter["count"] > 1:
raise RuntimeError("crash on later trial")
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate with trial_count=3 — first trial succeeds, the
# second crashes and the engine re-raises.
try:
opik.evaluate(
dataset=dataset,
task=flaky_task,
trial_count=3,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except Exception:
pass
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# 3. The item has at least one completed trial (the first one).
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids={the_item_id},
)
# 4. Resume with a non-crashing task. The item had 1 of 3 runs done,
# so resume should replay only the 2 missing runs.
resume_invocations = []
def working_task(item: Dict[str, Any]):
resume_invocations.append(item["id"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=experiment_id,
task=working_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. Only the 2 missing runs replayed.
assert resume_invocations == [the_item_id, the_item_id], (
f"Only missing runs should be replayed; got {resume_invocations}"
)
# The merged EvaluationResult has 3 runs total: 1 reconstructed +
# 2 freshly replayed.
assert len(resume_result.test_results) == 3
assert all(
tr.test_case.dataset_item_id == the_item_id for tr in resume_result.test_results
)
assert all(tr.score_results[0].value == 1.0 for tr in resume_result.test_results)
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids={the_item_id},
)
def test_evaluate_resume__mixed_partial_and_fully_completed_items(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
With ``trial_count=2`` over three items, the original run leaves a mix:
- item-0 fully completed (2 of 2 trials)
- item-1 partially done (1 of 2 trials — second trial crashed)
- item-2 fully completed (2 of 2 trials)
The engine submits every trial up front and the executor only re-raises
the first failure after collecting all results, so item-1's crash does
not prevent item-2's trials from running. The interesting partial state
is item-1.
Resume must:
- leave item-0 alone (no task invocations; stored trials reconstructed)
- replay only the 1 missing run for item-1 (trials are independent)
- leave item-2 alone (no task invocations; stored trials reconstructed)
The merged result has 5 reconstructed (2 + 1 + 2) + 1 fresh = 6 trials.
"""
# 1. Three items. Labels carried as input text so the task can pick
# them out without touching the UUID ``id`` field.
labels = [f"item-{i}" for i in range(3)]
items, ids_by_label, _labels_by_id = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
# Original task: crashes on item-1's SECOND call; everything else
# succeeds (including all of item-2's trials).
call_log = []
def flaky_task(item: Dict[str, Any]):
label = item["input"]["text"]
call_log.append(label)
is_item_1_second_call = label == "item-1" and call_log.count("item-1") == 2
if is_item_1_second_call:
raise RuntimeError("crash on item-1 second trial")
return {"output": label}
scoring_key_mapping = {"reference": "expected_output"}
# 2. Original evaluate — single-threaded so the trial order is
# deterministic and item-1 fails on its 2nd trial as designed.
try:
opik.evaluate(
dataset=dataset,
task=flaky_task,
trial_count=2,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except Exception:
pass
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# 3. All three items have at least one successful trial logged — the
# failure on item-1's second trial does not stop the executor from
# completing item-2's trials.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=set(ids_by_label.values()),
)
# 4. Resume with a working task.
resume_invocations = []
def working_task(item: Dict[str, Any]):
resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=experiment_id,
task=working_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. item-0 fully completed (2/2 successful) → no resume invocations.
# item-1 partial (1/2 successful) → only the 1 missing run replays.
# item-2 fully completed (2/2 successful) → no resume invocations.
counts_by_label = {label: resume_invocations.count(label) for label in labels}
assert counts_by_label == {"item-0": 0, "item-1": 1, "item-2": 0}, (
f"Unexpected resume task invocation distribution: {counts_by_label}"
)
# Merged result: 2 reconstructed for item-0 + 1 reconstructed + 1 fresh
# for item-1 + 2 reconstructed for item-2 = 6 trials total.
assert len(resume_result.test_results) == 6
counts_in_result = {
label: sum(
1
for tr in resume_result.test_results
if tr.test_case.dataset_item_id == ids_by_label[label]
)
for label in labels
}
assert counts_in_result == {"item-0": 2, "item-1": 2, "item-2": 2}
# All three items end up in the converged completed set.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=set(ids_by_label.values()),
)
# === Marker-design failure modes ==========================================
class _MetricRaisingBaseException(base_metric.BaseMetric):
"""
Metric that succeeds on most items but raises ``BaseException`` on a
chosen subset. ``BaseException`` (not ``Exception``) escapes the
per-metric ``except Exception`` handler inside the engine, so the
failure propagates past scoring even though the task itself returned
cleanly. End result: the trial's trace is written with ``output`` set
(task succeeded) and the pending marker still at ``True`` (scoring
never reached the happy-path-only line that clears it).
This is the failure mode the marker design exists to detect — the old
``evaluation_task_output is not None`` predicate would have classified
the trial as fully completed and resume would have skipped it.
"""
def __init__(self, failing_labels: Set[str]) -> None:
super().__init__(name="raises_on_subset")
self._failing_labels = failing_labels
def score(
self, output: str, reference: str, **ignored_kwargs: Any
) -> score_result.ScoreResult:
if output in self._failing_labels:
# SystemExit is a BaseException; the engine's per-metric
# except-clause catches Exception only, so this escapes.
raise SystemExit(f"simulated scoring crash on label={output!r}")
return score_result.ScoreResult(
name=self.name,
value=1.0 if output == reference else 0.0,
)
def test_evaluate_resume__scoring_crash_after_task_success__trial_replayed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
The case the marker design exists for: the task succeeds (so the
trace's ``output`` is set), but a metric raises ``BaseException``
mid-scoring. The trial is recorded with output set but the marker
still at ``True``. Resume must read the marker and replay.
Under the pre-marker predicate (``evaluation_task_output is not None``)
these items would be misclassified as fully completed and silently
skipped on resume.
"""
# 1. 3-item dataset. Single-threaded scoring keeps the failure
# deterministic regardless of submission order.
labels = [f"item-{i}" for i in range(3)]
items, ids_by_label, _ = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
scoring_will_fail = {"item-1"}
fully_ok_uuids = {
ids_by_label[label] for label in labels if label not in scoring_will_fail
}
def working_task(item: Dict[str, Any]):
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output", "output": "output"}
# 2. Original evaluate — task is healthy, but the metric raises on
# ``item-1``. The simulated crash is ``SystemExit`` (a
# ``BaseException`` subclass) so it escapes the engine's
# ``except Exception`` handler; we catch it narrowly here so any
# unrelated ``KeyboardInterrupt`` / ``GeneratorExit`` is not
# silently swallowed.
try:
opik.evaluate(
dataset=dataset,
task=working_task,
scoring_metrics=[
_MetricRaisingBaseException(failing_labels=scoring_will_fail)
],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except SystemExit:
pass
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# 3. Verify the partial state from the marker's point of view:
# item-0 and item-2 reached the happy-path line and count as
# completed; item-1 did not (its scoring crashed) and is excluded
# even though its task wrote output to the trace.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=fully_ok_uuids,
)
# 4. Resume with a healthy task + a metric that never raises.
resume_invocations: list = []
def resume_task(item: Dict[str, Any]):
resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=experiment_id,
task=resume_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
# 5. Only the scoring-failed item was replayed; the two items that
# cleared their happy-path line were left alone.
assert resume_invocations == ["item-1"], (
f"Only the scoring-failed item should be replayed; got {resume_invocations}"
)
assert len(resume_result.test_results) == 3
assert all(tr.score_results[0].value == 1.0 for tr in resume_result.test_results)
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=set(ids_by_label.values()),
)
def test_evaluate_resume__metric_scoring_failed_inside_loop__not_replayed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Counterpart to the BaseException case: when a metric raises a regular
``Exception`` (or returns ``scoring_failed=True``), the engine catches
it inside the per-metric loop and the scoring step still reaches the
happy-path line. The trial is fully completed (marker flipped to
``False``), and resume must NOT replay it — even though the stored
feedback score is missing or marked as failed.
This regression-guards the "scoring loop reached its end" semantics
against future changes to the marker logic.
"""
labels = [f"item-{i}" for i in range(3)]
items, ids_by_label, _ = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
metric_will_fail_on = {"item-1"}
class _MetricRaisingException(base_metric.BaseMetric):
def __init__(self) -> None:
super().__init__(name="raises_caught_by_engine")
def score(
self, output: str, reference: str, **ignored_kwargs: Any
) -> score_result.ScoreResult:
if output in metric_will_fail_on:
raise RuntimeError(f"caught simulated failure on {output!r}")
return score_result.ScoreResult(
name=self.name,
value=1.0 if output == reference else 0.0,
)
def working_task(item: Dict[str, Any]):
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output", "output": "output"}
# 2. Evaluate runs to completion — RuntimeError is caught inside the
# metric loop (engine converts it to ``ScoreResult(scoring_failed=True)``),
# so the scoring step still returns and the happy-path marker is
# cleared on every trial.
evaluate_result = opik.evaluate(
dataset=dataset,
task=working_task,
scoring_metrics=[_MetricRaisingException()],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
assert len(evaluate_result.test_results) == 3
experiment_id = evaluate_result.experiment_id
# 3. All three items have cleared markers; resume should treat the
# experiment as fully completed.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=set(ids_by_label.values()),
)
# 4. Resume with a healthy metric — none of the items should be
# re-invoked, even item-1 whose only stored score is failed.
resume_invocations: list = []
def resume_task(item: Dict[str, Any]):
resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=experiment_id,
task=resume_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
assert resume_invocations == [], (
"Items with a cleared marker must not be replayed even when the "
f"stored score is failed; got resume invocations: {resume_invocations}"
)
assert len(resume_result.test_results) == 3
def test_evaluate_resume__mixed_task_and_scoring_failures__only_failed_items_replayed(
opik_client: opik.Opik, dataset_name: str, experiment_name: str
):
"""
Combined coverage: one item fails in the task, one fails in scoring
(BaseException), one completes happily. Resume must replay exactly the
two failed items — distinguishing them from the happy one purely via
the marker.
"""
labels = ["task_fails", "scoring_fails", "all_good"]
items, ids_by_label, _ = _items_with_labels(labels)
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
dataset.insert(items)
def task_failing_for_one(item: Dict[str, Any]):
if item["input"]["text"] == "task_fails":
raise RuntimeError("simulated task crash on task_fails")
return {"output": item["input"]["text"]}
scoring_key_mapping = {"reference": "expected_output", "output": "output"}
# ``_MetricRaisingBaseException`` raises ``SystemExit`` (a
# ``BaseException`` subclass) on the scoring-failure label; the task
# raises ``RuntimeError`` on the task-failure label. Catch the scoring
# crash narrowly so we don't mask unrelated ``KeyboardInterrupt`` /
# ``GeneratorExit``; the ``RuntimeError`` is consumed inside the
# engine and does not escape.
try:
opik.evaluate(
dataset=dataset,
task=task_failing_for_one,
scoring_metrics=[
_MetricRaisingBaseException(failing_labels={"scoring_fails"})
],
scoring_key_mapping=scoring_key_mapping,
experiment_name=experiment_name,
task_threads=1,
verbose=0,
)
except SystemExit:
pass
experiment_id = _experiment_id_after_failed_evaluate(opik_client, experiment_name)
# Only the all-good item finished the happy path.
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids={ids_by_label["all_good"]},
)
resume_invocations: list = []
def resume_task(item: Dict[str, Any]):
resume_invocations.append(item["input"]["text"])
return {"output": item["input"]["text"]}
resume_result = opik.evaluate_resume(
experiment_id=experiment_id,
task=resume_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping=scoring_key_mapping,
verbose=0,
)
assert set(resume_invocations) == {"task_fails", "scoring_fails"}
assert len(resume_result.test_results) == 3
verifiers.verify_experiment_items_completed(
opik_client,
experiment_id,
expected_completed_dataset_item_ids=set(ids_by_label.values()),
)