项目文件夹

文件
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

4106 行
138 KiB
Python

import logging
from contextlib import contextmanager
from typing import Any, Dict, List
from unittest import mock
import pytest
import opik
from opik import evaluation, exceptions, rest_api, url_helpers, PromptType
from opik.api_objects import opik_client, prompt
from opik.api_objects.dataset import dataset_item
from opik.api_objects.experiment import experiment
from opik.evaluation import (
evaluator as evaluator_module,
helpers as helpers_module,
metrics,
samplers,
score_statistics,
)
from opik.evaluation.engine import engine
from opik.evaluation.metrics import score_result
from opik.evaluation.models import models_factory
from opik.evaluation.evaluator import _build_prompt_evaluation_task
from ...testlib import ANY_BUT_NONE, ANY_STRING, ANY_LIST, SpanModel, assert_equal
from ...testlib.models import FeedbackScoreModel, TraceModel
def create_mock_dataset(
name: str = "the-dataset-name",
items: List[dataset_item.DatasetItem] = None,
) -> mock.MagicMock:
"""Create a mock dataset with streaming support."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"project_name",
]
)
mock_dataset.name = name
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
if items is not None:
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
items
)
return mock_dataset
def create_mock_experiment() -> tuple[mock.Mock, mock.Mock, mock.Mock]:
"""Create mock experiment and related mocks for patching.
Returns:
Tuple of (mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id)
"""
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_experiment.id = "exp-mock-id"
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
return mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id
def create_mock_model(
model_name: str = "gpt-3.5-turbo",
response_content: str = "Hello, world!",
) -> tuple[mock.Mock, mock.Mock]:
"""Create mock model and factory for evaluate_prompt tests.
Returns:
Tuple of (mock_models_factory_get, mock_model)
"""
mock_models_factory_get = mock.Mock()
mock_model = mock.Mock()
mock_model.model_name = model_name
mock_model.generate_provider_response.return_value = mock.Mock(
choices=[mock.Mock(message=mock.Mock(content=response_content))]
)
mock_models_factory_get.return_value = mock_model
return mock_models_factory_get, mock_model
@contextmanager
def patch_evaluation_dependencies(
mock_create_experiment: mock.Mock,
mock_get_experiment_url_by_id: mock.Mock,
mock_models_factory_get: mock.Mock = None,
):
"""Context manager to patch evaluation dependencies.
Args:
mock_create_experiment: Mock for opik_client.Opik.create_experiment
mock_get_experiment_url_by_id: Mock for url_helpers.get_experiment_url_by_id
mock_models_factory_get: Optional mock for models_factory.get (for evaluate_prompt tests)
"""
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
if mock_models_factory_get is not None:
with mock.patch.object(
models_factory,
"get",
mock_models_factory_get,
):
yield
else:
yield
def test_evaluate__happyflow(
fake_backend,
):
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
if dataset_item["input"]["message"] == "say hello":
return {"output": "hello"}
if dataset_item["input"]["message"] == "say bye":
return {"output": "not bye"}
raise Exception
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
experiment_tags = ["one", "two", "three"]
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
experiment_tags=experiment_tags,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=experiment_tags,
dataset_version_id=None,
project_name=None,
)
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
EXPECTED_TRACE_TREES = [
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
},
output={
"output": "hello",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
},
},
output={
"output": "hello",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input={
"test_case_": ANY_BUT_NONE,
"trial_id": 0,
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="equals_metric",
input={
"ignored_kwargs": {
"input": {"message": "say hello"},
"id": "dataset-item-id-1",
},
"output": "hello",
"reference": "hello",
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
],
source="experiment",
),
],
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=1.0,
)
],
source="experiment",
),
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say bye"},
"reference": "bye",
"id": "dataset-item-id-2",
},
output={
"output": "not bye",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say bye"},
"reference": "bye",
"id": "dataset-item-id-2",
}
},
output={"output": "not bye"},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input={
"test_case_": ANY_BUT_NONE,
"trial_id": 0,
},
output={"output": ANY_BUT_NONE},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="equals_metric",
input={
"ignored_kwargs": {
"input": {"message": "say bye"},
"id": "dataset-item-id-2",
},
"output": "not bye",
"reference": "bye",
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
)
],
source="experiment",
),
],
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=0.0,
)
],
source="experiment",
),
]
for expected_trace, actual_trace in zip(
EXPECTED_TRACE_TREES, fake_backend.trace_trees
):
assert_equal(expected_trace, actual_trace)
def test_evaluate__prompts_are_attached_to_each_trace(fake_backend):
"""When prompts are passed to `evaluate`, every trace produced by the
evaluation run must carry them in `metadata["opik_prompts"]` so the
backend can show prompt linkage on each trace (not only on the
experiment row)."""
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
prompts = [
prompt.Prompt.from_fern_prompt_version(
name="system_prompt",
prompt_version=rest_api.PromptVersionDetail(
template="You are a helpful assistant.",
commit="abc123",
type=PromptType.MUSTACHE,
),
),
prompt.Prompt.from_fern_prompt_version(
name="user_prompt",
prompt_version=rest_api.PromptVersionDetail(
template="Say what the user asks.",
commit="def456",
type=PromptType.MUSTACHE,
),
),
]
expected_prompts_metadata = [p.__internal_api__to_info_dict__() for p in prompts]
def say_task(item: Dict[str, Any]):
if item["input"]["message"] == "say hello":
return {"output": "hello"}
return {"output": "bye"}
(
mock_experiment,
mock_create_experiment,
mock_get_experiment_url_by_id,
) = create_mock_experiment()
# The engine reads prompts off the experiment object it receives, so the
# mocked experiment must expose them (create_experiment is mocked here).
mock_experiment.prompts = prompts
with patch_evaluation_dependencies(
mock_create_experiment, mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="experiment-with-prompts",
scoring_metrics=[metrics.Equals()],
prompts=prompts,
task_threads=1,
)
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="experiment-with-prompts",
experiment_config=mock.ANY,
prompts=prompts,
tags=None,
dataset_version_id=None,
project_name=None,
)
assert len(fake_backend.trace_trees) == 2
for actual_trace in fake_backend.trace_trees:
assert actual_trace.metadata is not None, (
"Trace metadata must not be None when prompts are passed to evaluate"
)
assert actual_trace.metadata.get("opik_prompts") == expected_prompts_metadata
def test_evaluate_prompt__prompt_attached_to_each_trace(fake_backend):
"""`evaluate_prompt` should also attach the prompt to each generated trace."""
MODEL_NAME = "gpt-3.5-turbo"
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
]
)
prompt_obj = prompt.Prompt.from_fern_prompt_version(
name="single_prompt",
prompt_version=rest_api.PromptVersionDetail(
template="LLM response: {{question}}",
commit="cafe01",
type=PromptType.MUSTACHE,
),
)
expected_prompt_metadata = [prompt_obj.__internal_api__to_info_dict__()]
(
mock_experiment,
mock_create_experiment,
mock_get_experiment_url_by_id,
) = create_mock_experiment()
# The engine reads prompts off the experiment object it receives, so the
# mocked experiment must expose them (create_experiment is mocked here).
mock_experiment.prompts = [prompt_obj]
mock_models_factory_get, _ = create_mock_model(model_name=MODEL_NAME)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
mock_models_factory_get=mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[{"role": "user", "content": "LLM response: {{question}}"}],
experiment_name="prompt-experiment",
model=MODEL_NAME,
prompt=prompt_obj,
scoring_metrics=[metrics.Equals()],
task_threads=1,
)
assert len(fake_backend.trace_trees) == 1
actual_trace = fake_backend.trace_trees[0]
assert actual_trace.metadata is not None
assert actual_trace.metadata.get("opik_prompts") == expected_prompt_metadata
def test_evaluate_with_scoring_key_mapping(
fake_backend,
):
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
expected_output={"message": "hello"},
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
expected_output={"message": "bye"},
),
]
)
def say_task(dataset_item: Dict[str, Any]):
if dataset_item["input"]["message"] == "say hello":
return {"result": "hello"}
if dataset_item["input"]["message"] == "say bye":
return {"result": "not bye"}
raise Exception
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
scoring_key_mapping={
"output": "result",
"reference": lambda x: x["expected_output"]["message"],
},
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
EXPECTED_TRACE_TREES = [
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say hello"},
"expected_output": {"message": "hello"},
"id": "dataset-item-id-1",
},
output={
"result": "hello",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say hello"},
"expected_output": {"message": "hello"},
"id": "dataset-item-id-1",
},
},
output={
"result": "hello",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input={
"test_case_": ANY_BUT_NONE,
"trial_id": 0,
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="equals_metric",
input={
"ignored_kwargs": {
"expected_output": {"message": "hello"},
"input": {"message": "say hello"},
"result": "hello",
"id": "dataset-item-id-1",
},
"output": "hello",
"reference": "hello",
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
],
source="experiment",
),
],
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=1.0,
)
],
source="experiment",
),
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say bye"},
"expected_output": {"message": "bye"},
"id": "dataset-item-id-2",
},
output={
"result": "not bye",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say bye"},
"expected_output": {"message": "bye"},
"id": "dataset-item-id-2",
},
},
output={
"result": "not bye",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input={
"test_case_": ANY_BUT_NONE,
"trial_id": 0,
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="equals_metric",
input={
"ignored_kwargs": {
"expected_output": {"message": "bye"},
"input": {"message": "say bye"},
"result": "not bye",
"id": "dataset-item-id-2",
},
"output": "not bye",
"reference": "bye",
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
)
],
source="experiment",
),
],
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=0.0,
)
],
source="experiment",
),
]
for expected_trace, actual_trace in zip(
EXPECTED_TRACE_TREES, fake_backend.trace_trees
):
assert_equal(expected_trace, actual_trace)
def test_evaluate___output_key_is_missing_in_task_output_dict__equals_metric_misses_output_argument__exception_raised():
# Dataset is the only thing which is mocked for this test because
# evaluate should raise an exception right after the first attempt
# to compute Equals metric score.
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
expected_output={"message": "hello"},
),
]
)
def say_task(dataset_item: Dict[str, Any]):
if dataset_item["input"]["message"] == "say hello":
return {
"the-key-that-is-not-named-output": "hello",
"reference": dataset_item["expected_output"]["message"],
}
raise Exception
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with pytest.raises(exceptions.ScoreMethodMissingArguments):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
def test_evaluate__exception_raised_from_the_task__error_info_added_to_the_trace(
fake_backend,
):
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
raise Exception("some-error-message")
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with pytest.raises(Exception):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[],
task_threads=1,
)
opik.flush_tracker()
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
mock_experiment.insert.assert_called_once_with(
experiment_items_references=[mock.ANY]
)
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
},
output=None,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
error_info={
"exception_type": "Exception",
"message": "some-error-message",
"traceback": ANY_STRING,
},
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
}
},
error_info={
"exception_type": "Exception",
"message": "some-error-message",
"traceback": ANY_STRING,
},
output=None,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
],
source="experiment",
)
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
def test_evaluate__with_random_sampler__happy_flow(
fake_backend,
):
# Creates a dataset with 5 items and then evaluates it using a random dataset sampler with 3 samples limit.
# Checks that only three samples are selected and that the metrics are computed for the three samples.
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
# When dataset_sampler is provided, streaming is used but exhausted to a list
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "hi there"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-3",
input={"message": "how are you"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-4",
input={"message": "say bye"},
reference="bye",
),
dataset_item.DatasetItem(
id="dataset-item-id-5",
input={"message": "see ya"},
reference="bye",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
if dataset_item["reference"] == "hello":
return {"output": "hello"}
if dataset_item["reference"] == "bye":
return {"output": "not bye"}
raise Exception
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
# create a random sampler with 3 samples limit
sampler = samplers.RandomDatasetSampler(max_samples=3)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_sampler=sampler,
)
# When dataset_sampler is provided, streaming is still used but exhausted to a list
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
# Due to the random nature of the sampler, we need to verify the structure
# and that exactly 3 samples were selected, but not specific dataset items
actual_traces = fake_backend.trace_trees
assert len(actual_traces) == 3, f"Expected 3 traces, got {len(actual_traces)}"
# Verify each trace has the expected values
# Checks business logic consistency based on the reference value:
# - If reference is "hello" → output should be "hello" and score should be 1.0
# - If reference is "bye" → output should be "not bye" and score should be 0.0
for actual_trace in actual_traces:
# Verify feedback scores
assert len(actual_trace.feedback_scores) == 1
feedback_score = actual_trace.feedback_scores[0]
assert feedback_score.name == "equals_metric"
assert feedback_score.value in [0.0, 1.0] # Should be either 0 or 1
# Verify task behavior based on reference value
reference = actual_trace.input["reference"]
expected_output = "hello" if reference == "hello" else "not bye"
expected_score = 1.0 if reference == "hello" else 0.0
assert actual_trace.output["output"] == expected_output
assert feedback_score.value == expected_score
def test_evaluate__with_random_sampler__total_items_reflects_sampled_count(
fake_backend,
):
"""Test that total_items passed to executor reflects the sampled count, not the original dataset size."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = 10 # Original dataset has 10 items
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
# Return 10 items
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id=f"dataset-item-id-{i}",
input={"message": f"message {i}"},
reference="hello",
)
for i in range(10)
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
# Create a sampler that will reduce to 3 items
sampler = samplers.RandomDatasetSampler(max_samples=3)
# Patch the engine's _compute_test_results_with_execution_policy to capture total_items
captured_total_items = []
original_compute = (
engine.EvaluationEngine._compute_test_results_with_execution_policy
)
def patched_compute(self, *args, **kwargs):
captured_total_items.append(kwargs.get("total_items"))
return original_compute(self, *args, **kwargs)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with mock.patch.object(
engine.EvaluationEngine,
"_compute_test_results_with_execution_policy",
patched_compute,
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_sampler=sampler,
)
# Verify that total_items was 3 (sampled count), not 10 (original dataset size)
assert len(captured_total_items) == 1
assert captured_total_items[0] == 3, (
f"Expected total_items to be 3 (sampled count), "
f"but got {captured_total_items[0]} (original dataset size)"
)
# Also verify that only 3 items were actually processed
actual_traces = fake_backend.trace_trees
assert len(actual_traces) == 3, f"Expected 3 traces, got {len(actual_traces)}"
def test_evaluate__with_task_span_metrics__total_items_reflects_actual_count(
fake_backend,
):
"""Test that total_items is correct when task_span_metrics forces non-streaming mode."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = 5
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
# Return 5 items
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id=f"dataset-item-id-{i}",
input={"message": f"message {i}"},
reference="hello",
)
for i in range(5)
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
# Create a task span metric to force non-streaming mode
class TaskSpanMetric(metrics.base_metric.BaseMetric):
def score(self, **kwargs):
return score_result.ScoreResult(name="task_span_metric", value=1.0)
@property
def track_task_span(self) -> bool:
return True
# Patch the engine's _compute_test_results_for_llm_task to capture total_items
captured_total_items = []
original_compute = (
engine.EvaluationEngine._compute_test_results_with_execution_policy
)
def patched_compute(self, *args, **kwargs):
captured_total_items.append(kwargs.get("total_items"))
return original_compute(self, *args, **kwargs)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with mock.patch.object(
engine.EvaluationEngine,
"_compute_test_results_with_execution_policy",
patched_compute,
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[TaskSpanMetric()],
task_threads=1,
)
# Verify that total_items was 5 (actual count from non-streaming list)
assert len(captured_total_items) == 1
assert captured_total_items[0] == 5, (
f"Expected total_items to be 5 (actual list length), "
f"but got {captured_total_items[0]}"
)
# Also verify that 5 items were actually processed
actual_traces = fake_backend.trace_trees
assert len(actual_traces) == 5, f"Expected 5 traces, got {len(actual_traces)}"
def test_evaluate__with_sampler_and_nb_samples__total_items_reflects_final_count(
fake_backend,
):
"""Test that total_items is correct when both nb_samples and dataset_sampler are used."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = 100 # Original dataset has 100 items
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
# nb_samples=10 will fetch 10 items
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id=f"dataset-item-id-{i}",
input={"message": f"message {i}"},
reference="hello",
)
for i in range(10) # 10 items fetched due to nb_samples
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
# Create a sampler that will further reduce to 3 items
sampler = samplers.RandomDatasetSampler(max_samples=3)
# Patch the engine's _compute_test_results_for_llm_task to capture total_items
captured_total_items = []
original_compute = (
engine.EvaluationEngine._compute_test_results_with_execution_policy
)
def patched_compute(self, *args, **kwargs):
captured_total_items.append(kwargs.get("total_items"))
return original_compute(self, *args, **kwargs)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with mock.patch.object(
engine.EvaluationEngine,
"_compute_test_results_with_execution_policy",
patched_compute,
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
nb_samples=10, # First filter: 10 items
dataset_sampler=sampler, # Second filter: 3 items
)
# Verify that total_items was 3 (final sampled count), not 10 (nb_samples) or 100 (dataset size)
assert len(captured_total_items) == 1
assert captured_total_items[0] == 3, (
f"Expected total_items to be 3 (final sampled count), "
f"but got {captured_total_items[0]}"
)
# Verify streaming was called with nb_samples
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=10,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=None,
)
# Also verify that only 3 items were actually processed
actual_traces = fake_backend.trace_trees
assert len(actual_traces) == 3, f"Expected 3 traces, got {len(actual_traces)}"
def test_build_prompt_evaluation_task_logs_when_vision_missing() -> None:
model = mock.Mock()
model.model_name = "text-only-model"
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe the picture"},
{"type": "image_url", "image_url": {"url": "{{image_url}}"}},
],
}
]
with mock.patch.object(evaluator_module.LOGGER, "warning") as warning_mock:
_build_prompt_evaluation_task(model=model, messages=messages)
warning_mock.assert_called_once()
message_template, model_name, modal_list, doc_url = warning_mock.call_args[0]
assert "does not support %s content" in message_template
assert model_name == "text-only-model"
assert modal_list == "vision"
assert "comet.com/docs/opik" in doc_url
def test_evaluate_prompt_happyflow(
fake_backend,
):
MODEL_NAME = "gpt-3.5-turbo"
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
mock_models_factory_get = mock.Mock()
mock_model = mock.Mock()
mock_model.model_name = MODEL_NAME
mock_model.generate_provider_response.return_value = mock.Mock(
choices=[mock.Mock(message=mock.Mock(content="Hello, world!"))]
)
mock_models_factory_get.return_value = mock_model
experiment_tags = ["one", "two", "three"]
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with mock.patch.object(
models_factory,
"get",
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[
{"role": "user", "content": "LLM response: {{input}}"},
],
experiment_name="the-experiment-name",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
experiment_tags=experiment_tags,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=experiment_tags,
dataset_version_id=None,
project_name=None,
)
# ``evaluate_prompt`` is contractually required to auto-populate
# ``prompt_template`` and ``model`` into ``experiment_config``. The
# resume blob coexists under a separate key, so we pin the prompt
# contract by drilling in rather than asserting whole-dict equality.
forwarded_config = mock_create_experiment.call_args.kwargs["experiment_config"]
assert forwarded_config["prompt_template"] == [
{"role": "user", "content": "LLM response: {{input}}"}
]
assert forwarded_config["model"] == MODEL_NAME
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
EXPECTED_TRACE_TREES = [
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"question": "Hello, world!",
"reference": "Hello, world!",
"id": "dataset-item-id-1",
},
output={
"input": [{"role": "user", "content": "LLM response: {{input}}"}],
"output": "Hello, world!",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="_prompt_evaluation_task",
input={
"prompt_variables": {
"question": "Hello, world!",
"reference": "Hello, world!",
"id": "dataset-item-id-1",
}
},
output={
"input": [
{"role": "user", "content": "LLM response: {{input}}"}
],
"output": "Hello, world!",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input=ANY_BUT_NONE,
output=ANY_BUT_NONE,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[ANY_BUT_NONE],
source="experiment",
),
],
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=1.0,
)
],
source="experiment",
),
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"question": "What is the capital of France?",
"reference": "Paris",
"id": "dataset-item-id-2",
},
output={
"input": [{"role": "user", "content": "LLM response: {{input}}"}],
"output": "Hello, world!",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="_prompt_evaluation_task",
input={
"prompt_variables": {
"question": "What is the capital of France?",
"reference": "Paris",
"id": "dataset-item-id-2",
}
},
output={
"input": [
{"role": "user", "content": "LLM response: {{input}}"}
],
"output": "Hello, world!",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input=ANY_BUT_NONE,
output=ANY_BUT_NONE,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[ANY_BUT_NONE],
source="experiment",
),
],
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=0.0,
)
],
source="experiment",
),
]
for expected_trace, actual_trace in zip(
EXPECTED_TRACE_TREES, fake_backend.trace_trees
):
assert_equal(expected_trace, actual_trace)
def test_evaluate__aggregated_metric__happy_flow(
fake_backend,
):
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
if dataset_item["input"]["message"] == "say hello":
return {"output": "hello"}
if dataset_item["input"]["message"] == "say bye":
return {"output": "not bye"}
raise Exception
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
def aggregator(results: List[score_result.ScoreResult]) -> score_result.ScoreResult:
value = sum([result.value for result in results])
return score_result.ScoreResult(name="aggregated_metric_result", value=value)
metrics_list = [metrics.Equals(), metrics.Contains()]
aggregated_metric = metrics.AggregatedMetric(
name="aggregated_metric",
metrics=metrics_list,
aggregator=aggregator,
)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[aggregated_metric],
task_threads=1,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
EXPECTED_TRACE_TREES = [
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
},
output={
"output": "hello",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
},
},
output={
"output": "hello",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input={
"test_case_": ANY_BUT_NONE,
"trial_id": 0,
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="aggregated_metric",
input={
"kwargs": {
"input": {"message": "say hello"},
"reference": "hello",
"output": "hello",
"id": "dataset-item-id-1",
}
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="equals_metric",
input={
"ignored_kwargs": {
"input": {"message": "say hello"},
"id": "dataset-item-id-1",
},
"output": "hello",
"reference": "hello",
},
output={
"output": score_result.ScoreResult(
name="equals_metric",
value=1.0,
).__dict__,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="contains_metric",
input={
"ignored_kwargs": {
"input": {"message": "say hello"},
"id": "dataset-item-id-1",
},
"output": "hello",
"reference": "hello",
},
output={
"output": score_result.ScoreResult(
name="contains_metric",
value=1.0,
).__dict__,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
source="experiment",
),
],
source="experiment",
),
],
source="experiment",
),
],
feedback_scores=[
# both contains and equals metrics will add to an aggregated result
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="aggregated_metric_result",
value=2.0,
)
],
source="experiment",
),
TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say bye"},
"reference": "bye",
"id": "dataset-item-id-2",
},
output={
"output": "not bye",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="say_task",
input={
"dataset_item": {
"input": {"message": "say bye"},
"reference": "bye",
"id": "dataset-item-id-2",
}
},
output={"output": "not bye"},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="metrics_calculation",
tags=["__opik_eval_internal__"],
input={
"test_case_": ANY_BUT_NONE,
"trial_id": 0,
},
output={"output": ANY_BUT_NONE},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="aggregated_metric",
input={
"kwargs": {
"input": {"message": "say bye"},
"reference": "bye",
"output": "not bye",
"id": "dataset-item-id-2",
}
},
output={
"output": ANY_BUT_NONE,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="equals_metric",
input={
"ignored_kwargs": {
"input": {"message": "say bye"},
"id": "dataset-item-id-2",
},
"reference": "bye",
"output": "not bye",
},
output={
"output": score_result.ScoreResult(
name="equals_metric",
value=0.0,
).__dict__,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
source="experiment",
),
SpanModel(
id=ANY_BUT_NONE,
type="general",
name="contains_metric",
input={
"ignored_kwargs": {
"input": {"message": "say bye"},
"id": "dataset-item-id-2",
},
"reference": "bye",
"output": "not bye",
},
output={
"output": score_result.ScoreResult(
name="contains_metric",
value=1.0,
).__dict__,
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
source="experiment",
),
],
source="experiment",
)
],
source="experiment",
),
],
feedback_scores=[
# only contains metric will add to an aggregated result
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="aggregated_metric_result",
value=1.0,
)
],
source="experiment",
),
]
for expected_trace, actual_trace in zip(
EXPECTED_TRACE_TREES, fake_backend.trace_trees
):
assert_equal(expected_trace, actual_trace)
def test_evaluate_prompt__with_random_sampling__happy_flow(
fake_backend,
):
# Creates a dataset with 5 items and then evaluates it using a random dataset sampler with 3 samples limit.
# Checks that only three samples are selected and that the metrics are computed for the three samples.
MODEL_NAME = "gpt-3.5-turbo"
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
# When dataset_sampler is provided, streaming is used but exhausted to a list
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
dataset_item.DatasetItem(
id="dataset-item-id-3",
question="Say hello",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-4",
question="How are you!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-5",
question="What time is it?",
reference="Tea time!",
),
]
)
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
mock_models_factory_get = mock.Mock()
mock_model = mock.Mock()
mock_model.model_name = MODEL_NAME
mock_model.generate_provider_response.return_value = mock.Mock(
choices=[mock.Mock(message=mock.Mock(content="Hello, world!"))]
)
mock_models_factory_get.return_value = mock_model
# create a random sampler with 3 samples limit
sampler = samplers.RandomDatasetSampler(max_samples=3)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with mock.patch.object(
models_factory,
"get",
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[
{"role": "user", "content": "LLM response: {{input}}"},
],
experiment_name="the-experiment-name",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_sampler=sampler,
)
# When dataset_sampler is provided, streaming is still used but exhausted to a list
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
# ``evaluate_prompt`` is contractually required to auto-populate
# ``prompt_template`` and ``model`` into ``experiment_config``. The
# resume blob coexists under a separate key, so we pin the prompt
# contract by drilling in rather than asserting whole-dict equality.
forwarded_config = mock_create_experiment.call_args.kwargs["experiment_config"]
assert forwarded_config["prompt_template"] == [
{"role": "user", "content": "LLM response: {{input}}"}
]
assert forwarded_config["model"] == MODEL_NAME
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
# Due to the random nature of the sampler, we need to verify the structure
# and that exactly 3 samples were selected, but not specific dataset items
actual_traces = fake_backend.trace_trees
assert len(actual_traces) == 3, f"Expected 3 traces, got {len(actual_traces)}"
# Verify each trace has the expected structure for prompt evaluation
# Since the mock LLM always returns "Hello, world!", the test verifies:
# - Score = 1.0 when reference = "Hello, world!"
# - Score = 0.0 when reference = anything else
for actual_trace in actual_traces:
# Verify feedback scores
assert len(actual_trace.feedback_scores) == 1
feedback_score = actual_trace.feedback_scores[0]
assert feedback_score.name == "equals_metric"
assert feedback_score.value in [0.0, 1.0] # Should be either 0 or 1
# Verify scoring logic - LLM always outputs "Hello, world!"
reference = actual_trace.input["reference"]
expected_score = 1.0 if reference == "Hello, world!" else 0.0
assert feedback_score.value == expected_score
def test_evaluate__2_trials_lead_to_2_experiment_items_per_dataset_item(
fake_backend,
):
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 2,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
if dataset_item["input"]["message"] == "say hello":
return {"output": "hello"}
if dataset_item["input"]["message"] == "say bye":
return {"output": "not bye"}
raise Exception
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
trial_count=2,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
# With 2 trials and 2 dataset items, we expect 4 calls to insert
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
# With 2 trials and 2 dataset items, we should have 4 trace trees total
assert len(fake_backend.trace_trees) == 4
# Check that we have 2 traces for each dataset item
dataset_item_1_traces = [
trace
for trace in fake_backend.trace_trees
if trace.input["id"] == "dataset-item-id-1"
]
dataset_item_2_traces = [
trace
for trace in fake_backend.trace_trees
if trace.input["id"] == "dataset-item-id-2"
]
assert len(dataset_item_1_traces) == 2
assert len(dataset_item_2_traces) == 2
# Define expected trace models
EXPECTED_TRACE_DATASET_ITEM_1 = TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say hello"},
"reference": "hello",
"id": "dataset-item-id-1",
},
output={"output": "hello"},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=1.0,
)
],
source="experiment",
spans=ANY_BUT_NONE, # We don't need to verify span details for this test
)
EXPECTED_TRACE_DATASET_ITEM_2 = TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"input": {"message": "say bye"},
"reference": "bye",
"id": "dataset-item-id-2",
},
output={"output": "not bye"},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=0.0,
)
],
source="experiment",
spans=ANY_BUT_NONE, # We don't need to verify span details for this test
)
# Verify each trace matches the expected model
for trace in dataset_item_1_traces:
assert_equal(EXPECTED_TRACE_DATASET_ITEM_1, trace)
for trace in dataset_item_2_traces:
assert_equal(EXPECTED_TRACE_DATASET_ITEM_2, trace)
def test_evaluate_prompt__2_trials_lead_to_2_experiment_items_per_dataset_item(
fake_backend,
):
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 2,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
mock_models_factory_get = mock.Mock()
mock_model = mock.Mock()
mock_model.model_name = "some-model-name"
mock_model.generate_provider_response.return_value = mock.Mock(
choices=[mock.Mock(message=mock.Mock(content="Hello, world!"))]
)
mock_models_factory_get.return_value = mock_model
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
with mock.patch.object(
models_factory,
"get",
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[
{"role": "user", "content": "LLM response: {{input}}"},
],
experiment_name="the-experiment-name",
model="some-model-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
trial_count=2,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once()
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="the-experiment-name",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name=None,
)
# ``evaluate_prompt`` is contractually required to auto-populate
# ``prompt_template`` and ``model`` into ``experiment_config``. The
# resume blob coexists under a separate key, so we pin the prompt
# contract by drilling in rather than asserting whole-dict equality.
forwarded_config = mock_create_experiment.call_args.kwargs["experiment_config"]
assert forwarded_config["prompt_template"] == [
{"role": "user", "content": "LLM response: {{input}}"}
]
assert forwarded_config["model"] == "some-model-name"
# With 2 trials and 2 dataset items, we expect 4 calls to insert
mock_experiment.insert.assert_has_calls(
[
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
mock.call(experiment_items_references=mock.ANY),
]
)
# With 2 trials and 2 dataset items, we should have 4 trace trees total
assert len(fake_backend.trace_trees) == 4
# Check that we have 2 traces for each dataset item
dataset_item_1_traces = [
trace
for trace in fake_backend.trace_trees
if trace.input["id"] == "dataset-item-id-1"
]
dataset_item_2_traces = [
trace
for trace in fake_backend.trace_trees
if trace.input["id"] == "dataset-item-id-2"
]
assert len(dataset_item_1_traces) == 2
assert len(dataset_item_2_traces) == 2
# Define expected trace models
EXPECTED_TRACE_DATASET_ITEM_1 = TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"question": "Hello, world!",
"reference": "Hello, world!",
"id": "dataset-item-id-1",
},
output={
"input": [{"role": "user", "content": "LLM response: {{input}}"}],
"output": "Hello, world!",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=1.0,
)
],
source="experiment",
spans=ANY_LIST, # We don't need to verify span details for this test
)
EXPECTED_TRACE_DATASET_ITEM_2 = TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input={
"question": "What is the capital of France?",
"reference": "Paris",
"id": "dataset-item-id-2",
},
output={
"input": [{"role": "user", "content": "LLM response: {{input}}"}],
"output": "Hello, world!",
},
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
feedback_scores=[
FeedbackScoreModel(
id=ANY_BUT_NONE,
name="equals_metric",
value=0.0,
)
],
source="experiment",
spans=ANY_LIST, # We don't need to verify span details for this test
)
for trace in dataset_item_1_traces:
assert_equal(EXPECTED_TRACE_DATASET_ITEM_1, trace)
for trace in dataset_item_2_traces:
assert_equal(EXPECTED_TRACE_DATASET_ITEM_2, trace)
def test_evaluate__with_experiment_scores(fake_backend):
"""Test that experiment_scores are computed and stored correctly."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"name",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "test-dataset"
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.dataset_items_count = None
mock_dataset.id = "dataset-id"
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
# Create a real Experiment instance with mocked dependencies
mock_rest_client = mock.Mock()
mock_experiments_api = mock.Mock()
mock_update_experiment = mock.Mock()
mock_experiments_api.update_experiment = mock_update_experiment
mock_rest_client.experiments = mock_experiments_api
real_experiment = experiment.Experiment(
id="experiment-id",
name="test-experiment",
dataset_name="test-dataset",
rest_client=mock_rest_client,
streamer=mock.Mock(),
experiments_client=mock.Mock(),
)
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = real_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
def compute_accuracy_stats(test_results: List) -> List[score_result.ScoreResult]:
"""Compute max accuracy across all test results."""
accuracy_scores = [
score.value
for test_result in test_results
for score in test_result.score_results
if score.name == "equals_metric"
]
if not accuracy_scores:
return []
return [
score_result.ScoreResult(
name="equals_metric (max)",
value=max(accuracy_scores),
),
]
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
result = evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="test-experiment",
scoring_metrics=[metrics.Equals()],
task_threads=1,
experiment_scoring_functions=[compute_accuracy_stats],
)
# Verify experiment scores were computed and stored
assert len(result.experiment_scores) == 1
assert result.experiment_scores[0].name == "equals_metric (max)"
assert result.experiment_scores[0].value == 1.0
# Verify experiment scores were logged to backend
mock_update_experiment.assert_called_once()
call_args = mock_update_experiment.call_args
assert call_args[1]["id"] == "experiment-id"
assert len(call_args[1]["experiment_scores"]) == 1
assert call_args[1]["experiment_scores"][0].name == "equals_metric (max)"
assert call_args[1]["experiment_scores"][0].value == 1.0
def test_evaluate__with_experiment_scores_empty_results(fake_backend):
"""Test that experiment_scores handle empty test results gracefully."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"name",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "test-dataset"
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.dataset_items_count = None
mock_dataset.id = "dataset-id"
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter([])
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_experiment.id = "experiment-id"
mock_experiment.name = "test-experiment"
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
def compute_accuracy_stats(test_results: List) -> List[score_result.ScoreResult]:
"""Compute max accuracy across all test results."""
return [
score_result.ScoreResult(
name="equals_metric (max)",
value=0.5,
),
]
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
result = evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="test-experiment",
scoring_metrics=[metrics.Equals()],
task_threads=1,
experiment_scoring_functions=[compute_accuracy_stats],
)
# Verify experiment scores are empty when no test results
assert len(result.experiment_scores) == 0
def test_evaluate_on_dict_items__happyflow(fake_backend):
items = [
{"input": "What is 2+2?", "expected_output": "4"},
{"input": "What is 3+3?", "expected_output": "6"},
]
def simple_task(item):
# Simple echo task for testing
if "2+2" in item["input"]:
return {"output": "4"}
return {"output": "6"}
result = evaluation.evaluator.evaluate_on_dict_items(
items=items,
task=simple_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping={"reference": "expected_output"},
scoring_threads=1, # Use single thread for deterministic order
)
assert len(result.test_results) == 2
# Check first result
assert result.test_results[0].test_case.task_output == {"output": "4"}
assert result.test_results[0].score_results[0].value == 1.0
assert result.test_results[0].score_results[0].name == "equals_metric"
# Check second result
assert result.test_results[1].test_case.task_output == {"output": "6"}
assert result.test_results[1].score_results[0].value == 1.0
assert result.test_results[1].score_results[0].name == "equals_metric"
# Test aggregation
aggregated = result.aggregate_evaluation_scores()
assert aggregated == {
"equals_metric": score_statistics.ScoreStatistics(
mean=1.0,
max=1.0,
min=1.0,
values=[1.0, 1.0],
std=0.0,
)
}
def test_evaluate_on_dict_items__with_scoring_key_mapping(fake_backend):
items = [
{"user_question": "Hello?", "expected_answer": "Hi"},
]
def task(item):
return {"model_response": "Hi"}
result = evaluation.evaluate_on_dict_items(
items=items,
task=task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping={
"input": "user_question",
"output": "model_response",
"reference": "expected_answer",
},
scoring_threads=1,
)
assert len(result.test_results) == 1
assert result.test_results[0].score_results[0].value == 1.0
def test_evaluate_on_dict_items__multiple_metrics(fake_backend):
items = [
{"input": "test", "expected_output": "test"},
]
def task(item):
return {"output": "test"}
class CustomMetric(metrics.base_metric.BaseMetric):
def score(self, output: str, **kwargs):
return score_result.ScoreResult(
name="custom_metric",
value=0.5,
)
result = evaluation.evaluator.evaluate_on_dict_items(
items=items,
task=task,
scoring_metrics=[metrics.Equals(), CustomMetric()],
scoring_key_mapping={"reference": "expected_output"},
scoring_threads=1,
)
assert len(result.test_results) == 1
assert len(result.test_results[0].score_results) == 2
assert result.test_results[0].score_results[0] == score_result.ScoreResult(
name="equals_metric",
value=1.0,
)
assert result.test_results[0].score_results[1] == score_result.ScoreResult(
name="custom_metric",
value=0.5,
)
# Test aggregation with multiple metrics
aggregated = result.aggregate_evaluation_scores()
assert aggregated == {
"equals_metric": score_statistics.ScoreStatistics(
mean=1.0,
max=1.0,
min=1.0,
values=[1.0],
std=None,
),
"custom_metric": score_statistics.ScoreStatistics(
mean=0.5,
max=0.5,
min=0.5,
values=[0.5],
std=None,
),
}
def test_evaluate_on_dict_items__task_execution(fake_backend):
items = [{"value": 5, "expected": 10}]
task_calls = []
def task(item):
task_calls.append(item)
return {"result": item["value"] * 2}
class CustomMetric(metrics.base_metric.BaseMetric):
def score(self, output: int, reference: int, **kwargs):
return score_result.ScoreResult(
name="result_check",
value=1.0 if output == reference else 0.0,
)
result = evaluation.evaluator.evaluate_on_dict_items(
items=items,
task=task,
scoring_metrics=[CustomMetric()],
scoring_key_mapping={"output": "result", "reference": "expected"},
scoring_threads=1,
)
# Verify task was called with correct input
assert task_calls == [{"value": 5, "expected": 10, "id": "temp_item_0"}]
# Verify result
assert result.test_results[0].test_case.task_output == {"result": 10}
assert result.test_results[0].score_results[0].value == 1.0
def test_evaluate_on_dict_items__no_metrics_returns_empty(fake_backend):
items = [{"input": "test"}]
def task(item):
return {"output": "test"}
result = evaluation.evaluate_on_dict_items(
items=items,
task=task,
scoring_metrics=[],
scoring_threads=1,
)
assert result.test_results == []
def test_evaluate_on_dict_items__empty_items_list(fake_backend):
"""Test that empty items list returns empty results."""
items = []
def task(item):
return {"output": "test"}
result = evaluation.evaluate_on_dict_items(
items=items,
task=task,
scoring_metrics=[metrics.Equals()],
scoring_threads=1,
)
assert result.test_results == []
def test_evaluate_on_dict_items__task_raises_exception(fake_backend):
"""Test that exceptions in task execution are properly propagated."""
items = [{"input": "test", "expected": "result"}]
def failing_task(item):
raise ValueError("Task failed")
with pytest.raises(ValueError, match="Task failed"):
evaluation.evaluate_on_dict_items(
items=items,
task=failing_task,
scoring_metrics=[metrics.Equals()],
scoring_key_mapping={"reference": "expected"},
scoring_threads=1,
)
def test_evaluate_on_dict_items__with_scoring_functions(fake_backend):
"""Test evaluate_on_dict_items with scoring functions instead of metrics."""
items = [
{"input": "What is 2+2?", "expected_output": "4"},
{"input": "What is 3+3?", "expected_output": "6"},
]
def task(item: Dict[str, Any]) -> Dict[str, Any]:
if "2+2" in item["input"]:
return {"output": "4"}
return {"output": "6"}
def custom_scorer(
dataset_item: Dict[str, Any],
task_outputs: Dict[str, Any],
) -> score_result.ScoreResult:
expected = dataset_item.get("expected_output", "")
actual = task_outputs.get("output", "")
return score_result.ScoreResult(
name="custom_scorer",
value=1.0 if expected == actual else 0.0,
reason=f"Expected: {expected}, Got: {actual}",
)
result = evaluation.evaluate_on_dict_items(
items=items,
task=task,
scoring_functions=[custom_scorer],
scoring_threads=1,
)
# Verify results structure
assert len(result.test_results) == 2
# Verify scoring results
assert result.test_results[0].score_results[0] == score_result.ScoreResult(
name="custom_scorer",
value=1.0,
reason="Expected: 4, Got: 4",
)
assert result.test_results[1].score_results[0] == score_result.ScoreResult(
name="custom_scorer",
value=1.0,
reason="Expected: 6, Got: 6",
)
# Verify aggregation
aggregated = result.aggregate_evaluation_scores()
assert aggregated == {
"custom_scorer": score_statistics.ScoreStatistics(
mean=1.0,
max=1.0,
min=1.0,
values=[1.0, 1.0],
std=0.0,
)
}
def test_evaluate__uses_streaming_by_default(fake_backend):
"""Test that evaluate uses streaming mode by default when no dataset_item_ids or dataset_sampler is provided."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
# Mock the streaming method to return an iterator
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
)
# Verify streaming method was called and non-streaming was not
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=None,
)
def test_evaluate__uses_streaming_with_dataset_item_ids(fake_backend):
"""Test that evaluate uses streaming mode even when dataset_item_ids is provided."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_item_ids=["dataset-item-id-1"],
)
# Verify streaming method was called with dataset_item_ids
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=["dataset-item-id-1"],
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=None,
)
def test_evaluate__falls_back_to_non_streaming_with_dataset_sampler(fake_backend):
"""Test that evaluate falls back to non-streaming mode when dataset_sampler is provided."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__stream_items_as_dataclasses__",
"id",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
sampler = samplers.RandomDatasetSampler(max_samples=1)
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_sampler=sampler,
)
# Verify streaming method was called (but list() was used to exhaust it for sampling)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=None,
)
def test_evaluate__streaming_with_nb_samples(fake_backend):
"""Test that streaming mode correctly passes nb_samples parameter."""
mock_dataset = mock.MagicMock(
spec=[
"__internal_api__get_items_as_dataclasses__",
"__internal_api__stream_items_as_dataclasses__",
"id",
"name",
"dataset_items_count",
"get_version_info",
"get_execution_policy",
"project_name",
"get_evaluators",
]
)
mock_dataset.get_version_info.return_value = None
mock_dataset.project_name = None
mock_dataset.get_execution_policy.return_value = {
"runs_per_item": 1,
"pass_threshold": 1,
}
mock_dataset.get_evaluators.return_value = []
mock_dataset.name = "the-dataset-name"
mock_dataset.dataset_items_count = None
# Mock the streaming method to return an iterator with limited items
mock_dataset.__internal_api__stream_items_as_dataclasses__.return_value = iter(
[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment = mock.Mock()
mock_experiment.prompts = None
mock_create_experiment = mock.Mock()
mock_create_experiment.return_value = mock_experiment
mock_get_experiment_url_by_id = mock.Mock()
mock_get_experiment_url_by_id.return_value = "any_url"
with mock.patch.object(
opik_client.Opik, "create_experiment", mock_create_experiment
):
with mock.patch.object(
url_helpers, "get_experiment_url_by_id", mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
nb_samples=2,
)
# Verify streaming method was called with nb_samples parameter and non-streaming was not
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=2,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=None,
)
def test_evaluate_prompt__with_filter_string__passes_to_streaming(fake_backend):
"""Test that evaluate_prompt correctly passes filter_string to streaming method."""
MODEL_NAME = "gpt-3.5-turbo"
filter_string = 'tags contains "important"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
]
)
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
mock_models_factory_get, mock_model = create_mock_model(model_name=MODEL_NAME)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[
{"role": "user", "content": "LLM response: {{input}}"},
],
experiment_name="the-experiment-name",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate_prompt__with_filter_string_and_nb_samples__passes_both_parameters(
fake_backend,
):
"""Test that evaluate_prompt correctly passes both filter_string and nb_samples to streaming method."""
MODEL_NAME = "gpt-3.5-turbo"
filter_string = 'data.category = "test"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
mock_models_factory_get, mock_model = create_mock_model(model_name=MODEL_NAME)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[
{"role": "user", "content": "LLM response: {{input}}"},
],
experiment_name="the-experiment-name",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
nb_samples=2,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=2,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate_prompt__with_filter_string_and_dataset_sampler__passes_filter_string(
fake_backend,
):
"""Test that evaluate_prompt passes filter_string even when dataset_sampler is used."""
MODEL_NAME = "gpt-3.5-turbo"
sampler = samplers.RandomDatasetSampler(max_samples=1)
filter_string = 'created_at >= "2024-01-01T00:00:00Z"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
mock_models_factory_get, mock_model = create_mock_model(model_name=MODEL_NAME)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[
{"role": "user", "content": "LLM response: {{input}}"},
],
experiment_name="the-experiment-name",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_sampler=sampler,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate__with_filter_string__passes_to_streaming(fake_backend):
"""Test that evaluate correctly passes filter_string to streaming method."""
filter_string = 'tags contains "important"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate__with_filter_string_and_nb_samples__passes_both_parameters(
fake_backend,
):
"""Test that evaluate correctly passes both filter_string and nb_samples to streaming method."""
filter_string = 'data.category = "test"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
nb_samples=2,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=2,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate__with_filter_string_and_dataset_sampler__passes_filter_string(
fake_backend,
):
"""Test that evaluate passes filter_string even when dataset_sampler is used."""
sampler = samplers.RandomDatasetSampler(max_samples=1)
filter_string = 'created_at >= "2024-01-01T00:00:00Z"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_sampler=sampler,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate_optimization_trial__with_filter_string__passes_to_streaming(
fake_backend,
):
"""Test that evaluate_optimization_trial correctly passes filter_string to streaming method."""
filter_string = 'tags contains "test"'
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
question="Hello, world!",
reference="Hello, world!",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
question="What is the capital of France?",
reference="Paris",
),
]
)
def say_task(dataset_item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
):
evaluator_module.evaluate_optimization_trial(
optimization_id="opt-123",
dataset=mock_dataset,
task=say_task,
experiment_name="the-experiment-name",
scoring_metrics=[metrics.Equals()],
task_threads=1,
dataset_filter_string=filter_string,
)
mock_dataset.__internal_api__stream_items_as_dataclasses__.assert_called_once_with(
nb_samples=None,
dataset_item_ids=None,
batch_size=helpers_module.EVALUATION_STREAM_DATASET_BATCH_SIZE,
filter_string=filter_string,
)
def test_evaluate_optimization_trial__traces_and_spans__have_source_optimization(
fake_backend,
):
"""evaluate_optimization_trial always passes source='optimization' → all traces carry it."""
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
dataset_item.DatasetItem(
id="dataset-item-id-2",
input={"message": "say bye"},
reference="bye",
),
]
)
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
):
evaluator_module.evaluate_optimization_trial(
optimization_id="opt-123",
dataset=mock_dataset,
task=say_task,
scoring_metrics=[metrics.Equals()],
experiment_name="the-experiment-name",
task_threads=1,
verbose=0,
)
assert len(fake_backend.trace_trees) == 2
for trace in fake_backend.trace_trees:
assert trace.source == "optimization", (
f"Expected trace source 'optimization', got '{trace.source}'"
)
for span in trace.spans:
assert span.source == "optimization", (
f"Expected span source 'optimization', got '{span.source}'"
)
def test_evaluate_optimization_trial__trace_tree_source_experiment_and_spans_source_experiment(
fake_backend,
):
"""Full trace tree assertion: source='optimization' on the trace and both task/scoring spans."""
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="dataset-item-id-1",
input={"message": "say hello"},
reference="hello",
),
]
)
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
):
evaluator_module.evaluate_optimization_trial(
optimization_id="opt-789",
dataset=mock_dataset,
task=say_task,
scoring_metrics=[metrics.Equals()],
experiment_name="the-experiment-name",
task_threads=1,
verbose=0,
)
EXPECTED_TRACE_TREE = TraceModel(
id=ANY_BUT_NONE,
name="evaluation_task",
input=ANY_BUT_NONE,
output=ANY_BUT_NONE,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
last_updated_at=ANY_BUT_NONE,
source="optimization",
feedback_scores=ANY_LIST,
spans=[
SpanModel(
id=ANY_BUT_NONE,
name="say_task",
type="general",
input=ANY_BUT_NONE,
output=ANY_BUT_NONE,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=[],
source="optimization",
),
SpanModel(
id=ANY_BUT_NONE,
name="metrics_calculation",
tags=["__opik_eval_internal__"],
type="general",
input=ANY_BUT_NONE,
output=ANY_BUT_NONE,
start_time=ANY_BUT_NONE,
end_time=ANY_BUT_NONE,
spans=ANY_LIST,
source="optimization",
),
],
)
assert_equal(EXPECTED_TRACE_TREE, fake_backend.trace_trees[0])
def test_evaluate__verbose_zero__progress_bar_disabled(fake_backend):
"""Test that verbose=0 disables the progress bar."""
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"message": "hello"}, reference="hello"
),
]
)
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with mock.patch(
"opik.environment.get_tqdm_for_current_environment"
) as mock_get_tqdm:
mock_tqdm_factory = mock.Mock()
mock_progress_bar = mock.Mock()
mock_tqdm_factory.return_value = mock_progress_bar
mock_get_tqdm.return_value = mock_tqdm_factory
with patch_evaluation_dependencies(
mock_create_experiment, mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="verbose-off-test",
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
# tqdm should be created with disable=True when verbose=0
mock_tqdm_factory.assert_called_once_with(
disable=True,
desc=mock.ANY,
total=mock.ANY,
)
def test_evaluate__dataset_has_project_name__caller_override_ignored_and_warning_logged(
fake_backend, capture_log
):
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"message": "hello"}, reference="hello"
),
]
)
mock_dataset.project_name = "dataset-project"
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment, mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="project-override-test",
project_name="caller-project",
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="project-override-test",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name="dataset-project",
)
deprecation_warnings = [
record
for record in capture_log.records
if record.levelno == logging.WARNING
and "deprecated" in record.getMessage()
and "project_name" in record.getMessage()
]
assert len(deprecation_warnings) == 1
def test_evaluate__dataset_has_no_project_name__caller_value_preserved(fake_backend):
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"message": "hello"}, reference="hello"
),
]
)
mock_dataset.project_name = None
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment, mock_get_experiment_url_by_id
):
evaluation.evaluate(
dataset=mock_dataset,
task=say_task,
experiment_name="project-fallback-test",
project_name="caller-project",
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
mock_create_experiment.assert_called_once_with(
dataset_name="the-dataset-name",
name="project-fallback-test",
experiment_config=mock.ANY,
prompts=None,
tags=None,
dataset_version_id=None,
project_name="caller-project",
)
def test_evaluate_prompt__dataset_has_project_name__caller_override_ignored_and_warning_logged(
fake_backend, capture_log
):
MODEL_NAME = "gpt-3.5-turbo"
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(id="item-1", input="hello", reference="hello"),
]
)
mock_dataset.project_name = "dataset-project"
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
mock_models_factory_get, _mock_model = create_mock_model(model_name=MODEL_NAME)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[{"role": "user", "content": "Say: {{input}}"}],
experiment_name="prompt-project-override-test",
project_name="caller-project",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
call_kwargs = mock_create_experiment.call_args.kwargs
assert call_kwargs["project_name"] == "dataset-project"
deprecation_warnings = [
record
for record in capture_log.records
if record.levelno == logging.WARNING
and "deprecated" in record.getMessage()
and "evaluate_prompt()" in record.getMessage()
]
assert len(deprecation_warnings) == 1
def test_evaluate_prompt__dataset_has_no_project_name__caller_value_preserved(
fake_backend,
):
MODEL_NAME = "gpt-3.5-turbo"
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(id="item-1", input="hello", reference="hello"),
]
)
mock_dataset.project_name = None
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
mock_models_factory_get, _mock_model = create_mock_model(model_name=MODEL_NAME)
with patch_evaluation_dependencies(
mock_create_experiment,
mock_get_experiment_url_by_id,
mock_models_factory_get,
):
evaluation.evaluate_prompt(
dataset=mock_dataset,
messages=[{"role": "user", "content": "Say: {{input}}"}],
experiment_name="prompt-project-fallback-test",
project_name="caller-project",
model=MODEL_NAME,
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
call_kwargs = mock_create_experiment.call_args.kwargs
assert call_kwargs["project_name"] == "caller-project"
def test_evaluate_optimization_trial__dataset_has_project_name__caller_override_ignored_and_warning_logged(
fake_backend, capture_log
):
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"message": "hello"}, reference="hello"
),
]
)
mock_dataset.project_name = "dataset-project"
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment, mock_get_experiment_url_by_id
):
evaluator_module.evaluate_optimization_trial(
optimization_id="opt-123",
dataset=mock_dataset,
task=say_task,
experiment_name="trial-project-override-test",
project_name="caller-project",
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
call_kwargs = mock_create_experiment.call_args.kwargs
assert call_kwargs["project_name"] == "dataset-project"
deprecation_warnings = [
record
for record in capture_log.records
if record.levelno == logging.WARNING
and "deprecated" in record.getMessage()
and "evaluate_optimization_trial()" in record.getMessage()
]
assert len(deprecation_warnings) == 1
def test_evaluate_optimization_trial__dataset_has_no_project_name__caller_value_preserved(
fake_backend,
):
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"message": "hello"}, reference="hello"
),
]
)
mock_dataset.project_name = None
def say_task(item: Dict[str, Any]):
return {"output": "hello"}
mock_experiment, mock_create_experiment, mock_get_experiment_url_by_id = (
create_mock_experiment()
)
with patch_evaluation_dependencies(
mock_create_experiment, mock_get_experiment_url_by_id
):
evaluator_module.evaluate_optimization_trial(
optimization_id="opt-123",
dataset=mock_dataset,
task=say_task,
experiment_name="trial-project-fallback-test",
project_name="caller-project",
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
call_kwargs = mock_create_experiment.call_args.kwargs
assert call_kwargs["project_name"] == "caller-project"
# =============================================================================
# Config / metric defaults — previously covered by e2e tests, moved here
# because the behaviour is purely SDK-local (what's sent to create_experiment,
# whether an empty metrics list is accepted). No backend needed.
# =============================================================================
def test_evaluate__experiment_config_not_set__only_resume_state_added(
fake_backend,
):
"""When experiment_config is omitted the SDK still embeds resume state.
This test's mock dataset has no version (``get_version_info`` returns
``None``), so the embedded state marks the experiment non-resumable —
resume requires a pinned dataset version. The key point is that the
``_opik_resume`` blob is still the only thing added to the config; the
SDK does not auto-populate other keys.
"""
from opik.evaluation import resume
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"question": "hi"}, reference="hi"
)
]
)
_, mock_create_experiment, mock_get_url = create_mock_experiment()
with patch_evaluation_dependencies(mock_create_experiment, mock_get_url):
evaluator_module.evaluate(
dataset=mock_dataset,
task=lambda item: {"output": "hi"},
experiment_name="no-config-experiment",
scoring_metrics=[metrics.Equals()],
task_threads=1,
verbose=0,
)
import json as _json
sent_config = mock_create_experiment.call_args.kwargs["experiment_config"]
assert list(sent_config.keys()) == [resume.RESUME_METADATA_KEY]
blob = _json.loads(sent_config[resume.RESUME_METADATA_KEY])
assert blob["resumable"] is False
assert "pinned dataset version" in blob["non_resumable_reason"]
def test_evaluate__no_scoring_metrics__completes_and_writes_no_feedback_scores(
fake_backend,
):
"""An empty scoring_metrics list is accepted — traces are produced but
no feedback scores are attached to them."""
mock_dataset = create_mock_dataset(
items=[
dataset_item.DatasetItem(
id="item-1", input={"question": "hi"}, reference="hi"
)
]
)
_, mock_create_experiment, mock_get_url = create_mock_experiment()
with patch_evaluation_dependencies(mock_create_experiment, mock_get_url):
evaluator_module.evaluate(
dataset=mock_dataset,
task=lambda item: {"output": "hi"},
experiment_name="no-metrics-experiment",
scoring_metrics=[],
task_threads=1,
verbose=0,
)
mock_create_experiment.assert_called_once()
assert len(fake_backend.trace_trees) == 1
assert not fake_backend.trace_trees[0].feedback_scores