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

559 lines
17 KiB
Python

"""
E2E tests for automatic attachment extraction from trace/span input, output, and metadata.
This module tests the automatic extraction and upload of base64-encoded attachments
embedded in trace and span data when attachment extraction is enabled.
"""
import base64
import pytest
import opik
from opik import id_helpers, datetime_helpers, synchronization
from . import verifiers
from ..testlib import generate_project_name
from ..unit.api_objects.attachment import constants
from .. import testlib
PROJECT_NAME = generate_project_name("e2e", __name__)
@pytest.fixture(autouse=True)
def disable_tests_if_attachment_extraction_not_enabled(opik_client: opik.Opik):
"""Disable tests if attachment extraction is not enabled."""
if not opik_client.config.is_attachment_extraction_active:
pytest.skip("Attachment extraction is not enabled - skipping E2E tests")
@pytest.fixture(autouse=True, scope="module")
def configure_min_size_tests_env():
with testlib.patch_environ({"OPIK_MIN_BASE64_EMBEDDED_ATTACHMENT_SIZE": "30"}):
yield
def _create_base64_url(media_type: str, base64_data: str) -> str:
"""Create a data URL with base64-encoded content."""
return f"data:{media_type};base64,{base64_data}"
def test_extraction__trace_with_end_time__extracts_attachments_from_input(
opik_client: opik.Opik,
):
"""Test that traces with end_time has attachments extracted from the input field."""
trace_id = id_helpers.generate_id()
# Create a trace with end_time and base64-encoded images in input
opik_client.trace(
id=trace_id,
name="test-trace-extraction-input",
project_name=PROJECT_NAME,
input={
"image1": _create_base64_url("image/png", constants.PNG_BASE64),
"image2": _create_base64_url("image/jpeg", constants.JPEG_BASE64),
"text": "regular text field",
},
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
len(base64.b64decode(constants.JPEG_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
def test_extraction__trace_without_end_time__does_not_extract_attachments(
opik_client: opik.Opik,
):
"""Test that traces without end_time does NOT have attachments extracted."""
trace_id = id_helpers.generate_id()
# Create a trace WITHOUT calling end() - no end_time set
opik_client.trace(
id=trace_id,
name="test-trace-no-extraction",
project_name=PROJECT_NAME,
input={
"image": _create_base64_url("image/png", constants.PNG_BASE64),
},
)
# Note: NOT calling trace.end()
opik_client.flush()
# Wait for the trace to arrive at the backend — once queryable, message
# processing is done and attachment-extraction would already have fired.
assert synchronization.until(
lambda: opik_client.get_trace_content(id=trace_id) is not None,
allow_errors=True,
), f"Failed to get trace with id {trace_id}."
# Verify NO attachments were extracted
attachments_client = opik_client.get_attachment_client()
attachment_list = attachments_client.get_attachment_list(
project_name=PROJECT_NAME,
entity_id=trace_id,
entity_type="trace",
)
assert len(attachment_list) == 0, (
f"Expected no attachments, but found {len(attachment_list)}"
)
def test_extraction__trace_with_end_time__extracts_attachments_from_output(
opik_client: opik.Opik,
):
"""Test that traces with end_time has attachments extracted from the output field."""
trace_id = id_helpers.generate_id()
opik_client.trace(
id=trace_id,
name="test-trace-extraction-output",
project_name=PROJECT_NAME,
input={"prompt": "generate an image"},
output={
"result_image": _create_base64_url("image/png", constants.PNG_BASE64),
"result_pdf": _create_base64_url("application/pdf", constants.PDF_BASE64),
},
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
len(base64.b64decode(constants.PDF_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
def test_extraction__trace_with_end_time__extracts_attachments_from_metadata(
opik_client: opik.Opik,
):
"""Test that traces with end_time has attachments extracted from the metadata field."""
trace_id = id_helpers.generate_id()
opik_client.trace(
id=trace_id,
name="test-trace-extraction-metadata",
project_name=PROJECT_NAME,
metadata={
"screenshot": _create_base64_url("image/png", constants.PNG_BASE64),
"version": "1.0",
},
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [len(base64.b64decode(constants.PNG_BASE64))]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
def test_extraction__trace_with_end_time__extracts_from_all_fields(
opik_client: opik.Opik,
):
"""Test extraction from input, output, and metadata simultaneously."""
trace_id = id_helpers.generate_id()
opik_client.trace(
id=trace_id,
name="test-trace-extraction-all-fields",
project_name=PROJECT_NAME,
input={
"input_img": _create_base64_url("image/png", constants.PNG_BASE64),
},
output={
"output_img": _create_base64_url("image/jpeg", constants.JPEG_BASE64),
},
metadata={
"meta_gif": _create_base64_url("image/gif", constants.GIF89_BASE64),
},
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
len(base64.b64decode(constants.JPEG_BASE64)),
len(base64.b64decode(constants.GIF89_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
def test_extraction__span_with_end_time__extracts_attachments(
opik_client: opik.Opik,
):
"""Test that spans with end_time has attachments extracted."""
trace_id = id_helpers.generate_id()
span_id = id_helpers.generate_id()
# Create trace first
trace = opik_client.trace(
id=trace_id,
name="test-trace-for-span-extraction",
project_name=PROJECT_NAME,
)
# Create a span with end_time and attachments
trace.span(
id=span_id,
name="test-span-extraction",
input={
"image": _create_base64_url("image/png", constants.PNG_BASE64),
},
output={
"result": _create_base64_url("image/webp", constants.WEBP_BASE64),
},
metadata={
"meta_gif": _create_base64_url("image/gif", constants.GIF89_BASE64),
},
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
len(base64.b64decode(constants.WEBP_BASE64)),
len(base64.b64decode(constants.GIF89_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="span",
entity_id=span_id,
expected_sizes=expected_sizes,
)
def test_extraction__span_without_end_time__does_not_extract_attachments(
opik_client: opik.Opik,
):
"""Test that spans without end_time does NOT have attachments extracted."""
trace_id = id_helpers.generate_id()
span_id = id_helpers.generate_id()
# Create trace first
trace = opik_client.trace(
id=trace_id,
name="test-trace-for-span-no-extraction",
project_name=PROJECT_NAME,
)
# Create a span WITHOUT an end_time set
trace.span(
id=span_id,
name="test-span-no-extraction",
input={
"image": _create_base64_url("image/png", constants.PNG_BASE64),
},
)
opik_client.flush()
# Wait for the span to arrive at the backend — once queryable, message
# processing is done and attachment-extraction would already have fired.
assert synchronization.until(
lambda: opik_client.get_span_content(id=span_id) is not None,
allow_errors=True,
), f"Failed to get span with id {span_id}."
# Verify NO attachments were extracted
attachments_client = opik_client.get_attachment_client()
attachment_list = attachments_client.get_attachment_list(
project_name=PROJECT_NAME,
entity_id=span_id,
entity_type="span",
)
assert len(attachment_list) == 0, (
f"Expected no attachments, but found {len(attachment_list)}"
)
def test_extraction__trace_update__extracts_attachments(
opik_client: opik.Opik,
):
"""Test that trace updates have attachments extracted (updates are always processed)."""
trace_id = id_helpers.generate_id()
# Create an initial trace without attachments
trace = opik_client.trace(
id=trace_id,
name="test-trace-update-extraction",
project_name=PROJECT_NAME,
input={"prompt": "initial input"},
)
opik_client.flush()
# Update the trace with attachment data
trace.update(
output={
"result_image": _create_base64_url("image/png", constants.PNG_BASE64),
}
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [len(base64.b64decode(constants.PNG_BASE64))]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
def test_extraction__span_update__extracts_attachments(
opik_client: opik.Opik,
):
"""Test that span updates have attachments extracted (updates are always processed)."""
trace_id = id_helpers.generate_id()
span_id = id_helpers.generate_id()
# Create trace
trace = opik_client.trace(
id=trace_id,
name="test-trace-for-span-update",
project_name=PROJECT_NAME,
)
# Create an initial span without attachments
span = trace.span(
id=span_id,
name="test-span-update-extraction",
input={"data": "initial"},
)
opik_client.flush()
# Update the span with attachment data
span.update(
output={
"chart": _create_base64_url("image/svg+xml", constants.SVG_BASE64),
}
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [len(base64.b64decode(constants.SVG_BASE64))]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="span",
entity_id=span_id,
expected_sizes=expected_sizes,
)
def test_extraction__various_file_types__all_extracted(
opik_client: opik.Opik,
):
"""Test extraction of various file types (PNG, JPEG, PDF, GIF, WebP, SVG, JSON)."""
trace_id = id_helpers.generate_id()
opik_client.trace(
id=trace_id,
name="test-trace-various-types",
project_name=PROJECT_NAME,
input={
"png": _create_base64_url("image/png", constants.PNG_BASE64),
"jpeg": _create_base64_url("image/jpeg", constants.JPEG_BASE64),
"pdf": _create_base64_url("application/pdf", constants.PDF_BASE64),
"gif": _create_base64_url("image/gif", constants.GIF89_BASE64),
"webp": _create_base64_url("image/webp", constants.WEBP_BASE64),
"svg": _create_base64_url("image/svg+xml", constants.SVG_BASE64),
"json": _create_base64_url("application/json", constants.JSON_BASE64),
},
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
len(base64.b64decode(constants.JPEG_BASE64)),
len(base64.b64decode(constants.PDF_BASE64)),
len(base64.b64decode(constants.GIF89_BASE64)),
len(base64.b64decode(constants.WEBP_BASE64)),
len(base64.b64decode(constants.SVG_BASE64)),
len(base64.b64decode(constants.JSON_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
def test_extraction__backend_reinjects_extracted_attachments(
opik_client: opik.Opik,
):
"""Test that backend reinjects extracted attachments."""
trace_id = id_helpers.generate_id()
span_id = id_helpers.generate_id()
# Create a trace with end_time and base64-encoded images in input
trace_input = {
"image1": _create_base64_url("image/png", constants.PNG_BASE64),
"text": "regular text field",
}
trace = opik_client.trace(
id=trace_id,
name="test-trace-backend_reinjects_extracted_attachments",
project_name=PROJECT_NAME,
input=trace_input,
end_time=datetime_helpers.local_timestamp(),
)
# Create a span with end_time and attachments
span_input = {
"image": _create_base64_url("image/png", constants.PNG_BASE64),
}
trace.span(
id=span_id,
name="test-span--backend_reinjects_extracted_attachments",
input=span_input,
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
#
# Verify attachments were extracted and uploaded for trace and span
#
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="trace",
entity_id=trace_id,
expected_sizes=expected_sizes,
)
expected_sizes = [
len(base64.b64decode(constants.PNG_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="span",
entity_id=span_id,
expected_sizes=expected_sizes,
)
#
# Verify trace and span returned by backend has extracted attachments injected back into
#
# The SDK consumes the `data:<mime>;base64,` prefix as part of the
# extraction match (OPIK-6608), so the placeholder replaces the whole data
# URI. On reinjection the backend only knows the raw base64 payload —
# round-trip strips the data URI prefix by design.
expected_trace_input = {
"image1": constants.PNG_BASE64,
"text": "regular text field",
}
expected_span_input = {
"image": constants.PNG_BASE64,
}
# Verify trace
verifiers.verify_trace(
opik_client=opik_client,
trace_id=trace.id,
name="test-trace-backend_reinjects_extracted_attachments",
input=expected_trace_input,
project_name=PROJECT_NAME,
)
# Verify span
verifiers.verify_span(
opik_client=opik_client,
span_id=span_id,
parent_span_id=None,
trace_id=trace_id,
name="test-span--backend_reinjects_extracted_attachments",
input=expected_span_input,
project_name=PROJECT_NAME,
)
def test_extraction__input_as_top_level_list(
opik_client: opik.Opik,
):
"""Test that top-level lists are extracted and uploaded as separate attachments."""
trace_id = id_helpers.generate_id()
span_id = id_helpers.generate_id()
# Create trace first
trace = opik_client.trace(
id=trace_id,
name="test-trace-for-attachment-list-extraction",
project_name=PROJECT_NAME,
)
# Create a span with end_time and a top-level list in the input with attachments
data = [
{"image": constants.JPEG_BASE64},
{"pdf": constants.PDF_BASE64},
]
trace.span(
id=span_id,
name="test-span-extraction",
input=data,
end_time=datetime_helpers.local_timestamp(),
)
opik_client.flush()
# Verify attachments were extracted and uploaded
expected_sizes = [
len(base64.b64decode(constants.JPEG_BASE64)),
len(base64.b64decode(constants.PDF_BASE64)),
]
verifiers.verify_auto_extracted_attachments(
opik_client=opik_client,
entity_type="span",
entity_id=span_id,
expected_sizes=expected_sizes,
)