chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
import opik
|
||||
from opik import synchronization
|
||||
from opik.evaluation.metrics import score_result
|
||||
|
||||
from .. import verifiers
|
||||
from ...testlib import generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
|
||||
def _wait_for_version(dataset, expected_version: str, timeout: float = 10) -> None:
|
||||
"""Wait for dataset to have the expected version, fail if not reached."""
|
||||
success = synchronization.until(
|
||||
lambda: dataset.get_current_version_name() == expected_version,
|
||||
max_try_seconds=timeout,
|
||||
)
|
||||
assert success, f"Expected version '{expected_version}' was not created in time"
|
||||
|
||||
|
||||
def test_evaluate__with_dataset_version__evaluates_version_items_only__happyflow(
|
||||
opik_client: opik.Opik, dataset_name: str
|
||||
):
|
||||
"""Test that opik.evaluate works with DatasetVersion and only evaluates items from that version."""
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
# Insert first batch - creates v1 with 2 items
|
||||
dataset.insert(
|
||||
[
|
||||
{"input": {"question": "Q1"}, "expected_output": {"answer": "A1"}},
|
||||
{"input": {"question": "Q2"}, "expected_output": {"answer": "A2"}},
|
||||
]
|
||||
)
|
||||
_wait_for_version(dataset, "v1")
|
||||
|
||||
# Insert second batch - creates v2 with 4 items total
|
||||
dataset.insert(
|
||||
[
|
||||
{"input": {"question": "Q3"}, "expected_output": {"answer": "A3"}},
|
||||
{"input": {"question": "Q4"}, "expected_output": {"answer": "A4"}},
|
||||
]
|
||||
)
|
||||
_wait_for_version(dataset, "v2")
|
||||
|
||||
# Get v1 view - should have only 2 items
|
||||
v1_view = dataset.get_version_view("v1")
|
||||
assert v1_view.items_total == 2
|
||||
|
||||
# Simple task that returns the expected output
|
||||
def task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return item["expected_output"]
|
||||
|
||||
# Simple scoring function
|
||||
def scoring_function(
|
||||
dataset_item: Dict[str, Any], task_outputs: Dict[str, Any]
|
||||
) -> score_result.ScoreResult:
|
||||
return score_result.ScoreResult(name="test_score", value=1.0)
|
||||
|
||||
# Evaluate using DatasetVersion (v1)
|
||||
result = opik.evaluate(
|
||||
dataset=v1_view,
|
||||
task=task,
|
||||
scoring_functions=[scoring_function],
|
||||
verbose=0,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# Should have evaluated only 2 items (from v1), not 4 (from v2/current)
|
||||
assert len(result.test_results) == 2
|
||||
|
||||
# Verify the items evaluated were from v1
|
||||
evaluated_questions = {
|
||||
tr.test_case.dataset_item_content["input"]["question"]
|
||||
for tr in result.test_results
|
||||
}
|
||||
assert evaluated_questions == {"Q1", "Q2"}
|
||||
|
||||
# Verify the experiment is linked to v1's version ID
|
||||
v1_version_info = v1_view.get_version_info()
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=result.experiment_id,
|
||||
experiment_name=result.experiment_name,
|
||||
experiment_metadata=None,
|
||||
traces_amount=2,
|
||||
feedback_scores_amount=1,
|
||||
dataset_version_id=v1_version_info.id,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
@@ -0,0 +1,141 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
import opik
|
||||
from opik import id_helpers
|
||||
from opik.evaluation import metrics
|
||||
from opik.evaluation import evaluator as evaluator_module
|
||||
|
||||
from ...testlib import generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
|
||||
def test_evaluate__with_filter_string__filters_dataset_items(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""Test that evaluate correctly filters dataset items using filter_string."""
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset_items = [
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is the capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
"category": "geography",
|
||||
},
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is 2+2?"},
|
||||
"expected_model_output": {"output": "4"},
|
||||
"category": "math",
|
||||
},
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is the capital of Germany?"},
|
||||
"expected_model_output": {"output": "Berlin"},
|
||||
"category": "geography",
|
||||
},
|
||||
]
|
||||
|
||||
dataset.insert(dataset_items)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the capital of Germany?"}:
|
||||
return {"output": "Berlin"}
|
||||
if item["input"] == {"question": "What is 2+2?"}:
|
||||
return {"output": "4"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
dataset_filter_string='data.category = "geography"',
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_name(
|
||||
experiment_name, project_name=PROJECT_NAME
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 2, (
|
||||
f"Expected 2 experiment items (filtered by geography category), but got {len(experiment_items_contents)}. "
|
||||
f"Experiment items: {experiment_items_contents}"
|
||||
)
|
||||
assert retrieved_experiment.project_name == PROJECT_NAME
|
||||
|
||||
|
||||
def test_evaluate_optimization_trial__with_filter_string__filters_dataset_items(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""Test that evaluate_optimization_trial correctly filters dataset items using filter_string."""
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset_items = [
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is the capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
"category": "geography",
|
||||
},
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is 2+2?"},
|
||||
"expected_model_output": {"output": "4"},
|
||||
"category": "math",
|
||||
},
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is the capital of Germany?"},
|
||||
"expected_model_output": {"output": "Berlin"},
|
||||
"category": "geography",
|
||||
},
|
||||
]
|
||||
|
||||
dataset.insert(dataset_items)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the capital of Germany?"}:
|
||||
return {"output": "Berlin"}
|
||||
if item["input"] == {"question": "What is 2+2?"}:
|
||||
return {"output": "4"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluator_module.evaluate_optimization_trial(
|
||||
optimization_id=id_helpers.generate_id(),
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
dataset_filter_string='data.category = "math"',
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_name(
|
||||
experiment_name, project_name=PROJECT_NAME
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 1, (
|
||||
f"Expected 1 experiment item (filtered by math category), but got {len(experiment_items_contents)}. "
|
||||
f"Experiment items: {experiment_items_contents}"
|
||||
)
|
||||
assert retrieved_experiment.project_name == PROJECT_NAME
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,320 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
import opik
|
||||
from opik import synchronization
|
||||
from opik.api_objects.experiment import experiment_item
|
||||
from opik.evaluation import metrics
|
||||
from opik.evaluation.metrics import score_result
|
||||
from opik.message_processing.emulation import models
|
||||
from opik.types import FeedbackScoreDict
|
||||
|
||||
from .. import verifiers
|
||||
from ...testlib import assert_equal, ANY_BUT_NONE, generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
|
||||
def _wait_for_version(dataset, expected_version: str, timeout: float = 10) -> None:
|
||||
"""Wait for dataset to have the expected version, fail if not reached."""
|
||||
success = synchronization.until(
|
||||
lambda: dataset.get_current_version_name() == expected_version,
|
||||
max_try_seconds=timeout,
|
||||
)
|
||||
assert success, f"Expected version '{expected_version}' was not created in time"
|
||||
|
||||
|
||||
DATASET_ITEMS = [
|
||||
{
|
||||
"input": {"question": "What is the of capital of Ukraine?"},
|
||||
"expected_model_output": {"output": "Kyiv"},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def llm_task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the of capital of Ukraine?"}:
|
||||
return {"output": "Kyiv"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
|
||||
def equals_scoring_function(dataset_item: Dict[str, Any], task_outputs: Dict[str, Any]):
|
||||
reference = dataset_item["expected_model_output"]["output"]
|
||||
prediction = task_outputs["output"]
|
||||
if reference == prediction:
|
||||
value = 1.0
|
||||
else:
|
||||
value = 0.0
|
||||
return score_result.ScoreResult(
|
||||
name="equals_scoring_function",
|
||||
value=value,
|
||||
reason="Correct output value" if value == 1.0 else "Incorrect output value",
|
||||
)
|
||||
|
||||
|
||||
def task_span_name_scoring_function(
|
||||
task_span: models.SpanModel, **ignored_kwargs: Any
|
||||
) -> score_result.ScoreResult:
|
||||
score = 1.0 if task_span.name == "llm_task" else 0.0
|
||||
return score_result.ScoreResult(
|
||||
value=score,
|
||||
name="task_span_name_scoring_function",
|
||||
reason="Correct task span name" if score == 1.0 else "Incorrect task span name",
|
||||
)
|
||||
|
||||
|
||||
def task_span_name_and_equals_scoring_function(
|
||||
dataset_item: Dict[str, Any],
|
||||
task_outputs: Dict[str, Any],
|
||||
task_span: models.SpanModel,
|
||||
) -> score_result.ScoreResult:
|
||||
score = 1.0 if task_span.name == "llm_task" else 0.0
|
||||
|
||||
reference = dataset_item["expected_model_output"]["output"]
|
||||
prediction = task_outputs["output"]
|
||||
if reference == prediction:
|
||||
value = 1.0
|
||||
else:
|
||||
value = 0.0
|
||||
|
||||
score *= value
|
||||
|
||||
return score_result.ScoreResult(
|
||||
value=score,
|
||||
name="task_span_name_and_equals_scoring_function",
|
||||
reason="Correct task span name and output value"
|
||||
if score == 1.0
|
||||
else "Incorrect task span name and output value",
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate__scoring_functions__happy_flow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
# Tests that ordinary scoring functions work correctly.
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
dataset.insert(DATASET_ITEMS)
|
||||
_wait_for_version(dataset, "v1")
|
||||
|
||||
# Get the version ID to verify it's passed to the experiment
|
||||
version_info = dataset.get_version_info()
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=llm_task,
|
||||
scoring_functions=[equals_scoring_function],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=1, # one trace per dataset item
|
||||
feedback_scores_amount=1,
|
||||
prompts=None,
|
||||
dataset_version_id=version_info.id,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 1
|
||||
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT = [
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of Ukraine?"},
|
||||
"expected_model_output": {"output": "Kyiv"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Kyiv"},
|
||||
feedback_scores=[
|
||||
FeedbackScoreDict(
|
||||
category_name=None,
|
||||
name="equals_scoring_function",
|
||||
reason="Correct output value",
|
||||
value=1.0,
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
assert_equal(
|
||||
expected=EXPECTED_EXPERIMENT_ITEMS_CONTENT,
|
||||
actual=experiment_items_contents,
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate__scoring_functions_mixed_with_scoring_metrics__happy_flow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
# Tests that mix of ordinary scoring functions and scoring metrics work correctly.
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
dataset.insert(DATASET_ITEMS)
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=llm_task,
|
||||
scoring_metrics=[equals_metric],
|
||||
scoring_functions=[equals_scoring_function],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=1, # one trace per dataset item
|
||||
feedback_scores_amount=2,
|
||||
prompts=None,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 1
|
||||
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT = [
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of Ukraine?"},
|
||||
"expected_model_output": {"output": "Kyiv"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Kyiv"},
|
||||
feedback_scores=[
|
||||
FeedbackScoreDict(
|
||||
category_name=None, name="equals_metric", reason=None, value=1.0
|
||||
),
|
||||
FeedbackScoreDict(
|
||||
category_name=None,
|
||||
name="equals_scoring_function",
|
||||
reason="Correct output value",
|
||||
value=1.0,
|
||||
),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
# sort feedback scores by name
|
||||
for item in experiment_items_contents:
|
||||
item.feedback_scores = sorted(item.feedback_scores, key=lambda x: x["name"])
|
||||
|
||||
assert_equal(
|
||||
expected=EXPECTED_EXPERIMENT_ITEMS_CONTENT, actual=experiment_items_contents
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate__scoring_functions_mixed_with_task_span_scoring_functions__happy_flow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
# Tests that mix of ordinary scoring functions and task span scoring functions work correctly.
|
||||
# Also, it checks that task span scoring functions can access:
|
||||
# task span, dataset item content (dataset_item), and task output (task_outputs) parameters.
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
dataset.insert(DATASET_ITEMS)
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=llm_task,
|
||||
scoring_functions=[
|
||||
equals_scoring_function,
|
||||
task_span_name_scoring_function,
|
||||
task_span_name_and_equals_scoring_function,
|
||||
],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=1, # one trace per dataset item
|
||||
feedback_scores_amount=3,
|
||||
prompts=None,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 1
|
||||
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT = [
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of Ukraine?"},
|
||||
"expected_model_output": {"output": "Kyiv"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Kyiv"},
|
||||
feedback_scores=[
|
||||
FeedbackScoreDict(
|
||||
category_name=None,
|
||||
name="equals_scoring_function",
|
||||
reason="Correct output value",
|
||||
value=1.0,
|
||||
),
|
||||
FeedbackScoreDict(
|
||||
category_name=None,
|
||||
name="task_span_name_and_equals_scoring_function",
|
||||
reason="Correct task span name and output value",
|
||||
value=1.0,
|
||||
),
|
||||
FeedbackScoreDict(
|
||||
category_name=None,
|
||||
name="task_span_name_scoring_function",
|
||||
reason="Correct task span name",
|
||||
value=1.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
# sort feedback scores by name
|
||||
for item in experiment_items_contents:
|
||||
item.feedback_scores = sorted(item.feedback_scores, key=lambda x: x["name"])
|
||||
|
||||
assert_equal(
|
||||
expected=EXPECTED_EXPERIMENT_ITEMS_CONTENT, actual=experiment_items_contents
|
||||
)
|
||||
@@ -0,0 +1,158 @@
|
||||
import threading
|
||||
from typing import Dict, Any, List
|
||||
from unittest import mock
|
||||
|
||||
import opik
|
||||
from opik.api_objects import rest_stream_parser
|
||||
from opik.evaluation import helpers as helpers_module
|
||||
from opik.evaluation.metrics import score_result
|
||||
from opik.types import FeedbackScoreDict
|
||||
|
||||
from ...testlib import generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
|
||||
# Simple dataset items for testing
|
||||
DATASET_ITEMS = [
|
||||
{
|
||||
"input": {"question": f"Question {i}"},
|
||||
"expected_output": {"answer": f"Answer {i}"},
|
||||
}
|
||||
for i in range(20) # Create 20 items to test streaming behavior
|
||||
]
|
||||
|
||||
# Batch size for streaming - must be less than len(DATASET_ITEMS) to test multi-batch streaming
|
||||
TEST_BATCH_SIZE = 5
|
||||
|
||||
|
||||
def simple_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Simple task that echoes the expected output."""
|
||||
return item["expected_output"]
|
||||
|
||||
|
||||
def simple_scoring_function(
|
||||
dataset_item: Dict[str, Any], task_outputs: Dict[str, Any]
|
||||
) -> score_result.ScoreResult:
|
||||
"""Simple scoring function that always returns 1.0."""
|
||||
return score_result.ScoreResult(
|
||||
name="simple_score",
|
||||
value=1.0,
|
||||
reason="Test score",
|
||||
)
|
||||
|
||||
|
||||
def test_streaming_starts_evaluation_before_complete_download(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""
|
||||
Test that streaming evaluation starts processing items before all items are downloaded.
|
||||
|
||||
This test verifies the core streaming behavior by:
|
||||
1. Creating a dataset with multiple items
|
||||
2. Setting a small batch size to force multiple batches
|
||||
3. Patching read_and_parse_stream to track when items are yielded
|
||||
4. Patching the task execution to track when tasks start
|
||||
5. Verifying that the first task starts before the last item is yielded
|
||||
"""
|
||||
# Create dataset with multiple items
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
dataset.insert(DATASET_ITEMS)
|
||||
|
||||
# Track the sequence of events: 'yield' or 'task'
|
||||
events: List[str] = []
|
||||
events_lock = threading.Lock()
|
||||
|
||||
# Store original read_and_parse_stream function
|
||||
original_read_and_parse_stream = rest_stream_parser.read_and_parse_stream
|
||||
|
||||
def tracked_read_and_parse_stream(stream, item_class, nb_samples=None):
|
||||
"""Wrapper that tracks when items are yielded from the stream."""
|
||||
items = original_read_and_parse_stream(stream, item_class, nb_samples)
|
||||
tracked_items = []
|
||||
for item in items:
|
||||
with events_lock:
|
||||
events.append("yield")
|
||||
tracked_items.append(item)
|
||||
return tracked_items
|
||||
|
||||
def tracked_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Wrapper that tracks when tasks start executing."""
|
||||
with events_lock:
|
||||
events.append("task")
|
||||
return simple_task(item)
|
||||
|
||||
# Patch both read_and_parse_stream to track yields and EVALUATION_STREAM_DATASET_BATCH_SIZE
|
||||
# to ensure we stream in multiple batches
|
||||
with (
|
||||
mock.patch.object(
|
||||
rest_stream_parser,
|
||||
"read_and_parse_stream",
|
||||
side_effect=tracked_read_and_parse_stream,
|
||||
),
|
||||
mock.patch.object(
|
||||
helpers_module, "EVALUATION_STREAM_DATASET_BATCH_SIZE", TEST_BATCH_SIZE
|
||||
),
|
||||
):
|
||||
# Run evaluation with streaming
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=tracked_task,
|
||||
scoring_functions=[simple_scoring_function],
|
||||
experiment_name=experiment_name,
|
||||
verbose=1,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# Verify evaluation completed successfully
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
assert len(events) == len(DATASET_ITEMS) * 2 # Each item has yield + task
|
||||
|
||||
# Count yields and tasks
|
||||
yield_count = events.count("yield")
|
||||
task_count = events.count("task")
|
||||
assert yield_count == len(DATASET_ITEMS)
|
||||
assert task_count == len(DATASET_ITEMS)
|
||||
|
||||
# Critical assertion: Tasks should be interleaved with yields
|
||||
# This proves streaming is working - we don't wait for all yields before starting tasks
|
||||
# Find the index of the last yield
|
||||
last_yield_index = len(events) - 1 - events[::-1].index("yield")
|
||||
# Find the index of the first task
|
||||
first_task_index = events.index("task")
|
||||
|
||||
assert first_task_index < last_yield_index, (
|
||||
f"Streaming not working! First task appeared at index {first_task_index}, "
|
||||
f"but last yield appeared at index {last_yield_index}. "
|
||||
f"Tasks should start before all items are yielded. "
|
||||
f"Event sequence: {events}"
|
||||
)
|
||||
|
||||
# Verify the experiment was created correctly
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_items = retrieved_experiment.get_items()
|
||||
assert len(experiment_items) == len(DATASET_ITEMS)
|
||||
assert retrieved_experiment.project_name == PROJECT_NAME
|
||||
|
||||
# Verify scoring output: each item should have a score with name "simple_score" and value 1.0
|
||||
for item in experiment_items:
|
||||
# Check that feedback_scores exists and is not empty
|
||||
assert item.feedback_scores is not None and len(item.feedback_scores) == 1, (
|
||||
f"Experiment item {item.id} should have exactly 1 feedback score, got {len(item.feedback_scores) if item.feedback_scores else 0}"
|
||||
)
|
||||
|
||||
# Verify the score matches expected structure
|
||||
expected_score = FeedbackScoreDict(
|
||||
category_name=None,
|
||||
name="simple_score",
|
||||
reason="Test score",
|
||||
value=1.0,
|
||||
)
|
||||
|
||||
actual_score = item.feedback_scores[0]
|
||||
assert actual_score == expected_score, (
|
||||
f"Experiment item {item.id} has incorrect feedback score. "
|
||||
f"Expected: {expected_score}, Got: {actual_score}"
|
||||
)
|
||||
@@ -0,0 +1,487 @@
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
import opik
|
||||
from opik import Prompt, synchronization
|
||||
from opik.evaluation import metrics
|
||||
from opik.evaluation.metrics import BaseMetric, score_result
|
||||
from opik.message_processing.emulation import models
|
||||
|
||||
from .. import verifiers
|
||||
from ..conftest import random_chars
|
||||
|
||||
|
||||
def _wait_for_per_item_feedback_scores(
|
||||
opik_client: opik.Opik,
|
||||
experiment_name: str,
|
||||
expected_count_per_item: int,
|
||||
expected_item_count: int,
|
||||
max_try_seconds: float = 10.0,
|
||||
) -> List[Any]:
|
||||
"""
|
||||
Poll the backend until every experiment item has ``expected_count_per_item``
|
||||
feedback scores. Returns the materialized list of experiment items.
|
||||
|
||||
The verifier ``verifiers.verify_experiment`` only checks the **experiment-
|
||||
level** ``feedback_scores`` aggregate (one row per unique score name).
|
||||
Per-item feedback scores can lag for an extra moment after the
|
||||
aggregate has converged — especially when task-span scoring writes the
|
||||
second batch of scores via ``client.log_traces_feedback_scores`` after
|
||||
the per-item trace has been emitted. Polling avoids race-condition
|
||||
flakes against the read-back without masking real bugs (a bounded
|
||||
timeout still fails the test if the count never settles).
|
||||
"""
|
||||
last_items: List[Any] = []
|
||||
|
||||
def _check() -> bool:
|
||||
nonlocal last_items
|
||||
experiment = opik_client.get_experiment_by_name(experiment_name)
|
||||
last_items = experiment.get_items()
|
||||
if len(last_items) != expected_item_count:
|
||||
return False
|
||||
return all(
|
||||
len(item.feedback_scores) == expected_count_per_item for item in last_items
|
||||
)
|
||||
|
||||
converged = synchronization.until(
|
||||
_check, max_try_seconds=max_try_seconds, allow_errors=True
|
||||
)
|
||||
assert converged, (
|
||||
f"Per-item feedback score counts did not converge to "
|
||||
f"{expected_count_per_item} within {max_try_seconds}s. "
|
||||
f"Last observed: items={len(last_items)}, "
|
||||
f"counts={[len(item.feedback_scores) for item in last_items]}"
|
||||
)
|
||||
return last_items
|
||||
|
||||
|
||||
class TaskSpanTestMetric(BaseMetric):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "task_span_test_metric",
|
||||
track: bool = True,
|
||||
project_name: Optional[str] = None,
|
||||
):
|
||||
super().__init__(name=name, track=track, project_name=project_name)
|
||||
|
||||
def score(
|
||||
self, task_span: models.SpanModel, **ignored_kwargs: Any
|
||||
) -> score_result.ScoreResult:
|
||||
score = 1.0 if task_span.name == "task" else 0.0
|
||||
return score_result.ScoreResult(
|
||||
value=score,
|
||||
name=self.name,
|
||||
reason="Correct task span name"
|
||||
if score == 1.0
|
||||
else "Incorrect task span name",
|
||||
)
|
||||
|
||||
|
||||
class TaskSpanInputTestMetric(BaseMetric):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "task_span_input_test_metric",
|
||||
track: bool = True,
|
||||
project_name: Optional[str] = None,
|
||||
):
|
||||
super().__init__(name=name, track=track, project_name=project_name)
|
||||
|
||||
def score(
|
||||
self, task_span: models.SpanModel, **ignored_kwargs: Any
|
||||
) -> score_result.ScoreResult:
|
||||
input_data = task_span.input
|
||||
has_question = (
|
||||
isinstance(input_data, dict)
|
||||
and "item" in input_data
|
||||
and "input" in input_data["item"]
|
||||
and isinstance(input_data["item"]["input"], dict)
|
||||
and "question" in input_data["item"]["input"]
|
||||
)
|
||||
score = 1.0 if has_question else 0.0
|
||||
return score_result.ScoreResult(
|
||||
value=score,
|
||||
name=self.name,
|
||||
reason="Task span has question input"
|
||||
if score == 1.0
|
||||
else "Task span missing question input",
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate__with_task_span_metrics__single_metric__happy_flow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the capital of Germany?"},
|
||||
"expected_model_output": {"output": "Berlin"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the capital of Germany?"}:
|
||||
return {"output": "Berlin"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt = Prompt(
|
||||
name=f"test-task-span-prompt-{random_chars()}",
|
||||
prompt=f"test-task-span-prompt-template-{random_chars()}",
|
||||
)
|
||||
|
||||
task_span_metric = TaskSpanTestMetric()
|
||||
equals_metric = metrics.Equals()
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric, task_span_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "test-model",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
prompts=[prompt],
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "test-model"},
|
||||
traces_amount=2,
|
||||
feedback_scores_amount=2, # equals_metric + task_span_metric
|
||||
prompts=[prompt],
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
|
||||
experiment_items_contents = _wait_for_per_item_feedback_scores(
|
||||
opik_client,
|
||||
experiment_name,
|
||||
expected_count_per_item=2,
|
||||
expected_item_count=2,
|
||||
)
|
||||
|
||||
for item in experiment_items_contents:
|
||||
score_names = [score["name"] for score in item.feedback_scores]
|
||||
assert "equals_metric" in score_names
|
||||
assert "task_span_test_metric" in score_names
|
||||
|
||||
# Find task span metric score
|
||||
task_span_score = next(
|
||||
score
|
||||
for score in item.feedback_scores
|
||||
if score["name"] == "task_span_test_metric"
|
||||
)
|
||||
assert task_span_score["value"] == 1.0
|
||||
assert "Correct task span name" in task_span_score["reason"]
|
||||
|
||||
|
||||
def test_evaluate__with_task_span_metrics__multiple_task_span_metrics__happyflow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the capital of Spain?"},
|
||||
"expected_model_output": {"output": "Madrid"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of Spain?"}:
|
||||
return {"output": "Madrid"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
task_span_metric_1 = TaskSpanTestMetric(name="task_span_metric_1")
|
||||
task_span_metric_2 = TaskSpanInputTestMetric(name="task_span_metric_2")
|
||||
equals_metric = metrics.Equals()
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric, task_span_metric_1, task_span_metric_2],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "test-model-v2",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "test-model-v2"},
|
||||
traces_amount=1,
|
||||
feedback_scores_amount=3, # equals_metric + 2 task_span_metrics
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
|
||||
experiment_items_contents = _wait_for_per_item_feedback_scores(
|
||||
opik_client,
|
||||
experiment_name,
|
||||
expected_count_per_item=3,
|
||||
expected_item_count=1,
|
||||
)
|
||||
item = experiment_items_contents[0]
|
||||
score_names = [score["name"] for score in item.feedback_scores]
|
||||
assert "equals_metric" in score_names
|
||||
assert "task_span_metric_1" in score_names
|
||||
assert "task_span_metric_2" in score_names
|
||||
|
||||
# Verify all task span metrics scored correctly
|
||||
for score in item.feedback_scores:
|
||||
if score["name"] in ["task_span_metric_1", "task_span_metric_2"]:
|
||||
assert score["value"] == 1.0
|
||||
|
||||
|
||||
def test_evaluate__with_task_span_metrics__only_task_span_metrics__no_regular_metrics(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the capital of Italy?"},
|
||||
"expected_model_output": {"output": "Rome"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of Italy?"}:
|
||||
return {"output": "Rome"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
task_span_metric = TaskSpanTestMetric()
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[task_span_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "task-span-only-model",
|
||||
},
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "task-span-only-model"},
|
||||
traces_amount=1,
|
||||
feedback_scores_amount=1, # only task_span_metric
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id
|
||||
|
||||
experiment_items_contents = _wait_for_per_item_feedback_scores(
|
||||
opik_client,
|
||||
experiment_name,
|
||||
expected_count_per_item=1,
|
||||
expected_item_count=1,
|
||||
)
|
||||
item = experiment_items_contents[0]
|
||||
score = item.feedback_scores[0]
|
||||
assert score["name"] == "task_span_test_metric"
|
||||
assert score["value"] == 1.0
|
||||
|
||||
|
||||
def test_evaluate__with_task_span_metrics__mixed_with_regular_metrics__multiple_trials(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the capital of Japan?"},
|
||||
"expected_model_output": {"output": "Tokyo"},
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the capital of Canada?"},
|
||||
"expected_model_output": {"output": "Ottawa"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of Japan?"}:
|
||||
return {"output": "Tokyo"}
|
||||
if item["input"] == {"question": "What is the capital of Canada?"}:
|
||||
return {"output": "Ottawa"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt = Prompt(
|
||||
name=f"test-mixed-metrics-prompt-{random_chars()}",
|
||||
prompt=f"test-mixed-metrics-prompt-template-{random_chars()}",
|
||||
)
|
||||
|
||||
# Mix of regular and task span metrics
|
||||
equals_metric = metrics.Equals(name="regular_equals")
|
||||
contains_metric = metrics.Contains(name="regular_contains")
|
||||
task_span_metric = TaskSpanTestMetric(name="span_name_check")
|
||||
task_span_input_metric = TaskSpanInputTestMetric(name="span_input_check")
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[
|
||||
equals_metric,
|
||||
task_span_metric,
|
||||
contains_metric,
|
||||
task_span_input_metric,
|
||||
],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "mixed-metrics-model",
|
||||
"version": "1.0",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
prompt=prompt,
|
||||
trial_count=5,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "mixed-metrics-model", "version": "1.0"},
|
||||
traces_amount=2 * 5, # 2 traces per dataset item per trial
|
||||
feedback_scores_amount=4, # 2 regular + 2 task_span metrics
|
||||
prompts=[prompt],
|
||||
)
|
||||
|
||||
experiment_items_contents = _wait_for_per_item_feedback_scores(
|
||||
opik_client,
|
||||
experiment_name,
|
||||
expected_count_per_item=4,
|
||||
expected_item_count=2 * 5,
|
||||
)
|
||||
|
||||
expected_score_names = {
|
||||
"regular_equals",
|
||||
"regular_contains",
|
||||
"span_name_check",
|
||||
"span_input_check",
|
||||
}
|
||||
|
||||
for item in experiment_items_contents:
|
||||
actual_score_names = {score["name"] for score in item.feedback_scores}
|
||||
assert actual_score_names == expected_score_names
|
||||
|
||||
# Verify all metrics scored correctly (assuming perfect matches)
|
||||
for score in item.feedback_scores:
|
||||
assert score["value"] == 1.0
|
||||
|
||||
|
||||
class TaskSpanWithMultipleParametersMetric(BaseMetric):
|
||||
"""
|
||||
Metric that verifies multiple parameters are passed correctly:
|
||||
- task_span: the span information
|
||||
- input: from dataset item
|
||||
- output: from task output
|
||||
- **ignored_kwargs: to handle any other parameters
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "task_span_multi_param_metric",
|
||||
track: bool = True,
|
||||
project_name: Optional[str] = None,
|
||||
):
|
||||
super().__init__(name=name, track=track, project_name=project_name)
|
||||
|
||||
def score(
|
||||
self,
|
||||
task_span: models.SpanModel,
|
||||
input: Dict[str, Any],
|
||||
output: str,
|
||||
**ignored_kwargs: Any,
|
||||
) -> score_result.ScoreResult:
|
||||
# Simply verify all expected parameters are present and store them in metadata
|
||||
return score_result.ScoreResult(
|
||||
value=1.0,
|
||||
name=self.name,
|
||||
reason=f"Received task_span={type(task_span).__name__}, input={type(input).__name__}, output={type(output).__name__}",
|
||||
metadata={
|
||||
"input": input,
|
||||
"output": output,
|
||||
"task_span_name": task_span.name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate__with_task_span_metrics__metric_with_multiple_parameters__happy_flow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""
|
||||
Test that task_span metrics can access task_span, dataset item content (input),
|
||||
and task output (output) parameters. Verifies arguments are passed correctly.
|
||||
"""
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert([{"input": {"question": "What is 2+2?"}}])
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
return {"output": "4"}
|
||||
|
||||
multi_param_metric = TaskSpanWithMultipleParametersMetric()
|
||||
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[multi_param_metric],
|
||||
experiment_name=experiment_name,
|
||||
)
|
||||
|
||||
# Verify the metric received all expected parameters in local test results
|
||||
assert len(evaluation_result.test_results) == 1
|
||||
test_result = evaluation_result.test_results[0]
|
||||
assert len(test_result.score_results) == 1
|
||||
|
||||
score_result = test_result.score_results[0]
|
||||
assert score_result.name == "task_span_multi_param_metric"
|
||||
assert score_result.value == 1.0
|
||||
assert "task_span=SpanModel" in score_result.reason
|
||||
assert "input=dict" in score_result.reason
|
||||
assert "output=str" in score_result.reason
|
||||
|
||||
# Verify the parameters were stored correctly in metadata
|
||||
assert score_result.metadata is not None
|
||||
assert score_result.metadata["input"] == {"question": "What is 2+2?"}
|
||||
assert score_result.metadata["output"] == "4"
|
||||
assert score_result.metadata["task_span_name"] == "task"
|
||||
@@ -0,0 +1,749 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
import opik
|
||||
|
||||
from opik import Prompt, synchronization, id_helpers
|
||||
from opik.api_objects.dataset import dataset_item
|
||||
from opik.evaluation import metrics
|
||||
from opik.evaluation import test_result
|
||||
from opik.evaluation.metrics import score_result
|
||||
from opik.api_objects.experiment import experiment_item
|
||||
from .. import verifiers
|
||||
from ..conftest import random_chars
|
||||
from ...testlib import assert_equal, ANY_BUT_NONE, generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
|
||||
def test_experiment_creation_via_evaluate_function__single_prompt_arg_used__happyflow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the of capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the of capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the of capital of Poland?"}:
|
||||
return {"output": "Krakow"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt = Prompt(
|
||||
name=f"test-experiment-prompt-{random_chars()}",
|
||||
prompt=f"test-experiment-prompt-template-{random_chars()}",
|
||||
)
|
||||
|
||||
experiment_tags = ["capital", "geography", "europe"]
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
prompt=prompt,
|
||||
experiment_tags=experiment_tags,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=2, # one trace per dataset item
|
||||
feedback_scores_amount=1,
|
||||
prompts=[prompt],
|
||||
experiment_tags=experiment_tags,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id, (
|
||||
f"Expected evaluation result dataset_id '{dataset.id}', but got '{evaluation_result.dataset_id}'"
|
||||
)
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 2, (
|
||||
f"Expected 2 experiment items, but got {len(experiment_items_contents)}. "
|
||||
f"Experiment items: {experiment_items_contents}"
|
||||
)
|
||||
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT = [
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Paris"},
|
||||
feedback_scores=[
|
||||
{
|
||||
"category_name": None,
|
||||
"name": "equals_metric",
|
||||
"reason": None,
|
||||
"value": 1.0,
|
||||
}
|
||||
],
|
||||
),
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Krakow"},
|
||||
feedback_scores=[
|
||||
{
|
||||
"category_name": None,
|
||||
"name": "equals_metric",
|
||||
"reason": None,
|
||||
"value": 0.0,
|
||||
}
|
||||
],
|
||||
),
|
||||
]
|
||||
assert_equal(
|
||||
sorted(
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT,
|
||||
key=lambda item: str(item.dataset_item_data),
|
||||
),
|
||||
sorted(experiment_items_contents, key=lambda item: str(item.dataset_item_data)),
|
||||
)
|
||||
|
||||
|
||||
def test_experiment_creation_via_evaluate_function__single_prompt_arg_used__filter_dataset_items_by_id(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset_items = [
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
{
|
||||
"id": id_helpers.generate_id(),
|
||||
"input": {"question": "What is the of capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
},
|
||||
]
|
||||
|
||||
dataset.insert(dataset_items)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the of capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the of capital of Poland?"}:
|
||||
return {"output": "Krakow"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt = Prompt(
|
||||
name=f"test-experiment-prompt-{random_chars()}",
|
||||
prompt=f"test-experiment-prompt-template-{random_chars()}",
|
||||
)
|
||||
|
||||
# Keep France, drop Poland; append a fake id so the filter still covers
|
||||
# the "non-existent id is ignored" case.
|
||||
dataset_item_ids = [item["id"] for item in dataset_items]
|
||||
dataset_item_ids.pop(1)
|
||||
dataset_item_ids.append(id_helpers.generate_id())
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
prompt=prompt,
|
||||
dataset_item_ids=dataset_item_ids,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=1, # one trace per dataset item (fake id is skipped)
|
||||
feedback_scores_amount=1,
|
||||
prompts=[prompt],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id, (
|
||||
f"Expected evaluation result dataset_id '{dataset.id}', but got '{evaluation_result.dataset_id}'"
|
||||
)
|
||||
|
||||
retrieved_experiments = opik_client.get_experiments_by_name(
|
||||
experiment_name, project_name=PROJECT_NAME
|
||||
)
|
||||
assert len(retrieved_experiments) == 1, (
|
||||
f"Expected 1 experiment, but got {len(retrieved_experiments)}. "
|
||||
f"Experiments: {retrieved_experiments}"
|
||||
)
|
||||
retrieved_experiment = retrieved_experiments[0]
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 1, (
|
||||
f"Expected 1 experiment item, but got {len(experiment_items_contents)}. "
|
||||
f"Experiment items: {experiment_items_contents}"
|
||||
)
|
||||
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT = [
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Paris"},
|
||||
feedback_scores=[
|
||||
{
|
||||
"category_name": None,
|
||||
"name": "equals_metric",
|
||||
"reason": None,
|
||||
"value": 1.0,
|
||||
}
|
||||
],
|
||||
),
|
||||
]
|
||||
assert_equal(
|
||||
sorted(
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT,
|
||||
key=lambda item: str(item.dataset_item_data),
|
||||
),
|
||||
sorted(experiment_items_contents, key=lambda item: str(item.dataset_item_data)),
|
||||
)
|
||||
|
||||
|
||||
def test_experiment_creation_via_evaluate_function__multiple_prompts_arg_used__happyflow(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the of capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the of capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the of capital of Poland?"}:
|
||||
return {"output": "Krakow"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt1 = Prompt(
|
||||
name=f"test-experiment-prompt-{random_chars()}",
|
||||
prompt=f"test-experiment-prompt-template-{random_chars()}",
|
||||
)
|
||||
prompt2 = Prompt(
|
||||
name=f"test-experiment-prompt-{random_chars()}",
|
||||
prompt=f"test-experiment-prompt-template-{random_chars()}",
|
||||
)
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
prompts=[prompt1, prompt2],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=2, # one trace per dataset item
|
||||
feedback_scores_amount=1,
|
||||
prompts=[prompt1, prompt2],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id, (
|
||||
f"Expected evaluation result dataset_id '{dataset.id}', but got '{evaluation_result.dataset_id}'"
|
||||
)
|
||||
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_items_contents = retrieved_experiment.get_items()
|
||||
assert len(experiment_items_contents) == 2, (
|
||||
f"Expected 2 experiment items, but got {len(experiment_items_contents)}. "
|
||||
f"Experiment items: {experiment_items_contents}"
|
||||
)
|
||||
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT = [
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Paris"},
|
||||
feedback_scores=[
|
||||
{
|
||||
"category_name": None,
|
||||
"name": "equals_metric",
|
||||
"reason": None,
|
||||
"value": 1.0,
|
||||
}
|
||||
],
|
||||
),
|
||||
experiment_item.ExperimentItemContent(
|
||||
id=ANY_BUT_NONE,
|
||||
dataset_item_id=ANY_BUT_NONE,
|
||||
trace_id=ANY_BUT_NONE,
|
||||
dataset_item_data={
|
||||
"input": {"question": "What is the of capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
"id": ANY_BUT_NONE,
|
||||
},
|
||||
evaluation_task_output={"output": "Krakow"},
|
||||
feedback_scores=[
|
||||
{
|
||||
"category_name": None,
|
||||
"name": "equals_metric",
|
||||
"reason": None,
|
||||
"value": 0.0,
|
||||
}
|
||||
],
|
||||
),
|
||||
]
|
||||
assert_equal(
|
||||
sorted(
|
||||
EXPECTED_EXPERIMENT_ITEMS_CONTENT,
|
||||
key=lambda item: str(item.dataset_item_data),
|
||||
),
|
||||
sorted(experiment_items_contents, key=lambda item: str(item.dataset_item_data)),
|
||||
)
|
||||
|
||||
verifiers.verify_experiment_traces_have_opik_prompts(
|
||||
opik_client=opik_client,
|
||||
trace_ids=[item.trace_id for item in experiment_items_contents],
|
||||
prompts=[prompt1, prompt2],
|
||||
)
|
||||
|
||||
|
||||
def test_experiment_creation__name_can_be_omitted(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""
|
||||
We can send "None" as experiment_name and the backend will set it for us
|
||||
"""
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"reference": "Paris",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: dataset_item.DatasetItem):
|
||||
if item["input"] == {"question": "What is the of capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=None,
|
||||
)
|
||||
|
||||
experiment_id = evaluation_result.experiment_id
|
||||
|
||||
if not synchronization.until(
|
||||
lambda: (
|
||||
opik_client._rest_client.experiments.get_experiment_by_id(experiment_id)
|
||||
is not None
|
||||
),
|
||||
allow_errors=True,
|
||||
):
|
||||
raise AssertionError(f"Failed to get experiment with id {experiment_id}.")
|
||||
|
||||
experiment_content = opik_client._rest_client.experiments.get_experiment_by_id(
|
||||
experiment_id
|
||||
)
|
||||
|
||||
assert experiment_content.name is not None, (
|
||||
f"Expected experiment name to be set by backend, but got None. "
|
||||
f"Experiment ID: {experiment_id}, Experiment content: {experiment_content}"
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate_experiment__an_experiment_created_with_evaluate__then_new_scores_are_added_to_existing_experiment_items__amount_of_feedback_scores_increased(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the of capital of France?"}:
|
||||
return {
|
||||
"output": "Paris",
|
||||
"reference": item["expected_model_output"]["output"],
|
||||
}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt = Prompt(
|
||||
name=f"test-experiment-prompt-{random_chars()}",
|
||||
prompt=f"test-experiment-prompt-template-{random_chars()}",
|
||||
)
|
||||
|
||||
# Create the experiment first
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
prompt=prompt,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
traces_amount=1,
|
||||
feedback_scores_amount=0,
|
||||
prompts=[prompt],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# Populate the existing experiment with a new feedback score
|
||||
evaluation_result = opik.evaluate_experiment(
|
||||
experiment_name=experiment_name,
|
||||
scoring_metrics=[
|
||||
metrics.Equals(name="metric1"),
|
||||
metrics.Equals(name="metric2"),
|
||||
metrics.Equals(name="metric3"),
|
||||
],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
traces_amount=1,
|
||||
feedback_scores_amount=3,
|
||||
prompts=[prompt],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
assert evaluation_result.dataset_id == dataset.id, (
|
||||
f"Expected evaluation result dataset_id '{dataset.id}', but got '{evaluation_result.dataset_id}'"
|
||||
)
|
||||
|
||||
|
||||
def test_experiment__get_experiments_by_name(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the of capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the of capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the of capital of Poland?"}:
|
||||
return {"output": "Krakow"}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
prompt = Prompt(
|
||||
name=f"test-experiment-prompt-{random_chars()}",
|
||||
prompt=f"test-experiment-prompt-template-{random_chars()}",
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
experiments_names = [experiment_name, experiment_name, random_chars(10)]
|
||||
|
||||
evaluation_results = []
|
||||
equals_metric = metrics.Equals()
|
||||
for name in experiments_names:
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
prompt=prompt,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
evaluation_results.append(evaluation_result)
|
||||
|
||||
# make sure experiments saved and available
|
||||
for result in evaluation_results:
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=result.experiment_id,
|
||||
experiment_name=result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=2, # one trace per dataset item
|
||||
feedback_scores_amount=1,
|
||||
prompts=[prompt],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# check getting experiment by name
|
||||
experiments = opik_client.get_experiments_by_name(
|
||||
experiment_name, project_name=PROJECT_NAME
|
||||
)
|
||||
assert len(experiments) == 2, (
|
||||
f"Expected 2 experiments with name '{experiment_name}', but got {len(experiments)}. "
|
||||
f"Experiment IDs: {[e.id for e in experiments]}"
|
||||
)
|
||||
assert all(experiment.project_name == PROJECT_NAME for experiment in experiments)
|
||||
|
||||
single = opik_client.get_experiment_by_name(
|
||||
experiment_name, project_name=PROJECT_NAME
|
||||
)
|
||||
assert single is not None
|
||||
assert single.id in [e.id for e in experiments]
|
||||
|
||||
experiments = opik_client.get_experiments_by_name(
|
||||
experiments_names[2], project_name=PROJECT_NAME
|
||||
)
|
||||
assert len(experiments) == 1, (
|
||||
f"Expected 1 experiment with name '{experiments_names[2]}', but got {len(experiments)}. "
|
||||
f"Experiment IDs: {[e.id for e in experiments]}"
|
||||
)
|
||||
|
||||
|
||||
def test_experiment__get_experiment_items__no_feedback_scores(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
dataset = opik_client.create_dataset(dataset_name)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the of capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"output": "Paris",
|
||||
}
|
||||
|
||||
opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[],
|
||||
experiment_name=experiment_name,
|
||||
)
|
||||
|
||||
experiment = opik_client.get_experiment_by_name(experiment_name)
|
||||
items = experiment.get_items()
|
||||
|
||||
assert len(items) == 1, (
|
||||
f"Expected 1 experiment item, but got {len(items)}. Items: {items}"
|
||||
)
|
||||
assert items[0].feedback_scores == [], (
|
||||
f"Expected empty feedback scores, but got {items[0].feedback_scores}. "
|
||||
f"Item: {items[0]}"
|
||||
)
|
||||
|
||||
|
||||
def test_experiment_creation_via_evaluate_function__with_experiment_scoring_functions__scores_computed_and_logged(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""Test that experiment scoring functions compute and log experiment-level scores."""
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the capital of France?"},
|
||||
"expected_model_output": {"output": "Paris"},
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the capital of Poland?"},
|
||||
"expected_model_output": {"output": "Warsaw"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
if item["input"] == {"question": "What is the capital of France?"}:
|
||||
return {"output": "Paris"}
|
||||
if item["input"] == {"question": "What is the capital of Poland?"}:
|
||||
return {"output": "Krakow"} # Wrong answer
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
def constant_score(
|
||||
test_results: List[test_result.TestResult],
|
||||
) -> List[score_result.ScoreResult]:
|
||||
"""Compute a random number based on the number of test results."""
|
||||
return [
|
||||
score_result.ScoreResult(
|
||||
name="fixed_number",
|
||||
value=0.8,
|
||||
reason="Fixed score of 0.8",
|
||||
)
|
||||
]
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "gpt-3.5",
|
||||
},
|
||||
scoring_key_mapping={
|
||||
"reference": lambda x: x["expected_model_output"]["output"],
|
||||
},
|
||||
experiment_scoring_functions=[constant_score],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# Verify experiment scores are in the result
|
||||
assert len(evaluation_result.experiment_scores) == 1, (
|
||||
f"Expected 1 experiment score in evaluation result, but got {len(evaluation_result.experiment_scores)}. "
|
||||
f"Experiment scores: {evaluation_result.experiment_scores}"
|
||||
)
|
||||
assert evaluation_result.experiment_scores[0].name == "fixed_number", (
|
||||
f"Expected experiment score name 'fixed_number', but got '{evaluation_result.experiment_scores[0].name}'. "
|
||||
f"Full score object: {evaluation_result.experiment_scores[0]}"
|
||||
)
|
||||
assert evaluation_result.experiment_scores[0].value == 0.8, (
|
||||
f"Expected experiment score value 0.8, but got {evaluation_result.experiment_scores[0].value}. "
|
||||
f"Full score object: {evaluation_result.experiment_scores[0]}"
|
||||
)
|
||||
assert evaluation_result.experiment_scores[0].reason is not None, (
|
||||
f"Expected experiment score reason to be set, but got None. "
|
||||
f"Score: {evaluation_result.experiment_scores[0]}"
|
||||
)
|
||||
assert "Fixed score of 0.8" in evaluation_result.experiment_scores[0].reason, (
|
||||
f"Expected reason to contain 'Fixed score of 0.8', but got: '{evaluation_result.experiment_scores[0].reason}'"
|
||||
)
|
||||
|
||||
# Verify experiment was created with experiment scores
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "gpt-3.5"},
|
||||
traces_amount=2, # one trace per dataset item
|
||||
feedback_scores_amount=1,
|
||||
experiment_scores={"fixed_number": 0.8},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
import statistics
|
||||
from typing import Dict, Any, List
|
||||
|
||||
import pytest
|
||||
|
||||
import opik
|
||||
from opik.evaluation import metrics
|
||||
from opik.evaluation import test_result
|
||||
from opik.evaluation.metrics import score_result
|
||||
from .. import verifiers
|
||||
from ...testlib import generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
|
||||
def test_experiment_scoring_functions__standard_deviation__computed_and_logged(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""Test that experiment scoring functions can compute and log standard deviation of metric scores."""
|
||||
dataset = opik_client.create_dataset(dataset_name, project_name=PROJECT_NAME)
|
||||
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"question": "What is the capital of France?"},
|
||||
"expected_output": "Paris",
|
||||
},
|
||||
{
|
||||
"input": {"question": "What is the capital of Poland?"},
|
||||
"expected_output": "Warsaw",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def task(item: Dict[str, Any]):
|
||||
# Return correct answer for France, wrong answer for Poland so
|
||||
# metric scores are [1.0, 0.0] — enough to exercise stdev.
|
||||
if item["input"] == {"question": "What is the capital of France?"}:
|
||||
return {"output": "Paris", "reference": item["expected_output"]}
|
||||
if item["input"] == {"question": "What is the capital of Poland?"}:
|
||||
return {"output": "Krakow", "reference": item["expected_output"]}
|
||||
|
||||
raise AssertionError(
|
||||
f"Task received dataset item with an unexpected input: {item['input']}"
|
||||
)
|
||||
|
||||
def compute_std_deviation(
|
||||
test_results: List[test_result.TestResult],
|
||||
) -> List[score_result.ScoreResult]:
|
||||
"""Compute standard deviation of metric scores across all test results."""
|
||||
scores = [x.score_results[0].value for x in test_results]
|
||||
return [
|
||||
score_result.ScoreResult(
|
||||
name="equals_metric_std_dev",
|
||||
value=statistics.stdev(scores) if len(scores) > 1 else 0.0,
|
||||
reason=f"Standard deviation of {len(scores)} metric scores",
|
||||
)
|
||||
]
|
||||
|
||||
equals_metric = metrics.Equals()
|
||||
evaluation_result = opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=task,
|
||||
scoring_metrics=[equals_metric],
|
||||
experiment_name=experiment_name,
|
||||
experiment_config={
|
||||
"model_name": "test-model",
|
||||
},
|
||||
experiment_scoring_functions=[compute_std_deviation],
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# Verify experiment scores are in the result
|
||||
assert len(evaluation_result.experiment_scores) == 1, (
|
||||
f"Expected 1 experiment score in evaluation result, but got {len(evaluation_result.experiment_scores)}. "
|
||||
f"Experiment scores: {evaluation_result.experiment_scores}"
|
||||
)
|
||||
assert evaluation_result.experiment_scores[0].name == "equals_metric_std_dev", (
|
||||
f"Expected experiment score name 'equals_metric_std_dev', but got '{evaluation_result.experiment_scores[0].name}'. "
|
||||
f"Full score object: {evaluation_result.experiment_scores[0]}"
|
||||
)
|
||||
|
||||
expected_stdev = statistics.stdev([1.0, 0.0])
|
||||
assert evaluation_result.experiment_scores[0].value == pytest.approx(
|
||||
expected_stdev, abs=0.0001
|
||||
), (
|
||||
f"Expected experiment score value {expected_stdev}, but got {evaluation_result.experiment_scores[0].value}. "
|
||||
f"Full score object: {evaluation_result.experiment_scores[0]}"
|
||||
)
|
||||
assert evaluation_result.experiment_scores[0].reason is not None, (
|
||||
f"Expected experiment score reason to be set, but got None. "
|
||||
f"Score: {evaluation_result.experiment_scores[0]}"
|
||||
)
|
||||
assert (
|
||||
"Standard deviation of 2 metric scores"
|
||||
in evaluation_result.experiment_scores[0].reason
|
||||
), (
|
||||
f"Expected reason to contain 'Standard deviation of 2 metric scores', but got: '{evaluation_result.experiment_scores[0].reason}'"
|
||||
)
|
||||
|
||||
# Verify experiment was created
|
||||
verifiers.verify_experiment(
|
||||
opik_client=opik_client,
|
||||
id=evaluation_result.experiment_id,
|
||||
experiment_name=evaluation_result.experiment_name,
|
||||
experiment_metadata={"model_name": "test-model"},
|
||||
traces_amount=2, # one trace per dataset item
|
||||
feedback_scores_amount=1,
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
# Verify experiment scores are logged to backend by retrieving the experiment
|
||||
retrieved_experiment = opik_client.get_experiment_by_id(
|
||||
evaluation_result.experiment_id
|
||||
)
|
||||
experiment_data = retrieved_experiment.get_experiment_data()
|
||||
|
||||
# Check that experiment scores are present in the experiment data
|
||||
assert experiment_data.experiment_scores is not None, (
|
||||
f"Expected experiment_scores to be set in experiment data, but got None. "
|
||||
f"Experiment ID: {evaluation_result.experiment_id}, "
|
||||
f"Experiment name: {evaluation_result.experiment_name}, "
|
||||
f"Experiment data: {experiment_data}"
|
||||
)
|
||||
assert len(experiment_data.experiment_scores) == 1, (
|
||||
f"Expected 1 experiment score in backend, but got {len(experiment_data.experiment_scores) if experiment_data.experiment_scores else 0}. "
|
||||
f"Experiment scores: {experiment_data.experiment_scores}"
|
||||
)
|
||||
assert experiment_data.experiment_scores[0].name == "equals_metric_std_dev", (
|
||||
f"Expected experiment score name 'equals_metric_std_dev' in backend, but got '{experiment_data.experiment_scores[0].name}'. "
|
||||
f"Full score object: {experiment_data.experiment_scores[0]}"
|
||||
)
|
||||
assert experiment_data.experiment_scores[0].value == pytest.approx(
|
||||
expected_stdev, abs=0.0001
|
||||
), (
|
||||
f"Expected experiment score value {expected_stdev} in backend, but got {experiment_data.experiment_scores[0].value}. "
|
||||
f"Full score object: {experiment_data.experiment_scores[0]}"
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,314 @@
|
||||
"""E2E tests for the agentic LLM-judge path on test suites.
|
||||
|
||||
The one-shot LLMJudge sees only the dataset item's top-level `input` /
|
||||
`output`. The agentic judge — auto-engaged when the local emulator is
|
||||
active during `opik.run_tests` — receives a pre-rendered flat overview
|
||||
of the trace plus `read` / `scan` / `search` tools for drilling in.
|
||||
These tests craft assertions that are decidable ONLY from the
|
||||
intermediate span state, so a passing verdict is direct evidence the
|
||||
agentic judge ran:
|
||||
|
||||
1. A span-structure assertion ("the agent called the `fetch_user_data`
|
||||
helper") — verifiable only by looking at span names in the trace.
|
||||
2. A span-error assertion ("no errors occurred without errors") on a
|
||||
trace whose inner span recorded an error caught by the task —
|
||||
verifiable only by looking at span `error_info`.
|
||||
3. A buried-keyword assertion — the marker sits past the overview's
|
||||
floor-tier 500-char truncation, so the judge must call at least one
|
||||
of `read` / `scan` / `search` to recover it. The test pins the
|
||||
overview sizer's ladder to the floor entry to guarantee truncation
|
||||
regardless of the judge model's context budget.
|
||||
|
||||
**Judge-model choice.** These tests deliberately override the SDK's
|
||||
default judge model (`gpt-5-nano`) with a stronger one. `gpt-5-nano`
|
||||
is the canonical "doesn't engage the tool loop" failure mode the
|
||||
backend doc (`SupportedJudgeProvider.java`) warns about — it tends to
|
||||
judge from the inline overview alone, never calling `read`. The SDK
|
||||
can't prompt-engineer around that; the design doc §9 explicitly
|
||||
classifies model engagement as the operator's call. Here we pick
|
||||
`gpt-4o-mini` to match the backend's allow-list second choice — same
|
||||
OpenAI dependency story, but actually engages with tools.
|
||||
|
||||
All require `OPENAI_API_KEY`; the assertions are deliberately
|
||||
unambiguous to keep judge flakiness low even on a competent judge.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
|
||||
import opik
|
||||
from .. import verifiers
|
||||
from ...testlib import environment, generate_project_name
|
||||
|
||||
PROJECT_NAME = generate_project_name("e2e", __name__)
|
||||
|
||||
# Stronger than the SDK default (`gpt-5-nano`) and known to engage the
|
||||
# tool loop — backend `SupportedJudgeProvider.java` lists it as the
|
||||
# OpenAI allow-list entry. If you change this, re-run all three tests:
|
||||
# nano will pass the first two (structural assertions) but reliably
|
||||
# fail the third (it never calls `read` on truncated content).
|
||||
AGENTIC_JUDGE_MODEL = "gpt-5-mini" #
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not environment.has_openai_api_key(), reason="OPENAI_API_KEY is not set"
|
||||
)
|
||||
def test_test_suite_agentic__assertion_about_span_name__passes(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""The agentic judge must inspect the trace's span tree to verify
|
||||
that a specifically-named tool span was actually called. The task
|
||||
output deliberately omits the helper's name, so a one-shot judge
|
||||
has no way to verify the assertion from input/output alone.
|
||||
"""
|
||||
span_assertion = "The agent called a step named `fetch_user_data`"
|
||||
|
||||
@opik.track(name="fetch_user_data", project_name=PROJECT_NAME)
|
||||
def fetch_user_data(user_id: str) -> Dict[str, Any]:
|
||||
return {"id": user_id, "name": "alice"}
|
||||
|
||||
@opik.track(name="task", project_name=PROJECT_NAME)
|
||||
def run_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
fetch_user_data("u-1")
|
||||
# Output deliberately doesn't mention `fetch_user_data` — the
|
||||
# only evidence the helper was called lives in the span tree.
|
||||
return {"input": item["input"], "output": "ok"}
|
||||
|
||||
suite = opik_client.create_test_suite(
|
||||
name=dataset_name,
|
||||
description="Agentic judge — span-name assertion",
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
suite.insert(
|
||||
[
|
||||
{
|
||||
"data": {"input": {"question": "fetch user u-1"}},
|
||||
"assertions": [span_assertion],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
suite_result = opik.run_tests(
|
||||
test_suite=suite,
|
||||
task=run_task,
|
||||
experiment_name=experiment_name,
|
||||
verbose=0,
|
||||
model=AGENTIC_JUDGE_MODEL,
|
||||
scoring_tool_strategy="auto",
|
||||
)
|
||||
|
||||
# The one assertion must surface in the feedback scores.
|
||||
verifiers.verify_test_suite_result(
|
||||
opik_client=opik_client,
|
||||
suite_result=suite_result,
|
||||
items_total=1,
|
||||
items_passed=1,
|
||||
experiment_items_count=1,
|
||||
total_feedback_scores=1,
|
||||
expected_score_names={span_assertion},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not environment.has_openai_api_key(), reason="OPENAI_API_KEY is not set"
|
||||
)
|
||||
@pytest.mark.xfail(reason="flaky on CI with simple OpenAI models")
|
||||
def test_test_suite_agentic__assertion_about_span_error__detects_failure(
|
||||
opik_client: opik.Opik, dataset_name: str, experiment_name: str
|
||||
):
|
||||
"""The agentic judge must inspect `error_info` on a nested span to
|
||||
confirm a *specific* exception type and message. The task catches
|
||||
the exception so the trace's top-level output looks healthy.
|
||||
|
||||
Why the assertion names the exception type AND a phrase from the
|
||||
message: the inline overview already carries each span's
|
||||
`has_error` flag, so an assertion of the form "no errors occurred"
|
||||
would be decidable from the overview alone — the test would pass
|
||||
without ever calling `read` and prove nothing about drill-in. By
|
||||
pinning the verdict on the exception's type and message — both of
|
||||
which live only in `error_info` and are NOT in the overview — the
|
||||
correct verdict is only reachable by reading the span. A judge
|
||||
that stops at `has_error: true` lacks the evidence to confirm
|
||||
"type=RuntimeError" or "message contains 'internal failure'" and
|
||||
must drill in.
|
||||
"""
|
||||
span_error_assertion = (
|
||||
"An internal step raised an exception of type `RuntimeError` "
|
||||
"whose message contains the phrase 'internal failure'"
|
||||
)
|
||||
|
||||
@opik.track(name="risky_step", project_name=PROJECT_NAME)
|
||||
def risky_step() -> str:
|
||||
# @opik.track captures `error_info` on the inner span when this
|
||||
# raises, even though the caller swallows the exception. The
|
||||
# exception type and message are recorded in `error_info` —
|
||||
# *not* exposed via the overview's `has_error` flag — which is
|
||||
# what forces the judge to call `read`.
|
||||
raise RuntimeError("internal failure")
|
||||
|
||||
@opik.track(name="task", project_name=PROJECT_NAME)
|
||||
def run_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
try:
|
||||
risky_step()
|
||||
except RuntimeError:
|
||||
pass
|
||||
return {"input": item["input"], "output": "completed"}
|
||||
|
||||
suite = opik_client.create_test_suite(
|
||||
name=dataset_name,
|
||||
description="Agentic judge — span-error assertion (type + message)",
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
suite.insert(
|
||||
[
|
||||
{
|
||||
"data": {"input": {"question": "do the thing"}},
|
||||
"assertions": [span_error_assertion],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
suite_result = opik.run_tests(
|
||||
test_suite=suite,
|
||||
task=run_task,
|
||||
experiment_name=experiment_name,
|
||||
verbose=0,
|
||||
model=AGENTIC_JUDGE_MODEL,
|
||||
scoring_tool_strategy="auto",
|
||||
)
|
||||
|
||||
# The assertion is TRUE — the inner step did raise a RuntimeError
|
||||
# with message "internal failure". A judge that stayed at the
|
||||
# overview level would only see `has_error: true` with no type or
|
||||
# message info, so per the "absence of evidence" rule it would
|
||||
# fail the assertion (score=false). A passing verdict means it
|
||||
# drilled into `error_info` to verify both the type and the
|
||||
# message phrase.
|
||||
verifiers.verify_test_suite_result(
|
||||
opik_client=opik_client,
|
||||
suite_result=suite_result,
|
||||
items_total=1,
|
||||
items_passed=1,
|
||||
experiment_items_count=1,
|
||||
total_feedback_scores=1,
|
||||
expected_score_names={span_error_assertion},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not environment.has_openai_api_key(), reason="OPENAI_API_KEY is not set"
|
||||
)
|
||||
def test_test_suite_agentic__assertion_requires_buried_keyword_lookup__passes(
|
||||
opik_client: opik.Opik,
|
||||
dataset_name: str,
|
||||
experiment_name: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
"""The marker is placed past the floor-tier truncation the overview
|
||||
applies to every span's I/O. The overview alone physically cannot
|
||||
include it, so the judge MUST reach for at least one of
|
||||
`read` / `scan` / `search` to find the marker and pass the
|
||||
assertion. We don't pin which tool the model picks — that's a
|
||||
model-quality question. What we prove here is that the agentic loop
|
||||
engages beyond the overview, since a one-shot judge would lack the
|
||||
evidence.
|
||||
|
||||
The marker is a synthetic token (`MARKER-` + opaque tail) chosen so
|
||||
it can't appear anywhere else in the trace or in the model's
|
||||
pretraining. A `score: true` verdict here means the judge actually
|
||||
extracted it from span content.
|
||||
|
||||
NOTE on the ladder monkeypatch: the overview sizer normally picks
|
||||
the largest per-field limit that fits the model's context budget,
|
||||
which on `gpt-4o-mini` (128k window) would render this small trace
|
||||
at the `NO_OVERVIEW_TRUNCATION` tier — the marker would be visible
|
||||
inline and the test premise would silently regress. Forcing the
|
||||
ladder to its single floor entry (`OVERVIEW_IO_FLOOR_CHAR_LIMIT`) keeps
|
||||
the truncation guaranteed without needing to produce a multi-MB
|
||||
trace just to overflow the budget. The sizer's own behavior is
|
||||
covered by `test_overview_sizer.py`.
|
||||
"""
|
||||
from opik.evaluation.suite_evaluators.agentic.compression import (
|
||||
span_tree_serializer,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
span_tree_serializer,
|
||||
"OVERVIEW_IO_LIMIT_LADDER",
|
||||
(span_tree_serializer.OVERVIEW_IO_FLOOR_CHAR_LIMIT,),
|
||||
)
|
||||
|
||||
secret_marker = "MARKER-7f3a8c2e9b1d4f60"
|
||||
keyword_assertion = (
|
||||
f"At least one intermediate step processed a payload containing "
|
||||
f"the literal token '{secret_marker}'"
|
||||
)
|
||||
|
||||
@opik.track(name="process_step", project_name=PROJECT_NAME)
|
||||
def process_step(payload: str) -> str:
|
||||
# The output deliberately doesn't echo the marker — it only
|
||||
# references the payload length, so the agent can't shortcut
|
||||
# through `output` alone. The marker lives only in the input,
|
||||
# which is what the overview truncates at the floor tier.
|
||||
return f"step processed {len(payload)} chars"
|
||||
|
||||
@opik.track(name="noop_step", project_name=PROJECT_NAME)
|
||||
def noop_step() -> str:
|
||||
return "noop"
|
||||
|
||||
@opik.track(name="task", project_name=PROJECT_NAME)
|
||||
def run_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# Pad with filler so the marker lands well past the overview's
|
||||
# OVERVIEW_IO_FLOOR_CHAR_LIMIT (500 chars) in the JSON-rendered span
|
||||
# input `{"payload": "<filler><marker>"}`. `padding ` is 8 chars
|
||||
# × 70 = 560 chars; with the ~13-char JSON prefix the marker
|
||||
# starts past offset 573, so it sits beyond the overview cap by
|
||||
# construction (the ladder is pinned to the floor by the
|
||||
# monkeypatch above). Mixed-in noop_step guarantees more than
|
||||
# one span exists, so the judge can't trivially infer "must be
|
||||
# the only span."
|
||||
filler = "padding " * 170
|
||||
process_step(payload=f"{filler}{secret_marker} trailer")
|
||||
noop_step()
|
||||
return {"input": item["input"], "output": "completed"}
|
||||
|
||||
suite = opik_client.create_test_suite(
|
||||
name=dataset_name,
|
||||
description="Agentic judge — buried-keyword requires read/scan/search",
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
suite.insert(
|
||||
[
|
||||
{
|
||||
"data": {"input": {"question": "process the payload"}},
|
||||
"assertions": [keyword_assertion],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
suite_result = opik.run_tests(
|
||||
test_suite=suite,
|
||||
task=run_task,
|
||||
experiment_name=experiment_name,
|
||||
verbose=0,
|
||||
model=AGENTIC_JUDGE_MODEL,
|
||||
scoring_tool_strategy="auto",
|
||||
)
|
||||
|
||||
# If the judge stayed at the overview level, it would have seen the
|
||||
# truncated string (no marker) and could only have guessed at the
|
||||
# assertion. A passing verdict means it drilled into span content
|
||||
# via one of read / scan / search.
|
||||
verifiers.verify_test_suite_result(
|
||||
opik_client=opik_client,
|
||||
suite_result=suite_result,
|
||||
items_total=1,
|
||||
items_passed=1,
|
||||
experiment_items_count=1,
|
||||
total_feedback_scores=1,
|
||||
expected_score_names={keyword_assertion},
|
||||
project_name=PROJECT_NAME,
|
||||
)
|
||||
@@ -0,0 +1,220 @@
|
||||
import time
|
||||
import uuid
|
||||
import pytest
|
||||
|
||||
import opik
|
||||
from opik import synchronization
|
||||
from opik.evaluation import metrics
|
||||
from opik.evaluation.models import LiteLLMChatModel
|
||||
from opik.evaluation.threads import evaluator
|
||||
from opik.types import FeedbackScoreDict
|
||||
|
||||
from ...testlib import environment
|
||||
from .. import verifiers
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def real_model_conversation():
|
||||
return [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I need to book a flight to New York and find a hotel.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "I can help you with that. For flights to New York, what dates are you looking to travel?",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def active_thread_and_project_name(
|
||||
opik_client, real_model_conversation, temporary_project_name
|
||||
):
|
||||
thread_id = str(uuid.uuid4())[-6:]
|
||||
|
||||
# create conversation traces
|
||||
i = 0
|
||||
while i < len(real_model_conversation) - 1:
|
||||
opik_client.trace(
|
||||
name=f"trace-name-{i}:{thread_id}",
|
||||
input={"input": f"{real_model_conversation[i]['content']}"},
|
||||
output={"output": f"{real_model_conversation[i + 1]['content']}"},
|
||||
project_name=temporary_project_name,
|
||||
thread_id=thread_id,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
i += 2
|
||||
|
||||
opik_client.flush()
|
||||
|
||||
return thread_id, temporary_project_name
|
||||
|
||||
|
||||
def _one_thread_is_active(project_name: str, opik_client: opik.Opik) -> bool:
|
||||
threads = opik_client.search_threads(
|
||||
project_name=project_name, filter_string='status = "active"'
|
||||
)
|
||||
return len(threads) == 1
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def eval_project_name(temporary_project_name: str) -> str:
|
||||
return temporary_project_name
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not environment.has_openai_api_key(), reason="OPENAI_API_KEY is not set"
|
||||
)
|
||||
def test_evaluate_threads__happy_path(
|
||||
opik_client, active_thread_and_project_name, eval_project_name
|
||||
):
|
||||
active_thread, project_name = active_thread_and_project_name
|
||||
# wait for active threads to propagate
|
||||
if not synchronization.until(
|
||||
lambda: _one_thread_is_active(project_name, opik_client), max_try_seconds=30
|
||||
):
|
||||
raise AssertionError(f"Failed to create threads in project '{project_name}'")
|
||||
|
||||
judge_model = LiteLLMChatModel(reasoning_effort="minimal")
|
||||
|
||||
metrics_ = [
|
||||
metrics.ConversationalCoherenceMetric(model=judge_model),
|
||||
]
|
||||
|
||||
result = evaluator.evaluate_threads(
|
||||
project_name=project_name,
|
||||
filter_string=f'id = "{active_thread}"',
|
||||
metrics=metrics_,
|
||||
eval_project_name=eval_project_name,
|
||||
trace_input_transform=lambda x: x["input"],
|
||||
trace_output_transform=lambda x: x["output"],
|
||||
verbose=1,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert len(result.results) == 1 # we have only one thread
|
||||
|
||||
thread_result = result.results[0]
|
||||
assert thread_result.thread_id == active_thread
|
||||
assert len(thread_result.scores) == len(metrics_)
|
||||
|
||||
feedback_scores = [
|
||||
FeedbackScoreDict(
|
||||
id=active_thread,
|
||||
name=score.name,
|
||||
value=score.value,
|
||||
reason=score.reason.strip(),
|
||||
category_name=None,
|
||||
)
|
||||
for score in thread_result.scores
|
||||
if not score.scoring_failed
|
||||
]
|
||||
|
||||
verifiers.verify_thread(
|
||||
opik_client=opik_client,
|
||||
thread_id=active_thread,
|
||||
project_name=project_name,
|
||||
feedback_scores=feedback_scores,
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate_threads__no_truncation_for_long_traces(
|
||||
opik_client, temporary_project_name
|
||||
):
|
||||
"""E2E test verifying that long trace content is not truncated during evaluation.
|
||||
|
||||
The test creates a trace with output exceeding 15,000 characters with a unique marker
|
||||
at the end, then verifies the marker is present in the transform function, proving
|
||||
that truncation did not occur.
|
||||
"""
|
||||
thread_id = str(uuid.uuid4())[-6:]
|
||||
|
||||
# Create a long output that exceeds the truncation threshold (~9935 chars)
|
||||
# with a unique marker at the end that would be lost if truncated
|
||||
marker = "UNIQUE_END_MARKER_XYZ123"
|
||||
long_content = "a" * 15000 + marker
|
||||
|
||||
# Create a trace with very long output
|
||||
opik_client.trace(
|
||||
name=f"long-trace:{thread_id}",
|
||||
input={"input": "test input"},
|
||||
output={"output": long_content},
|
||||
project_name=temporary_project_name,
|
||||
thread_id=thread_id,
|
||||
)
|
||||
|
||||
opik_client.flush()
|
||||
|
||||
# Wait for thread to be created
|
||||
if not synchronization.until(
|
||||
lambda: _one_thread_is_active(temporary_project_name, opik_client),
|
||||
max_try_seconds=30,
|
||||
):
|
||||
raise AssertionError(
|
||||
f"Failed to create thread in project '{temporary_project_name}'"
|
||||
)
|
||||
|
||||
# Track what the transform receives
|
||||
received_outputs = []
|
||||
|
||||
def input_transform(x):
|
||||
return x.get("input", "")
|
||||
|
||||
def output_transform(x):
|
||||
"""Transform that captures the output to verify it's not truncated."""
|
||||
# When truncated, x might be a string (malformed JSON) instead of dict
|
||||
if isinstance(x, str):
|
||||
# This is the bug! Truncation causes malformed JSON string
|
||||
received_outputs.append(f"TRUNCATED_STRING:{x[:100]}...")
|
||||
return "TRUNCATED"
|
||||
output = x.get("output", "")
|
||||
received_outputs.append(output)
|
||||
return output
|
||||
|
||||
# Create a simple metric that just checks the output
|
||||
class ContentVerificationMetric(metrics.base_metric.BaseMetric):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="content_verification",
|
||||
track=False,
|
||||
)
|
||||
|
||||
def score(self, conversation, **ignored_kwargs):
|
||||
# Just return a dummy score - we're really testing the transform
|
||||
return metrics.score_result.ScoreResult(
|
||||
name=self.name,
|
||||
value=1.0,
|
||||
reason="Content verification",
|
||||
)
|
||||
|
||||
# Run evaluation
|
||||
result = evaluator.evaluate_threads(
|
||||
project_name=temporary_project_name,
|
||||
filter_string=f'id = "{thread_id}"',
|
||||
metrics=[ContentVerificationMetric()],
|
||||
eval_project_name=temporary_project_name,
|
||||
trace_input_transform=input_transform,
|
||||
trace_output_transform=output_transform,
|
||||
verbose=0,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert len(result.results) == 1
|
||||
|
||||
# Verify that the transform received the full content with the marker
|
||||
assert len(received_outputs) > 0, "Transform should have been called"
|
||||
transformed_content = received_outputs[0]
|
||||
|
||||
# This is the critical assertion: if truncation occurred, the marker would be missing
|
||||
assert marker in transformed_content, (
|
||||
f"Content was truncated! Expected marker '{marker}' not found. "
|
||||
f"Content length: {len(transformed_content)}, expected: {len(long_content)}. "
|
||||
f"Last 100 chars: {transformed_content[-100:]}"
|
||||
)
|
||||
|
||||
# Also verify the full length is preserved
|
||||
assert len(transformed_content) == len(long_content), (
|
||||
f"Content length mismatch: got {len(transformed_content)}, "
|
||||
f"expected {len(long_content)}"
|
||||
)
|
||||
Reference in New Issue
Block a user