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
297 lines
9.9 KiB
Python
297 lines
9.9 KiB
Python
"""
|
||
Sample: traces carrying an image attachment for the online LLM-as-judge eval (OPIK-6555).
|
||
|
||
Creates two artifacts so you can verify both code paths:
|
||
|
||
1. A standalone single trace (NO thread_id) with an image attachment — exercises the
|
||
trace-level LLM-as-judge attachment routing (the {{trace}} variable + agentic-tools
|
||
switch when a trace has attachments).
|
||
2. A 3-turn conversation thread that mimics a vision-Q&A session, with the image on
|
||
turn 1 — exercises the thread-level path.
|
||
|
||
Pass --single or --thread to create just one of them (default: both).
|
||
|
||
The image is attached so the online LLM-as-judge eval can fetch it via get_attachment
|
||
and score it.
|
||
|
||
Usage:
|
||
pip install opik pillow requests
|
||
OPIK_API_KEY=... OPIK_WORKSPACE=... python thread_with_image_attachment.py
|
||
|
||
# Just the single trace (to verify the latest trace-level change):
|
||
python thread_with_image_attachment.py --single
|
||
|
||
# Or point at a local instance:
|
||
OPIK_URL_OVERRIDE=http://localhost:5173/api python thread_with_image_attachment.py
|
||
"""
|
||
|
||
import datetime
|
||
import os
|
||
import uuid
|
||
|
||
import opik
|
||
from opik import Attachment, id_helpers
|
||
|
||
|
||
def _now() -> datetime.datetime:
|
||
"""UTC now — used to stamp end_time so traces count as 'complete'.
|
||
|
||
Online scoring (OnlineScoringSampler) skips traces with no end_time, treating
|
||
them as partial/in-flight. A one-shot client.trace(...) call does NOT set end_time
|
||
on its own, so we set it explicitly here or the eval rule never fires.
|
||
"""
|
||
return datetime.datetime.now(tz=datetime.timezone.utc)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Configuration
|
||
# ---------------------------------------------------------------------------
|
||
|
||
PROJECT_NAME = os.getenv("OPIK_PROJECT_NAME", "image-attachment-demo")
|
||
|
||
# Use a tiny sample image embedded as bytes so the script is self-contained.
|
||
# If you have a real image on disk, replace this with its path (string).
|
||
SAMPLE_IMAGE_PATH: str | None = os.getenv("IMAGE_PATH", None)
|
||
|
||
|
||
def _make_sample_png_bytes() -> bytes:
|
||
"""Build a minimal 1×1 red PNG in pure Python (no Pillow required)."""
|
||
import struct
|
||
import zlib
|
||
|
||
def chunk(name: bytes, data: bytes) -> bytes:
|
||
c = name + data
|
||
return (
|
||
struct.pack(">I", len(data))
|
||
+ c
|
||
+ struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
|
||
)
|
||
|
||
signature = b"\x89PNG\r\n\x1a\n"
|
||
ihdr = chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0))
|
||
raw_row = b"\x00\xff\x00\x00" # filter byte 0, R=255 G=0 B=0
|
||
idat = chunk(b"IDAT", zlib.compress(raw_row))
|
||
iend = chunk(b"IEND", b"")
|
||
return signature + ihdr + idat + iend
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Build the image attachment
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def build_attachment() -> tuple[Attachment, str]:
|
||
"""Return (Attachment, file_name)."""
|
||
if SAMPLE_IMAGE_PATH and os.path.isfile(SAMPLE_IMAGE_PATH):
|
||
file_name = os.path.basename(SAMPLE_IMAGE_PATH)
|
||
return (
|
||
Attachment(
|
||
data=SAMPLE_IMAGE_PATH,
|
||
file_name=file_name,
|
||
content_type="image/png",
|
||
),
|
||
file_name,
|
||
)
|
||
|
||
# Fall back to the embedded 1×1 PNG
|
||
png_bytes = _make_sample_png_bytes()
|
||
file_name = "sample-image.png"
|
||
return (
|
||
Attachment(
|
||
data=png_bytes,
|
||
file_name=file_name,
|
||
content_type="image/png",
|
||
),
|
||
file_name,
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Create a single standalone trace (no thread)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def create_single_trace(client: opik.Opik) -> str:
|
||
"""Create one standalone trace (no thread_id) carrying the image attachment.
|
||
|
||
This is the artifact for verifying the trace-level LLM-as-judge attachment
|
||
routing: when the trace has attachments and the toggle is on, scoring switches
|
||
to the agentic-tools path so the judge can load the media via get_attachment,
|
||
and the {{trace}} variable lists the attachment.
|
||
"""
|
||
trace_id = id_helpers.generate_id()
|
||
attachment, image_file_name = build_attachment()
|
||
|
||
client.trace(
|
||
id=trace_id,
|
||
name="single-trace-vision-question",
|
||
project_name=PROJECT_NAME,
|
||
end_time=_now(),
|
||
input={
|
||
"role": "user",
|
||
"content": (
|
||
f"I've attached an image ({image_file_name}). "
|
||
"Can you describe what you see and identify any anomalies?"
|
||
),
|
||
},
|
||
output={
|
||
"role": "assistant",
|
||
"content": (
|
||
"I can see an image. It appears to contain a solid red pixel. "
|
||
"I notice it is an extremely small (1×1) image."
|
||
),
|
||
},
|
||
tags=["image", "vision", "single-trace"],
|
||
attachments=[attachment],
|
||
)
|
||
|
||
return trace_id
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Create the thread
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def create_thread(client: opik.Opik) -> str:
|
||
thread_id = str(uuid.uuid4())
|
||
attachment, image_file_name = build_attachment()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Turn 1 — user sends an image and asks a question
|
||
# ------------------------------------------------------------------
|
||
turn1_id = id_helpers.generate_id()
|
||
client.trace(
|
||
id=turn1_id,
|
||
name="turn-1-user-question",
|
||
thread_id=thread_id,
|
||
project_name=PROJECT_NAME,
|
||
end_time=_now(),
|
||
input={
|
||
"role": "user",
|
||
"content": (
|
||
f"I've attached an image ({image_file_name}). "
|
||
"Can you describe what you see and identify any anomalies?"
|
||
),
|
||
},
|
||
output={
|
||
"role": "assistant",
|
||
"content": (
|
||
"I can see an image. It appears to contain a solid red pixel. "
|
||
"I notice it is an extremely small (1×1) image — could you confirm "
|
||
"whether this is intentional or a rendering issue?"
|
||
),
|
||
},
|
||
tags=["image", "vision", "turn-1"],
|
||
attachments=[attachment],
|
||
)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Turn 2 — assistant asks a follow-up (no attachment needed here)
|
||
# ------------------------------------------------------------------
|
||
turn2_id = id_helpers.generate_id()
|
||
client.trace(
|
||
id=turn2_id,
|
||
name="turn-2-clarification",
|
||
thread_id=thread_id,
|
||
project_name=PROJECT_NAME,
|
||
end_time=_now(),
|
||
input={
|
||
"role": "user",
|
||
"content": "It is intentional — it's a test image.",
|
||
},
|
||
output={
|
||
"role": "assistant",
|
||
"content": (
|
||
"Understood. The image is a 1×1 PNG with a single red (#FF0000) pixel. "
|
||
"No anomalies detected. Is there anything specific you'd like me to "
|
||
"analyse about the colour or format?"
|
||
),
|
||
},
|
||
tags=["vision", "turn-2"],
|
||
)
|
||
|
||
# ------------------------------------------------------------------
|
||
# Turn 3 — user wraps up; assistant gives a final summary
|
||
# ------------------------------------------------------------------
|
||
turn3_id = id_helpers.generate_id()
|
||
client.trace(
|
||
id=turn3_id,
|
||
name="turn-3-summary",
|
||
thread_id=thread_id,
|
||
project_name=PROJECT_NAME,
|
||
end_time=_now(),
|
||
input={
|
||
"role": "user",
|
||
"content": "No, that covers it. Thanks!",
|
||
},
|
||
output={
|
||
"role": "assistant",
|
||
"content": (
|
||
"You're welcome! To summarise: the image is a minimal 1×1 PNG containing "
|
||
"a pure red pixel with no anomalies. Let me know if you have more images "
|
||
"to analyse."
|
||
),
|
||
},
|
||
tags=["vision", "turn-3"],
|
||
)
|
||
|
||
return thread_id
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Main
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def main() -> None:
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
group = parser.add_mutually_exclusive_group()
|
||
group.add_argument(
|
||
"--single", action="store_true", help="Create only the standalone single trace."
|
||
)
|
||
group.add_argument(
|
||
"--thread", action="store_true", help="Create only the multi-turn thread."
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
do_single = args.single or not args.thread
|
||
do_thread = args.thread or not args.single
|
||
|
||
client = opik.Opik(project_name=PROJECT_NAME)
|
||
|
||
single_trace_id = None
|
||
thread_id = None
|
||
|
||
if do_single:
|
||
print(f"Creating single trace in project '{PROJECT_NAME}' ...")
|
||
single_trace_id = create_single_trace(client)
|
||
|
||
if do_thread:
|
||
print(f"Creating thread in project '{PROJECT_NAME}' ...")
|
||
thread_id = create_thread(client)
|
||
|
||
client.flush()
|
||
|
||
print(f"\nCreated successfully in project '{PROJECT_NAME}'.")
|
||
if single_trace_id is not None:
|
||
print(f" single trace_id : {single_trace_id}")
|
||
if thread_id is not None:
|
||
print(f" thread_id : {thread_id}")
|
||
print()
|
||
print(
|
||
"To verify the trace-level change, create a TRACE-level LLM-as-judge rule that"
|
||
)
|
||
print("references {{trace}} (map a variable to the bare string 'trace'), using a")
|
||
print("vision-capable, tool-calling model, with the agentic-tools toggle enabled.")
|
||
print(
|
||
"Scoring should switch to the agentic-tools path, read(type=trace) should list"
|
||
)
|
||
print("the attachment, and get_attachment should load it before a score is stored.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|