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
1143 lines
41 KiB
Python
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()),
|
|
)
|