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,515 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7fb27b941602401d91542211134fc71a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Agent Config API Demo\n",
|
||||
"\n",
|
||||
"This notebook walks through the Opik **Agent Config** API:\n",
|
||||
"\n",
|
||||
"1. `get_or_create_config` — fetch from the backend, auto-creating from a fallback when nothing exists yet\n",
|
||||
"2. `get_or_create_config` with `fallback` only (returns fallback when backend is unreachable or empty)\n",
|
||||
"3. `create_config` — unconditionally write a new config version\n",
|
||||
"4. `set_config_env` — tag a version with an environment name (e.g. `\"prod\"`, `\"staging\"`)\n",
|
||||
"5. Fetching by `env` and by explicit `version` name\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "acae54e37e7d407bbb7b55eff062a284",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9a63283cbaf04dbcab1f6479b197f3a8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install opik --quiet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "8dd0d8092fe74a7c96281538738b07e2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"from typing import Optional\n",
|
||||
"\n",
|
||||
"import opik\n",
|
||||
"from opik.api_objects.agent_config.cache import get_global_registry\n",
|
||||
"\n",
|
||||
"# Configure once — reads OPIK_API_KEY and OPIK_URL_OVERRIDE from env if set.\n",
|
||||
"# opik.configure(use_local=True) # swap for opik.configure() when using Opik Cloud"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "72eea5119410473aa328ad9291626812",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Project: agent-config-demo-e9013134\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client = opik.Opik()\n",
|
||||
"\n",
|
||||
"# Give each demo run its own project so configs don't bleed between runs.\n",
|
||||
"PROJECT = f\"agent-config-demo-{uuid.uuid4().hex[:8]}\"\n",
|
||||
"print(f\"Project: {PROJECT}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8edb47106e1a46a883d545849b8ab81b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define a typed Config subclass\n",
|
||||
"\n",
|
||||
"`opik.Config` is a Pydantic-based model. Subclass it to declare the fields your agent needs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "10185d26023b46108eb7d9f57d49d2b3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class AgentConfig(opik.Config):\n",
|
||||
" temperature: float\n",
|
||||
" model: str\n",
|
||||
" system_prompt: Optional[str] = None"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8763a12b2bbd4a93a75aff182afb95dc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 1. `get_or_create_config` — first call auto-creates from fallback\n",
|
||||
"\n",
|
||||
"The project has no config yet. `get_or_create_config` detects this and writes the\n",
|
||||
"fallback values as the first version. The backend automatically tags the first\n",
|
||||
"version as `\"prod\"`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7623eae2785240b9bd12b16a66d81610",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"OPIK: Started logging traces to the \"agent-config-demo-e9013134\" project at https://test.dev.comet.com/opik/api/v1/session/redirect/projects/?trace_id=019d8bf7-c5e8-7314-8849-2b52040092c0&path=aHR0cHM6Ly90ZXN0LmRldi5jb21ldC5jb20vb3Bpay9hcGkv.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== First call — auto-creates from fallback ===\n",
|
||||
" is_fallback : False\n",
|
||||
" temperature : 0.5\n",
|
||||
" model : gpt-3.5-turbo\n",
|
||||
" system_prompt: You are a helpful assistant.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"FALLBACK_V1 = AgentConfig(\n",
|
||||
" temperature=0.5,\n",
|
||||
" model=\"gpt-3.5-turbo\",\n",
|
||||
" system_prompt=\"You are a helpful assistant.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# get_or_create_config must be called from inside an @opik.track function.\n",
|
||||
"@opik.track(project_name=PROJECT)\n",
|
||||
"def run_agent(user_message: str):\n",
|
||||
" cfg = client.get_or_create_config(\n",
|
||||
" fallback=AgentConfig(\n",
|
||||
" temperature=0.54,\n",
|
||||
" model=\"gpt-3.5aaaa-turbo\",\n",
|
||||
" system_prompt=\"You are a helpful assistant.\",\n",
|
||||
" ), # optional, but preferred\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" )\n",
|
||||
" # cfg is an AgentConfig instance because we passed a typed fallback.\n",
|
||||
" print(f\" is_fallback : {cfg.is_fallback}\")\n",
|
||||
" print(f\" temperature : {cfg.temperature}\")\n",
|
||||
" print(f\" model : {cfg.model}\")\n",
|
||||
" print(f\" system_prompt: {cfg.system_prompt}\")\n",
|
||||
" return cfg\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"=== First call — auto-creates from fallback ===\")\n",
|
||||
"cfg_v1 = run_agent(\"Hello!\")\n",
|
||||
"assert cfg_v1.is_fallback is False, (\n",
|
||||
" \"auto-created config should NOT be marked as fallback\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7cdc8c89c7104fffa095e18ddfef8986",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 2. `get_or_create_config` — returns fallback when backend unreachable\n",
|
||||
"\n",
|
||||
"If the backend times out or is unreachable **and** a `fallback` is provided,\n",
|
||||
"`get_or_create_config` returns the fallback with `is_fallback=True` instead of\n",
|
||||
"raising an error. We simulate this by passing an unreachable URL."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "b118ea5561624da68c537baed56e602f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Call against unreachable backend — returns fallback ===\n",
|
||||
" is_fallback : True\n",
|
||||
" temperature : 0.5 (from local fallback)\n",
|
||||
" model : gpt-3.5-turbo\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Point a second client at a non-existent host to force a timeout.\n",
|
||||
"unreachable_client = opik.Opik(\n",
|
||||
" host=\"http://127.0.0.1:19999\", # nothing listening here\n",
|
||||
" api_key=\"demo\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@opik.track(project_name=PROJECT)\n",
|
||||
"def run_agent_offline(user_message: str):\n",
|
||||
" cfg = unreachable_client.get_or_create_config(\n",
|
||||
" fallback=FALLBACK_V1,\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" timeout_in_seconds=2,\n",
|
||||
" )\n",
|
||||
" print(f\" is_fallback : {cfg.is_fallback}\")\n",
|
||||
" print(f\" temperature : {cfg.temperature} (from local fallback)\")\n",
|
||||
" print(f\" model : {cfg.model}\")\n",
|
||||
" return cfg\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"=== Call against unreachable backend — returns fallback ===\")\n",
|
||||
"offline_cfg = run_agent_offline(\"Hello offline!\")\n",
|
||||
"assert offline_cfg.is_fallback is True, (\n",
|
||||
" \"should be marked as fallback when backend is unreachable\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "938c804e27f84196a10c8828c723f798",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 3. `create_config` — write a new version unconditionally\n",
|
||||
"\n",
|
||||
"`create_config` does not require a `@opik.track` context and always creates a new\n",
|
||||
"version. It returns the version name (a string) that you can use later with\n",
|
||||
"`set_config_env` or to fetch by explicit version."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "504fb2a444614c0babb325280ed9130a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Created version: 'v2'\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"v2 = AgentConfig(\n",
|
||||
" temperature=0.8,\n",
|
||||
" model=\"gpt-4o\",\n",
|
||||
" system_prompt=\"You are an expert assistant. Think step by step.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"v2_name = client.create_config(\n",
|
||||
" v2,\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" description=\"Upgraded to gpt-4o with chain-of-thought prompt\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Created version: {v2_name!r}\")\n",
|
||||
"assert isinstance(v2_name, str) and v2_name != \"\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "59bbdb311c014d738909a11f9e486628",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 4. `set_config_env` — tag a version with an environment\n",
|
||||
"\n",
|
||||
"Right now `\"prod\"` still points to the v1 values (auto-tagged by the backend on first\n",
|
||||
"write). We promote v2 to `\"prod\"` with `set_config_env`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "b43b363d81ae4b689946ece5c682cd59",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Version 'v2' is now tagged as 'prod'\n",
|
||||
"Version 'v2' is now also tagged as 'staging'\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"client.set_config_env(\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" version=v2_name,\n",
|
||||
" env=\"prod\",\n",
|
||||
")\n",
|
||||
"print(f\"Version {v2_name!r} is now tagged as 'prod'\")\n",
|
||||
"\n",
|
||||
"# Also tag the same version as 'staging' to show multi-env support.\n",
|
||||
"client.set_config_env(\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" version=v2_name,\n",
|
||||
" env=\"staging\",\n",
|
||||
")\n",
|
||||
"print(f\"Version {v2_name!r} is now also tagged as 'staging'\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8a65eabff63a45729fe45fb5ade58bdc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 5. Fetch by `env` — confirm prod now returns v2 values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "c3933fab20d04ec698c2621248eb3be0",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Fetch env='prod' (should return v2 after set_config_env) ===\n",
|
||||
" temperature : 0.8\n",
|
||||
" model : gpt-4o\n",
|
||||
" system_prompt: You are an expert assistant. Think step by step.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Clear cache so we hit the backend, not a locally cached copy.\n",
|
||||
"# get_global_registry().clear()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@opik.track(project_name=PROJECT)\n",
|
||||
"def fetch_prod():\n",
|
||||
" return client.get_or_create_config(\n",
|
||||
" fallback=FALLBACK_V1,\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" env=\"prod\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"=== Fetch env='prod' (should return v2 after set_config_env) ===\")\n",
|
||||
"prod_cfg = fetch_prod()\n",
|
||||
"print(f\" temperature : {prod_cfg.temperature}\")\n",
|
||||
"print(f\" model : {prod_cfg.model}\")\n",
|
||||
"print(f\" system_prompt: {prod_cfg.system_prompt}\")\n",
|
||||
"\n",
|
||||
"assert prod_cfg.temperature == 0.8\n",
|
||||
"assert prod_cfg.model == \"gpt-4o\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4dd4641cc4064e0191573fe9c69df29b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 6. Fetch by explicit `version` name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "8309879909854d7188b41380fd92a7c3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Fetch by explicit version 'v2' ===\n",
|
||||
" temperature : 0.8\n",
|
||||
" model : gpt-4o\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"@opik.track(project_name=PROJECT)\n",
|
||||
"def fetch_by_version(version_name: str):\n",
|
||||
" return client.get_or_create_config(\n",
|
||||
" fallback=FALLBACK_V1,\n",
|
||||
" project_name=PROJECT,\n",
|
||||
" version=version_name,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(f\"=== Fetch by explicit version {v2_name!r} ===\")\n",
|
||||
"by_name_cfg = fetch_by_version(v2_name)\n",
|
||||
"print(f\" temperature : {by_name_cfg.temperature}\")\n",
|
||||
"print(f\" model : {by_name_cfg.model}\")\n",
|
||||
"\n",
|
||||
"assert by_name_cfg.temperature == 0.8\n",
|
||||
"assert by_name_cfg.model == \"gpt-4o\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3ed186c9a28b402fb0bc4494df01f08d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## 7. Fetch without a fallback (generic `Config` return type)\n",
|
||||
"\n",
|
||||
"Omitting `fallback` returns a base `opik.Config` instance. Typed field access still\n",
|
||||
"works through attribute lookup, but you lose static type-checking of the subclass."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "cb1e1581032b452c9409d6c6813c49d1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"=== Fetch without fallback ===\n",
|
||||
" type : Config\n",
|
||||
" is_fallback : False\n",
|
||||
" temperature : 0.8\n",
|
||||
" model : gpt-4o\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"get_global_registry().clear()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@opik.track(project_name=PROJECT)\n",
|
||||
"def fetch_no_fallback():\n",
|
||||
" # No fallback — returns opik.Config, raises ConfigNotFound if project is empty.\n",
|
||||
" return client.get_or_create_config(project_name=PROJECT)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"=== Fetch without fallback ===\")\n",
|
||||
"no_fallback_cfg = fetch_no_fallback()\n",
|
||||
"print(f\" type : {type(no_fallback_cfg).__name__}\")\n",
|
||||
"print(f\" is_fallback : {no_fallback_cfg.is_fallback}\")\n",
|
||||
"print(f\" temperature : {no_fallback_cfg.temperature}\")\n",
|
||||
"print(f\" model : {no_fallback_cfg.model}\")\n",
|
||||
"\n",
|
||||
"assert type(no_fallback_cfg) is opik.Config\n",
|
||||
"assert no_fallback_cfg.is_fallback is False\n",
|
||||
"# Values come from the prod version (v2).\n",
|
||||
"assert no_fallback_cfg.temperature == by_name_cfg.temperature\n",
|
||||
"assert no_fallback_cfg.model == by_name_cfg.model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "379cbbc1e968416e875cc15c1202d7eb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"## Cleanup\n",
|
||||
"\n",
|
||||
"Delete the demo project so it doesn't clutter the workspace."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "277c27b1587741f2af2001be3712ef0d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from opik.rest_api import core as rest_api_core\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" project_id = client.rest_client.projects.retrieve_project(name=PROJECT).id\n",
|
||||
" client.rest_client.projects.delete_project_by_id(project_id)\n",
|
||||
" print(f\"Deleted project {PROJECT!r}\")\n",
|
||||
"except rest_api_core.ApiError as e:\n",
|
||||
" print(f\"Could not delete project: {e}\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.10.19"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell0",
|
||||
"metadata": {},
|
||||
"source": "# Opik Dashboards — Python SDK\n\nA comprehensive walkthrough of the **dashboard** API on the `opik.Opik` client:\n\n| Step | Topic |\n| --- | --- |\n| 1 | Setup |\n| 2–3 | Create a `MULTI_PROJECT` dashboard scoped to a project |\n| 4 | Stats-card widgets (snapshot metrics) |\n| 5 | Time-series chart widgets |\n| 6 | Markdown / notes widget |\n| 7 | Update widgets |\n| 8 | Inspect and rearrange the grid layout |\n| 9 | Add sections and move widgets between sections |\n| 10 | Remove widgets |\n| 11–12 | Create an `EXPERIMENTS` dashboard with evaluation widgets |\n| 13 | Fetch and list dashboards |\n| 14 | Clean up |\n\n**Project scope** — `project_stats_card` and `project_metrics` widgets are project-scoped.\nPass `project_name` to `create_dashboard` once; the SDK automatically injects the project\ninto every project-scoped widget added via `add_widget`.\n\n**Metric-ID namespaces** — easy to mix up:\n\n| Widget | Field | Namespace | Example |\n| --- | --- | --- | --- |\n| `project_stats_card` | `metric` | lowercase-dotted | `trace_count`, `duration.p50` |\n| `project_metrics` | `metric_type` | ALL-CAPS | `TRACE_COUNT`, `DURATION` |"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1 · Setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install opik --quiet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import copy\n",
|
||||
"\n",
|
||||
"import opik\n",
|
||||
"from opik import dashboard\n",
|
||||
"\n",
|
||||
"client = opik.Opik()\n",
|
||||
"\n",
|
||||
"PROJECT_NAME = \"Default Project\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2 · Create a MULTI_PROJECT dashboard\n",
|
||||
"\n",
|
||||
"`MULTI_PROJECT` dashboards support `project_stats_card`, `project_metrics`, and `text_markdown`\n",
|
||||
"widgets. A new dashboard starts with a single *Overview* section whose `id` we capture for\n",
|
||||
"adding widgets."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"mp_dash = client.create_dashboard(\n",
|
||||
" name=\"SDK comprehensive demo\",\n",
|
||||
" type=dashboard.DashboardType.MULTI_PROJECT,\n",
|
||||
" description=\"Created from the Python SDK walkthrough\",\n",
|
||||
" project_name=PROJECT_NAME,\n",
|
||||
")\n",
|
||||
"mp_section_id = mp_dash.sections[0].id\n",
|
||||
"print(f\"Dashboard id : {mp_dash.id}\")\n",
|
||||
"print(f\"Type : {mp_dash.type}\")\n",
|
||||
"print(f\"Scope : {mp_dash.scope}\")\n",
|
||||
"print(f\"Section id : {mp_section_id}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell6",
|
||||
"metadata": {},
|
||||
"source": "## 3 · Project scope\n\nThe `project_name` passed to `create_dashboard` links the dashboard to a project.\nThe SDK then automatically injects the project into every project-scoped widget\n(`project_stats_card`, `project_metrics`) when you call `add_widget` — you do **not**\nneed to repeat the project in the widget config."
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(f\"Dashboard linked to project: {PROJECT_NAME!r}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4 · Stats-card widgets\n",
|
||||
"\n",
|
||||
"`project_stats_card` shows a **single current-value metric** for a project.\n",
|
||||
"The `metric` field uses the **lowercase-dotted** namespace — see `dashboard.StatsCardMetric`\n",
|
||||
"for the full list (trace counts, duration percentiles, token usage, costs, …).\n",
|
||||
"\n",
|
||||
"`source` selects whether the metric is computed over traces or spans."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Total trace count\n",
|
||||
"sc_trace_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_STATS_CARD,\n",
|
||||
" title=\"Traces\",\n",
|
||||
" config=dashboard.ProjectStatsCardConfig(\n",
|
||||
" source=dashboard.TraceDataType.TRACES,\n",
|
||||
" metric=dashboard.StatsCardMetric.TRACE_COUNT,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Estimated total cost\n",
|
||||
"sc_cost_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_STATS_CARD,\n",
|
||||
" title=\"Total cost\",\n",
|
||||
" config=dashboard.ProjectStatsCardConfig(\n",
|
||||
" source=dashboard.TraceDataType.TRACES,\n",
|
||||
" metric=dashboard.StatsCardMetric.TOTAL_ESTIMATED_COST_SUM,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Median latency (p50)\n",
|
||||
"sc_p50_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_STATS_CARD,\n",
|
||||
" title=\"Latency p50\",\n",
|
||||
" config=dashboard.ProjectStatsCardConfig(\n",
|
||||
" source=dashboard.TraceDataType.TRACES,\n",
|
||||
" metric=dashboard.StatsCardMetric.DURATION_P50,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# LLM span count (source=SPANS to query span-level metrics)\n",
|
||||
"sc_llm_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_STATS_CARD,\n",
|
||||
" title=\"LLM calls\",\n",
|
||||
" config=dashboard.ProjectStatsCardConfig(\n",
|
||||
" source=dashboard.TraceDataType.SPANS,\n",
|
||||
" metric=dashboard.StatsCardMetric.LLM_SPAN_COUNT,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Stats cards added, total widgets: {len(mp_dash.sections[0].widgets)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell10",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5 · Time-series chart widgets\n",
|
||||
"\n",
|
||||
"`project_metrics` renders a time-series for an aggregate metric.\n",
|
||||
"The `metric_type` field uses the **ALL-CAPS** namespace — see `dashboard.ProjectMetricType`.\n",
|
||||
"\n",
|
||||
"Breakdowns split the series by a dimension: `MODEL`, `PROVIDER`, `TAGS`, `NAME`, etc.\n",
|
||||
"Available chart types: `LINE` (default), `BAR`, `RADAR`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell11",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Line chart: duration over time, broken down by model\n",
|
||||
"chart_duration_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_METRICS,\n",
|
||||
" title=\"Duration by model\",\n",
|
||||
" config=dashboard.ProjectMetricsConfig(\n",
|
||||
" metric_type=dashboard.ProjectMetricType.DURATION,\n",
|
||||
" chart_type=dashboard.ChartType.LINE,\n",
|
||||
" breakdown=dashboard.BreakdownConfig(field=dashboard.BreakdownField.MODEL),\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Bar chart: token usage over time\n",
|
||||
"chart_tokens_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_METRICS,\n",
|
||||
" title=\"Token usage\",\n",
|
||||
" config=dashboard.ProjectMetricsConfig(\n",
|
||||
" metric_type=dashboard.ProjectMetricType.TOKEN_USAGE,\n",
|
||||
" chart_type=dashboard.ChartType.BAR,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Line chart: trace count broken down by tag\n",
|
||||
"chart_count_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_METRICS,\n",
|
||||
" title=\"Trace count by tag\",\n",
|
||||
" config=dashboard.ProjectMetricsConfig(\n",
|
||||
" metric_type=dashboard.ProjectMetricType.TRACE_COUNT,\n",
|
||||
" chart_type=dashboard.ChartType.LINE,\n",
|
||||
" breakdown=dashboard.BreakdownConfig(field=dashboard.BreakdownField.TAGS),\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Line chart: estimated cost broken down by provider\n",
|
||||
"chart_cost_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.PROJECT_METRICS,\n",
|
||||
" title=\"Cost by provider\",\n",
|
||||
" config=dashboard.ProjectMetricsConfig(\n",
|
||||
" metric_type=dashboard.ProjectMetricType.COST,\n",
|
||||
" chart_type=dashboard.ChartType.LINE,\n",
|
||||
" breakdown=dashboard.BreakdownConfig(\n",
|
||||
" field=dashboard.BreakdownField.PROVIDER\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Total widgets: {len(mp_dash.sections[0].widgets)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell12",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6 · Markdown / notes widget\n",
|
||||
"\n",
|
||||
"`text_markdown` renders freeform Markdown — useful for section headers, runbook links,\n",
|
||||
"or context notes. It is valid in **both** `MULTI_PROJECT` and `EXPERIMENTS` dashboards.\n",
|
||||
"\n",
|
||||
"Widgets can also be created from a **raw dict**, which is the forward-compatible path\n",
|
||||
"for backend fields not yet modelled in the SDK."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell13",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Typed config\n",
|
||||
"notes_id = mp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.TEXT_MARKDOWN,\n",
|
||||
" title=\"\",\n",
|
||||
" config=dashboard.TextMarkdownConfig(\n",
|
||||
" content=(\n",
|
||||
" \"## Project overview\\n\"\n",
|
||||
" \"This dashboard tracks **Default Project** metrics.\\n\\n\"\n",
|
||||
" \"- Duration p50 / p90\\n\"\n",
|
||||
" \"- Token costs by provider\\n\"\n",
|
||||
" \"- Error rate\"\n",
|
||||
" )\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Raw-dict style: forward-compatible with new backend fields\n",
|
||||
"raw_id = mp_dash.add_widget(\n",
|
||||
" {\n",
|
||||
" \"type\": dashboard.WidgetType.TEXT_MARKDOWN.value,\n",
|
||||
" \"title\": \"Raw dict widget\",\n",
|
||||
" \"config\": {\"content\": \"Built with the `opik` Python SDK.\"},\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Total widgets: {len(mp_dash.sections[0].widgets)}\")\n",
|
||||
"print(f\"Notes id : {notes_id}\")\n",
|
||||
"print(f\"Raw id : {raw_id}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell14",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7 · Update widgets\n",
|
||||
"\n",
|
||||
"`update_widget` patches **only the fields you pass** — omitted kwargs are left unchanged.\n",
|
||||
"Config is **merged**, not replaced, so you can change a single key without restating the\n",
|
||||
"whole config object."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell15",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Change the chart title\n",
|
||||
"mp_dash.update_widget(chart_duration_id, title=\"Duration by model (ms)\")\n",
|
||||
"\n",
|
||||
"# Swap the markdown note content (config merge)\n",
|
||||
"mp_dash.update_widget(\n",
|
||||
" notes_id,\n",
|
||||
" config={\"content\": \"## Project overview (updated)\\nDashboard refreshed via SDK.\"},\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Add a subtitle to the trace-count stats card\n",
|
||||
"mp_dash.update_widget(sc_trace_id, subtitle=\"last 7 days\")\n",
|
||||
"\n",
|
||||
"# Switch the token-usage chart from BAR to LINE\n",
|
||||
"mp_dash.update_widget(\n",
|
||||
" chart_tokens_id,\n",
|
||||
" config=dashboard.ProjectMetricsConfig(\n",
|
||||
" metric_type=dashboard.ProjectMetricType.TOKEN_USAGE,\n",
|
||||
" chart_type=dashboard.ChartType.LINE,\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Rename the dashboard and update its description\n",
|
||||
"mp_dash.rename(\"SDK comprehensive demo (v2)\")\n",
|
||||
"mp_dash.set_description(\"Updated via the Python SDK.\")\n",
|
||||
"print(\"Name:\", mp_dash.name)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell16",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 8 · Inspect and rearrange the grid layout\n",
|
||||
"\n",
|
||||
"The grid is **6 columns wide** with unlimited rows. Each widget has a `DashboardLayoutItem`\n",
|
||||
"with `x` (column), `y` (row), `w` (width in columns), `h` (height in rows).\n",
|
||||
"\n",
|
||||
"`replace_sections` swaps the entire sections list in one call — use it to reposition\n",
|
||||
"widgets, resize them, or reorder sections. All other mutators persist immediately after\n",
|
||||
"each individual call."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell17",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"section = mp_dash.sections[0]\n",
|
||||
"by_id = {w.id: w for w in section.widgets}\n",
|
||||
"\n",
|
||||
"print(f\"{'title':35s} x y w h\")\n",
|
||||
"print(\"-\" * 50)\n",
|
||||
"for li in section.layout:\n",
|
||||
" title = by_id[li.id].title or \"(no title)\"\n",
|
||||
" print(f\"{title:35s} {li.x} {li.y} {li.w} {li.h}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell18",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Rearrange: full-width notes banner at the top (row 0),\n",
|
||||
"# four stats cards side-by-side below (row 2),\n",
|
||||
"# charts below that (rows 4+).\n",
|
||||
"new_section = copy.deepcopy(section)\n",
|
||||
"\n",
|
||||
"stats_ids = [sc_trace_id, sc_cost_id, sc_p50_id, sc_llm_id]\n",
|
||||
"chart_ids = [chart_duration_id, chart_tokens_id, chart_count_id, chart_cost_id]\n",
|
||||
"\n",
|
||||
"for li in new_section.layout:\n",
|
||||
" if li.id == notes_id:\n",
|
||||
" # Full-width banner spanning all 6 columns\n",
|
||||
" li.x, li.y, li.w, li.h = 0, 0, 6, 2\n",
|
||||
" elif li.id == raw_id:\n",
|
||||
" # Small note pinned to the top-right\n",
|
||||
" li.x, li.y, li.w, li.h = 4, 2, 2, 2\n",
|
||||
" elif li.id in stats_ids:\n",
|
||||
" col = stats_ids.index(li.id)\n",
|
||||
" li.x, li.y, li.w, li.h = col, 2, 1, 2\n",
|
||||
" elif li.id in chart_ids:\n",
|
||||
" col = chart_ids.index(li.id)\n",
|
||||
" li.x, li.y, li.w, li.h = (col % 3) * 2, 4 + (col // 3) * 4, 2, 4\n",
|
||||
"\n",
|
||||
"mp_dash.replace_sections([new_section])\n",
|
||||
"\n",
|
||||
"print(\"Layout after rearrangement:\")\n",
|
||||
"section = mp_dash.sections[0]\n",
|
||||
"by_id = {w.id: w for w in section.widgets}\n",
|
||||
"print(f\"{'title':35s} x y w h\")\n",
|
||||
"print(\"-\" * 50)\n",
|
||||
"for li in section.layout:\n",
|
||||
" title = by_id[li.id].title or \"(no title)\"\n",
|
||||
" print(f\"{title:35s} {li.x} {li.y} {li.w} {li.h}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell19",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 9 · Add sections and move widgets\n",
|
||||
"\n",
|
||||
"`add_section` appends a new empty section. \n",
|
||||
"To move widgets between sections use `replace_sections` with the complete new state."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell20",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"analytics_section_id = mp_dash.add_section(\"Analytics\")\n",
|
||||
"print(\"Sections:\", [s.title for s in mp_dash.sections])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell21",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Move the four chart widgets from Overview into the new Analytics section.\n",
|
||||
"new_sections = [copy.deepcopy(s) for s in mp_dash.sections]\n",
|
||||
"overview, analytics = new_sections\n",
|
||||
"\n",
|
||||
"move_ids = {chart_duration_id, chart_tokens_id, chart_count_id, chart_cost_id}\n",
|
||||
"\n",
|
||||
"# Extract chart widgets and their layout entries from Overview\n",
|
||||
"moved_widgets = [w for w in overview.widgets if w.id in move_ids]\n",
|
||||
"moved_layout = [li for li in overview.layout if li.id in move_ids]\n",
|
||||
"\n",
|
||||
"overview.widgets = [w for w in overview.widgets if w.id not in move_ids]\n",
|
||||
"overview.layout = [li for li in overview.layout if li.id not in move_ids]\n",
|
||||
"\n",
|
||||
"# Re-position charts inside Analytics (2-wide, 4-tall, three per row)\n",
|
||||
"for idx, li in enumerate(moved_layout):\n",
|
||||
" li.x, li.y, li.w, li.h = (idx % 3) * 2, (idx // 3) * 4, 2, 4\n",
|
||||
"\n",
|
||||
"analytics.widgets.extend(moved_widgets)\n",
|
||||
"analytics.layout.extend(moved_layout)\n",
|
||||
"\n",
|
||||
"mp_dash.replace_sections(new_sections)\n",
|
||||
"\n",
|
||||
"for s in mp_dash.sections:\n",
|
||||
" print(f\" [{s.title}] {len(s.widgets)} widget(s)\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell22",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 10 · Remove widgets\n",
|
||||
"\n",
|
||||
"`remove_widget` removes a widget and its layout entry from whichever section contains it.\n",
|
||||
"Raises `DashboardValidationError` if the ID is not found."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell23",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Remove the raw-dict markdown widget\n",
|
||||
"mp_dash.remove_widget(raw_id)\n",
|
||||
"\n",
|
||||
"total = sum(len(s.widgets) for s in mp_dash.sections)\n",
|
||||
"print(f\"Widgets after removal: {total}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell24",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 11 · EXPERIMENTS dashboard\n",
|
||||
"\n",
|
||||
"`EXPERIMENTS` dashboards target evaluation results rather than live traces.\n",
|
||||
"Supported widgets: `experiments_feedback_scores`, `experiment_leaderboard`, `text_markdown`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell25",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"exp_dash = client.create_dashboard(\n",
|
||||
" name=\"SDK experiments demo\",\n",
|
||||
" type=dashboard.DashboardType.EXPERIMENTS,\n",
|
||||
" description=\"Evaluation metrics overview\",\n",
|
||||
")\n",
|
||||
"exp_section_id = exp_dash.sections[0].id\n",
|
||||
"print(f\"Experiments dashboard id: {exp_dash.id}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell26",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 12 · Experiments evaluation widgets\n",
|
||||
"\n",
|
||||
"`experiments_feedback_scores` plots feedback score distributions across experiments. \n",
|
||||
"`experiment_leaderboard` shows a ranked table of runs against a chosen metric.\n",
|
||||
"\n",
|
||||
"Pass `max_experiments_count` (1–100) to control how many recent experiments are included."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell27",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Bar chart: feedback scores across the last 10 experiments\n",
|
||||
"fb_bar_id = exp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.EXPERIMENTS_FEEDBACK_SCORES,\n",
|
||||
" title=\"Feedback scores (bar)\",\n",
|
||||
" config=dashboard.ExperimentsFeedbackScoresConfig(\n",
|
||||
" chart_type=dashboard.ChartType.BAR,\n",
|
||||
" max_experiments_count=10,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Radar chart: quality shape across the last 5 experiments\n",
|
||||
"fb_radar_id = exp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.EXPERIMENTS_FEEDBACK_SCORES,\n",
|
||||
" title=\"Feedback scores (radar)\",\n",
|
||||
" config=dashboard.ExperimentsFeedbackScoresConfig(\n",
|
||||
" chart_type=dashboard.ChartType.RADAR,\n",
|
||||
" max_experiments_count=5,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Leaderboard with ranking enabled by a specific feedback-score metric\n",
|
||||
"leaderboard_id = exp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.EXPERIMENT_LEADERBOARD,\n",
|
||||
" title=\"Experiment leaderboard\",\n",
|
||||
" config=dashboard.ExperimentLeaderboardConfig(\n",
|
||||
" enable_ranking=True,\n",
|
||||
" ranking_metric=\"hallucination\", # name of the feedback score to rank by\n",
|
||||
" ranking_direction=True, # True = descending (higher score is better)\n",
|
||||
" selected_columns=[\"dataset_id\", \"created_at\", \"duration.p50\", \"pass_rate\"],\n",
|
||||
" max_rows=20,\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Context note\n",
|
||||
"exp_dash.add_widget(\n",
|
||||
" dashboard.DashboardWidget(\n",
|
||||
" type=dashboard.WidgetType.TEXT_MARKDOWN,\n",
|
||||
" title=\"\",\n",
|
||||
" config=dashboard.TextMarkdownConfig(\n",
|
||||
" content=\"### About\\nTracks evaluation runs ranked by the **hallucination** metric.\"\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Experiments dashboard widgets: {len(exp_dash.sections[0].widgets)}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell28",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 13 · Fetch and list dashboards\n",
|
||||
"\n",
|
||||
"`get_dashboard` retrieves a single dashboard by ID (re-fetches from the backend). \n",
|
||||
"`get_dashboards` pages through all dashboards with an optional name filter."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell29",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Fetch the multi-project dashboard by id\n",
|
||||
"fetched_mp = client.get_dashboard(mp_dash.id)\n",
|
||||
"print(f\"Fetched: {fetched_mp.name!r} ({len(fetched_mp.sections)} section(s))\")\n",
|
||||
"\n",
|
||||
"# List all dashboards whose name contains \"SDK\"\n",
|
||||
"found = client.get_dashboards(name=\"SDK\", max_results=20)\n",
|
||||
"print(f\"\\nDashboards matching 'SDK' ({len(found)} found):\")\n",
|
||||
"for d in found:\n",
|
||||
" print(f\" {d.id[:8]}… {d.type:15s} {d.name!r}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "cell32",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 14 · Clean up"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "cell33",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"mp_dash.delete()\n",
|
||||
"exp_dash.delete()\n",
|
||||
"print(\"Both dashboards deleted.\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.19"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
from opik import track, flush_tracker
|
||||
from opik import opik_context
|
||||
|
||||
|
||||
@track()
|
||||
def f3(x):
|
||||
# creates span3 attached to trace1 with parent span2
|
||||
opik_context.update_current_span(tags=["tag-f3"])
|
||||
print("Done f3")
|
||||
return "f3 output"
|
||||
|
||||
|
||||
@track()
|
||||
def f2(x):
|
||||
# creates span2 attached to trace1 with parent span1
|
||||
f3("f3 input")
|
||||
print("Done f2")
|
||||
return "f2 output"
|
||||
|
||||
|
||||
@track()
|
||||
def f1(x, y, z=1):
|
||||
# creates trace 1 and span 1
|
||||
f2("f2 input")
|
||||
print("Done f1")
|
||||
return "f1 output"
|
||||
|
||||
|
||||
f1("f1 input", 42)
|
||||
flush_tracker()
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,167 @@
|
||||
# Setting up a demo project
|
||||
#
|
||||
# Evaluation traces & spans
|
||||
# We start with evaluation so it shows up at the bottom.
|
||||
# The evaluation is going to be tracked into a separate project from the demo traces.
|
||||
# It was run using a simple context with 3 sentences, and 3 questions asking about it.
|
||||
|
||||
import opik
|
||||
import uuid6
|
||||
from demo_data import evaluation_traces, evaluation_spans, demo_traces, demo_spans
|
||||
|
||||
UUID_MAP = {}
|
||||
|
||||
|
||||
def get_new_uuid(old_id):
|
||||
"""
|
||||
The demo_data has the IDs hardcoded in, to preserve the relationships between the traces and spans.
|
||||
However, we need to generate unique ones before logging them.
|
||||
"""
|
||||
if old_id in UUID_MAP:
|
||||
new_id = UUID_MAP[old_id]
|
||||
else:
|
||||
new_id = str(uuid6.uuid7())
|
||||
UUID_MAP[old_id] = new_id
|
||||
return new_id
|
||||
|
||||
|
||||
def create_demo_data(base_url: str, workspace_name, comet_api_key):
|
||||
client = opik.Opik(
|
||||
project_name="Demo evaluation",
|
||||
workspace=workspace_name,
|
||||
host=base_url,
|
||||
api_key=comet_api_key,
|
||||
batching=True,
|
||||
)
|
||||
|
||||
for trace in sorted(evaluation_traces, key=lambda x: x["start_time"]):
|
||||
new_id = get_new_uuid(trace["id"])
|
||||
trace["id"] = new_id
|
||||
client.trace(**trace)
|
||||
|
||||
for span in sorted(evaluation_spans, key=lambda x: x["start_time"]):
|
||||
new_id = get_new_uuid(span["id"])
|
||||
span["id"] = new_id
|
||||
new_trace_id = get_new_uuid(span["trace_id"])
|
||||
span["trace_id"] = new_trace_id
|
||||
if "parent_span_id" in span:
|
||||
new_parent_span_id = get_new_uuid(span["parent_span_id"])
|
||||
span["parent_span_id"] = new_parent_span_id
|
||||
client.span(**span)
|
||||
|
||||
client.flush()
|
||||
|
||||
# Demo traces and spans
|
||||
# We have a simple chatbot application built using llama-index.
|
||||
# We gave it the content of Opik documentation as context, and then asked it a few questions.
|
||||
|
||||
client = opik.Opik(
|
||||
project_name="Demo chatbot 🤖",
|
||||
workspace=workspace_name,
|
||||
host=base_url,
|
||||
api_key=comet_api_key,
|
||||
batching=True,
|
||||
)
|
||||
|
||||
for trace in sorted(demo_traces, key=lambda x: x["start_time"]):
|
||||
new_id = get_new_uuid(trace["id"])
|
||||
trace["id"] = new_id
|
||||
client.trace(**trace)
|
||||
|
||||
for span in sorted(demo_spans, key=lambda x: x["start_time"]):
|
||||
new_id = get_new_uuid(span["id"])
|
||||
span["id"] = new_id
|
||||
new_trace_id = get_new_uuid(span["trace_id"])
|
||||
span["trace_id"] = new_trace_id
|
||||
if "parent_span_id" in span:
|
||||
new_parent_span_id = get_new_uuid(span["parent_span_id"])
|
||||
span["parent_span_id"] = new_parent_span_id
|
||||
client.span(**span)
|
||||
|
||||
# Prompts
|
||||
# We now create 3 versions of a Q&A prompt. The final version is from llama-index.
|
||||
|
||||
client.create_prompt(
|
||||
name="Q&A Prompt",
|
||||
prompt="""Answer the query using your prior knowledge.
|
||||
Query: {{query_str}}
|
||||
Answer:
|
||||
""",
|
||||
)
|
||||
|
||||
client.create_prompt(
|
||||
name="Q&A Prompt",
|
||||
prompt="""Here is the context information.
|
||||
-----------------
|
||||
{{context_str}}
|
||||
-----------------
|
||||
Answer the query using the given context and not prior knowledge.
|
||||
|
||||
Query: {{query_str}}
|
||||
Answer:
|
||||
""",
|
||||
)
|
||||
|
||||
client.create_prompt(
|
||||
name="Q&A Prompt",
|
||||
prompt="""You are an expert Q&A system that is trusted around the world.
|
||||
Always answer the query using the provided context information, and not prior knowledge.
|
||||
Some rules to follow:
|
||||
1. Never directly reference the given context in your answer.
|
||||
2. Avoid statements like 'Based on the context, ...' or 'The context information ...' or anything along those lines.
|
||||
|
||||
Context information is below.
|
||||
---------------------
|
||||
{{context_str}}
|
||||
---------------------
|
||||
Given the context information and not prior knowledge, answer the query.
|
||||
Query: {{query_str}}
|
||||
Answer:
|
||||
""",
|
||||
)
|
||||
|
||||
# Dataset
|
||||
|
||||
dataset = client.get_or_create_dataset(name="Demo dataset")
|
||||
dataset.insert(
|
||||
[
|
||||
{"input": "What is the best LLM evaluation tool?"},
|
||||
{"input": "What is the easiest way to start with Opik?"},
|
||||
{"input": "Is Opik open source?"},
|
||||
]
|
||||
)
|
||||
|
||||
# In addition to creating the dataset, we also create a mapping from the dataset items to the traces. This will be handy for creating the experiment.
|
||||
|
||||
items = dataset.get_items()
|
||||
dataset_id_map = {item["input"]: item["id"] for item in items}
|
||||
|
||||
# Experiment
|
||||
# The experiment is constructed by joining the traces with the dataset items.
|
||||
|
||||
experiment = client.create_experiment(
|
||||
name="Demo experiment", dataset_name="Demo dataset"
|
||||
)
|
||||
experiment_items = []
|
||||
|
||||
for trace in evaluation_traces:
|
||||
trace_id = trace["id"]
|
||||
dataset_item_id = dataset_id_map.get(trace.get("input", {}).get("input", " "))
|
||||
if dataset_item_id is not None:
|
||||
experiment_items.append(
|
||||
opik.api_objects.experiment.experiment_item.ExperimentItemReferences(
|
||||
dataset_item_id=dataset_item_id, trace_id=trace_id
|
||||
)
|
||||
)
|
||||
|
||||
experiment.insert(experiment_items)
|
||||
|
||||
client.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base_url = "http://localhost:5173/api"
|
||||
workspace_name = None
|
||||
comet_api_key = None
|
||||
|
||||
create_demo_data(base_url, workspace_name, comet_api_key)
|
||||
@@ -0,0 +1,31 @@
|
||||
import threading
|
||||
import time
|
||||
from opik import track, flush_tracker
|
||||
from opik.opik_context import get_distributed_trace_headers
|
||||
|
||||
|
||||
@track()
|
||||
def remote_function(x):
|
||||
time.sleep(0.1)
|
||||
return "output-from-remote-function"
|
||||
|
||||
|
||||
def remote_node(x, opik_headers):
|
||||
remote_function(x, opik_distributed_trace_headers=opik_headers)
|
||||
|
||||
|
||||
@track()
|
||||
def local_function(x):
|
||||
opik_headers = get_distributed_trace_headers()
|
||||
|
||||
t1 = threading.Thread(
|
||||
target=remote_node, args=("remote-function-input", opik_headers)
|
||||
)
|
||||
t1.start()
|
||||
t1.join()
|
||||
|
||||
return "output-from-local-function"
|
||||
|
||||
|
||||
local_function("local-function-input")
|
||||
flush_tracker()
|
||||
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
Dynamic Tracing Control Example
|
||||
|
||||
This example demonstrates how to enable and disable Opik tracing at runtime
|
||||
without modifying your instrumented code or restarting your application.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
import opik
|
||||
from opik.integrations import openai as openai_integration
|
||||
|
||||
|
||||
def simulate_openai_client() -> object:
|
||||
"""Create a mock OpenAI client for demonstration."""
|
||||
|
||||
class MockClient:
|
||||
def __init__(self) -> None:
|
||||
self.chat = type(
|
||||
"Chat",
|
||||
(),
|
||||
{
|
||||
"completions": type(
|
||||
"Completions",
|
||||
(),
|
||||
{"create": lambda self, **kwargs: {"content": "Mock response"}},
|
||||
)()
|
||||
},
|
||||
)()
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return None
|
||||
|
||||
return MockClient()
|
||||
|
||||
|
||||
@opik.track(name="llm_call")
|
||||
def call_llm(prompt: str, user_type: str = "free") -> str:
|
||||
"""Simulate an LLM call with user type information."""
|
||||
client = simulate_openai_client()
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
return f"Response for {user_type} user: {response['content']}"
|
||||
|
||||
|
||||
@opik.track(name="data_processing")
|
||||
def process_data(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Simulate data processing that we want to trace."""
|
||||
result = {"processed": True, "item_count": len(data)}
|
||||
time.sleep(0.01) # Simulate work
|
||||
return result
|
||||
|
||||
|
||||
def measure_performance(func, *args, iterations: int = 100) -> float:
|
||||
"""Measure average execution time of a function."""
|
||||
start_time = time.time()
|
||||
for _ in range(iterations):
|
||||
func(*args)
|
||||
end_time = time.time()
|
||||
return (end_time - start_time) / iterations
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Demonstrate dynamic tracing capabilities."""
|
||||
|
||||
print("=== Opik Dynamic Tracing Demo ===\n")
|
||||
|
||||
# 1. Basic enable/disable functionality
|
||||
print("1. Basic Runtime Control")
|
||||
print("-" * 30)
|
||||
|
||||
print(f"Initial tracing state: {opik.is_tracing_active()}")
|
||||
|
||||
# Disable tracing
|
||||
opik.set_tracing_active(False)
|
||||
print(f"After disabling: {opik.is_tracing_active()}")
|
||||
|
||||
# Call traced function - no traces will be created
|
||||
result = call_llm("Hello world", "free")
|
||||
print(f"Function result (no tracing): {result}")
|
||||
|
||||
# Re-enable tracing
|
||||
opik.set_tracing_active(True)
|
||||
print(f"After enabling: {opik.is_tracing_active()}\n")
|
||||
|
||||
# 2. Conditional tracing based on user type
|
||||
print("2. Conditional Tracing by User Type")
|
||||
print("-" * 40)
|
||||
|
||||
def handle_request(prompt: str, user_type: str) -> str:
|
||||
"""Handle request with conditional tracing."""
|
||||
# Only trace premium users
|
||||
should_trace = user_type == "premium"
|
||||
opik.set_tracing_active(should_trace)
|
||||
|
||||
print(f"Processing {user_type} user request (tracing: {should_trace})")
|
||||
return call_llm(prompt, user_type)
|
||||
|
||||
# Process different user types
|
||||
handle_request("What is AI?", "free")
|
||||
handle_request("Explain quantum computing", "premium")
|
||||
handle_request("Hello", "free")
|
||||
print()
|
||||
|
||||
# 3. Sampling-based tracing
|
||||
print("3. Sampling-Based Tracing (10% of requests)")
|
||||
print("-" * 50)
|
||||
|
||||
import random
|
||||
|
||||
def handle_request_with_sampling(request_id: int) -> Dict[str, Any]:
|
||||
"""Handle request with 10% sampling rate."""
|
||||
should_trace = random.random() < 0.1 # 10% sampling
|
||||
opik.set_tracing_active(should_trace)
|
||||
|
||||
data = {"request_id": request_id, "data": list(range(10))}
|
||||
result = process_data(data)
|
||||
|
||||
if should_trace:
|
||||
print(f"Request {request_id}: TRACED")
|
||||
else:
|
||||
print(f"Request {request_id}: not traced")
|
||||
|
||||
return result
|
||||
|
||||
# Process multiple requests
|
||||
for i in range(10):
|
||||
handle_request_with_sampling(i)
|
||||
print()
|
||||
|
||||
# 4. Performance comparison
|
||||
print("4. Performance Impact Comparison")
|
||||
print("-" * 40)
|
||||
|
||||
test_data = {"items": list(range(100))}
|
||||
|
||||
# Measure with tracing enabled
|
||||
opik.set_tracing_active(True)
|
||||
time_with_tracing = measure_performance(process_data, test_data, iterations=50)
|
||||
|
||||
# Measure with tracing disabled
|
||||
opik.set_tracing_active(False)
|
||||
time_without_tracing = measure_performance(process_data, test_data, iterations=50)
|
||||
|
||||
print(f"Average time with tracing: {time_with_tracing * 1000:.2f}ms")
|
||||
print(f"Average time without tracing: {time_without_tracing * 1000:.2f}ms")
|
||||
|
||||
if time_with_tracing > time_without_tracing:
|
||||
overhead = (
|
||||
(time_with_tracing - time_without_tracing) / time_without_tracing
|
||||
) * 100
|
||||
print(f"Tracing overhead: {overhead:.1f}%")
|
||||
print()
|
||||
|
||||
# 5. Integration tracking control
|
||||
print("5. Integration Tracking Control")
|
||||
print("-" * 40)
|
||||
|
||||
# Simulate tracking an OpenAI client
|
||||
mock_client = simulate_openai_client()
|
||||
|
||||
# Disable tracing before setting up integration
|
||||
opik.set_tracing_active(False)
|
||||
openai_integration.track_openai(mock_client)
|
||||
print(
|
||||
"OpenAI client tracking setup with tracing disabled - no instrumentation applied"
|
||||
)
|
||||
|
||||
# Enable tracing and set up integration
|
||||
opik.set_tracing_active(True)
|
||||
openai_integration.track_openai(mock_client)
|
||||
print("OpenAI client tracking setup with tracing enabled - instrumentation applied")
|
||||
print()
|
||||
|
||||
# 6. Reset to configuration default
|
||||
print("6. Reset to Configuration Default")
|
||||
print("-" * 40)
|
||||
|
||||
# Override runtime setting
|
||||
opik.set_tracing_active(False)
|
||||
print(f"Runtime override active: {opik.is_tracing_active()}")
|
||||
|
||||
# Reset to config default
|
||||
opik.reset_tracing_to_config_default()
|
||||
print(f"After reset to config: {opik.is_tracing_active()}")
|
||||
print("(This will use the value from OPIK_TRACK_DISABLE or config file)")
|
||||
|
||||
print("\n=== Demo Complete ===")
|
||||
print("Key benefits of dynamic tracing:")
|
||||
print("• Zero code changes required")
|
||||
print("• Runtime performance optimization")
|
||||
print("• Flexible sampling strategies")
|
||||
print("• Easy debugging and troubleshooting")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
from opik.evaluation import evaluate_experiment
|
||||
from opik.evaluation.metrics import base_metric, score_result
|
||||
|
||||
|
||||
class MyCustomMetric(base_metric.BaseMetric):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def score(self, **ignored_kwargs):
|
||||
# Add you logic here
|
||||
|
||||
return score_result.ScoreResult(
|
||||
value=10, name=self.name, reason="Optional reason for the score"
|
||||
)
|
||||
|
||||
|
||||
evaluate_experiment(
|
||||
experiment_name="round_trellis_3225",
|
||||
scoring_metrics=[MyCustomMetric(name="custom-metric")],
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
import opik
|
||||
from opik.evaluation import evaluate_prompt
|
||||
|
||||
# Create a dataset that contains the samples you want to evaluate
|
||||
opik_client = opik.Opik()
|
||||
dataset = opik_client.get_or_create_dataset("my_dataset")
|
||||
dataset.insert(
|
||||
[
|
||||
{"question": "Hello, world!", "expected_output": "Hello, world!"},
|
||||
{"question": "What is the capital of France?", "expected_output": "Paris"},
|
||||
]
|
||||
)
|
||||
|
||||
# Run the evaluation
|
||||
evaluate_prompt(
|
||||
dataset=dataset,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Translate the following text to French: {{question}}",
|
||||
},
|
||||
],
|
||||
model="gpt-3.5-turbo",
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from opik.evaluation.metrics import IsJson, Hallucination, score_result
|
||||
from opik.evaluation import evaluate, test_result
|
||||
from opik import Opik, track
|
||||
from opik.integrations.openai import track_openai
|
||||
import openai
|
||||
|
||||
|
||||
# os.environ["OPENAI_ORG_ID"] = "<>"
|
||||
# os.environ["OPENAI_API_KEY"] = "<>"
|
||||
|
||||
openai_client = track_openai(openai.OpenAI())
|
||||
|
||||
is_json = IsJson()
|
||||
hallucination = Hallucination()
|
||||
|
||||
client = Opik()
|
||||
dataset = client.get_or_create_dataset(
|
||||
name="My 42 dataset", description="For storing stuff"
|
||||
)
|
||||
|
||||
json = """
|
||||
[
|
||||
{
|
||||
"Model inputs": {"message": "Greet me!", "context": []}
|
||||
},
|
||||
{
|
||||
"Model inputs": {"message": "Ok, I'm leaving, bye!", "context": []}
|
||||
},
|
||||
{
|
||||
"Model inputs": {"message": "How are you doing?", "context": []}
|
||||
},
|
||||
{
|
||||
"Model inputs": {"message": "Give a json example!", "context": []}
|
||||
},
|
||||
{
|
||||
"Model inputs": {
|
||||
"message": "What is the main currency in european union?",
|
||||
"context": ["Euro is the main european currency. It is used across most EU countries"]
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
dataset.insert_from_json(json_array=json, keys_mapping={"Model inputs": "input"})
|
||||
|
||||
|
||||
@track()
|
||||
def llm_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": item["input"]["message"]}],
|
||||
)
|
||||
|
||||
return {
|
||||
"output": response.choices[0].message.content,
|
||||
"reference": "test",
|
||||
}
|
||||
|
||||
|
||||
def compute_hallucination_stats(
|
||||
test_results: List[test_result.TestResult],
|
||||
) -> List[score_result.ScoreResult]:
|
||||
# Extract scores safely, checking for empty score_results
|
||||
scores = [
|
||||
x.score_results[0].value
|
||||
for x in test_results
|
||||
if x.score_results and len(x.score_results) > 0
|
||||
]
|
||||
|
||||
# Return empty list if no scores available
|
||||
if not scores:
|
||||
return []
|
||||
|
||||
return [
|
||||
score_result.ScoreResult(
|
||||
name="Custom metric",
|
||||
value=max(scores) if len(scores) > 1 else 0.0,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
results = evaluate(
|
||||
experiment_name="My experiment",
|
||||
dataset=dataset,
|
||||
task=llm_task,
|
||||
nb_samples=2,
|
||||
scoring_metrics=[is_json, hallucination],
|
||||
experiment_scoring_functions=[compute_hallucination_stats],
|
||||
)
|
||||
|
||||
print(results)
|
||||
@@ -0,0 +1,27 @@
|
||||
import opik
|
||||
|
||||
client = opik.Opik()
|
||||
|
||||
trace = client.trace(
|
||||
name="trace-name",
|
||||
)
|
||||
span1 = trace.span(name="span-1")
|
||||
span2 = span1.span(name="span-2")
|
||||
|
||||
span2.end()
|
||||
span1.end()
|
||||
trace.end()
|
||||
|
||||
span1.log_feedback_score(name="toxicity", value=0.0, reason="Too many bad words")
|
||||
client.log_spans_feedback_scores(
|
||||
[
|
||||
{"id": span2.id, "name": "toxicity", "value": 0.5},
|
||||
{
|
||||
"id": span2.id,
|
||||
"name": "truthfullness",
|
||||
"value": 1.0,
|
||||
"reason": "some good reason",
|
||||
},
|
||||
]
|
||||
)
|
||||
client.end()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Harbor Integration Example
|
||||
|
||||
Track Harbor benchmark runs with Opik. The integration follows Opik's standard
|
||||
patterns (like CrewAI) and creates hierarchical spans for trial execution:
|
||||
|
||||
Trace: {agent_name}/{trial_name}
|
||||
├── Span: setup_environment
|
||||
├── Span: setup_agent
|
||||
├── Span: execute_agent
|
||||
│ └── [trajectory step spans streamed in real-time]
|
||||
├── Span: run_verification
|
||||
│ └── Span: verify
|
||||
|
||||
Features:
|
||||
- Automatic tracing of Trial.run and all sub-methods
|
||||
- Real-time streaming of trajectory steps during agent execution
|
||||
- Verifier rewards captured as feedback scores
|
||||
- Token usage and cost tracking from trajectory metrics
|
||||
- Automatic dataset and experiment creation for evaluation tracking
|
||||
|
||||
The integration automatically:
|
||||
- Creates an Opik dataset for each Harbor dataset source (e.g., "terminal-bench")
|
||||
- Creates an experiment named `harbor-job-{job_id[:8]}` to group all trial traces
|
||||
- Links each trial's trace to the experiment as an experiment item
|
||||
|
||||
Prerequisites:
|
||||
pip install opik harbor
|
||||
opik configure
|
||||
Docker must be running
|
||||
|
||||
Usage:
|
||||
OPENAI_API_KEY=... python harbor_integration_example.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from harbor.job import Job
|
||||
from harbor.models.job.config import (
|
||||
AgentConfig,
|
||||
JobConfig,
|
||||
EnvironmentConfig,
|
||||
OrchestratorConfig,
|
||||
RegistryDatasetConfig,
|
||||
)
|
||||
from harbor.models.registry import RemoteRegistryInfo
|
||||
|
||||
from opik.integrations.harbor import track_harbor
|
||||
|
||||
|
||||
async def main():
|
||||
# Configure agent - terminus-2 creates trajectory files for detailed tracing
|
||||
# Requires OPENAI_API_KEY environment variable
|
||||
agent = AgentConfig(
|
||||
name="terminus-2",
|
||||
model_name="gpt-4o-mini",
|
||||
override_timeout_sec=30, # 30 second timeout for demo
|
||||
)
|
||||
|
||||
# Configure Terminal-Bench 2.0 dataset from Harbor registry
|
||||
# See all tasks: https://github.com/laude-institute/terminal-bench-2
|
||||
dataset = RegistryDatasetConfig(
|
||||
registry=RemoteRegistryInfo(),
|
||||
name="terminal-bench",
|
||||
version="2.0",
|
||||
task_names=["fix-git", "chess-best-move"],
|
||||
)
|
||||
|
||||
# Create Harbor job with unique timestamp-based name
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
job = Job(
|
||||
JobConfig(
|
||||
job_name=f"opik-terminal-bench-{timestamp}",
|
||||
jobs_dir=Path("./harbor_jobs"),
|
||||
orchestrator=OrchestratorConfig(n_concurrent_trials=1),
|
||||
environment=EnvironmentConfig(delete=True),
|
||||
agents=[agent],
|
||||
datasets=[dataset],
|
||||
)
|
||||
)
|
||||
|
||||
# Enable Opik tracking - patches Trial class methods globally
|
||||
# This follows the same pattern as track_crewai, track_openai, etc.
|
||||
tracked_job = track_harbor(
|
||||
job,
|
||||
project_name="terminal-bench-demo",
|
||||
)
|
||||
|
||||
# Run benchmark - traces are created automatically
|
||||
result = await tracked_job.run()
|
||||
|
||||
print(f"\nCompleted {result.stats.n_trials} trials, {result.stats.n_errors} errors")
|
||||
print("View traces at: https://www.comet.com/opik")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,24 @@
|
||||
from langchain_community.llms import fake
|
||||
from langchain.prompts import PromptTemplate
|
||||
from opik.integrations.langchain.opik_tracer import OpikTracer
|
||||
|
||||
|
||||
# @opik.track(capture_input=False)
|
||||
def f(test_prompts, chain, callback):
|
||||
result = chain.invoke(input=test_prompts, config={"callbacks": [callback]})
|
||||
return result
|
||||
|
||||
|
||||
llm = fake.FakeListLLM(
|
||||
responses=["I'm sorry, I don't think I'm talented enough to write a synopsis"]
|
||||
)
|
||||
template = "Given the title of play, write a synopsys for that. Title: {title}."
|
||||
prompt_template = PromptTemplate(input_variables=["title"], template=template)
|
||||
synopsis_chain = prompt_template | llm
|
||||
callback = OpikTracer(tags=["tag1", "tag2"], metadata={"a": "b"})
|
||||
|
||||
test_prompts = {"title": "Documentary about Bigfoot in Paris"}
|
||||
|
||||
print(f(test_prompts, synopsis_chain, callback))
|
||||
|
||||
callback.flush()
|
||||
@@ -0,0 +1,15 @@
|
||||
import opik
|
||||
import os
|
||||
|
||||
os.environ["OPIK_URL_OVERRIDE"] = "http://localhost:5173/api"
|
||||
client = opik.Opik()
|
||||
|
||||
trace = client.trace(name="trace-1")
|
||||
span1 = trace.span(name="span-1")
|
||||
span2 = span1.span(name="span-2")
|
||||
span2.end()
|
||||
span1.end()
|
||||
|
||||
trace.end()
|
||||
|
||||
client.end()
|
||||
@@ -0,0 +1,215 @@
|
||||
from opik.evaluation import metrics
|
||||
|
||||
# Hallucination metric example
|
||||
if True:
|
||||
print("\n\nHallucination metric example:")
|
||||
|
||||
hallucination_metric = metrics.Hallucination()
|
||||
|
||||
hallucination_score = hallucination_metric.score(
|
||||
input="What is the capital of France?",
|
||||
output="The capital of France is Paris. It is famous for its iconic Eiffel Tower and rich cultural heritage.",
|
||||
)
|
||||
print("hallucination_score:", hallucination_score)
|
||||
|
||||
# G-Eval metric example
|
||||
if True:
|
||||
print("\n\nG-Eval metric example:")
|
||||
|
||||
g_eval_metric = metrics.GEval(
|
||||
task_introduction="You are an expert judge tasked with evaluating the faithfulness of an AI-generated answer to the given context.",
|
||||
evaluation_criteria="The OUTPUT must not introduce new information beyond what's provided in the CONTEXT.",
|
||||
# model="ollama/llama3"
|
||||
)
|
||||
|
||||
g_eval_score = g_eval_metric.score(
|
||||
output=str(
|
||||
{
|
||||
"OUTPUT": "What is the capital of France?",
|
||||
"CONTEXT": [
|
||||
"France is a country in Western Europe. Its capital is Paris, which is known for landmarks like the Eiffel Tower."
|
||||
],
|
||||
}
|
||||
)
|
||||
)
|
||||
print("g_eval_score:", g_eval_score)
|
||||
|
||||
# Moderation metric example
|
||||
if True:
|
||||
print("\n\nModeration metric example:")
|
||||
|
||||
moderation_metric = metrics.Moderation()
|
||||
|
||||
moderation_score = moderation_metric.score(
|
||||
input="What is the capital of France?",
|
||||
output="The capital of France is Paris. It is famous for its iconic Eiffel Tower and rich cultural heritage.",
|
||||
context=[
|
||||
"France is a country in Western Europe. Its capital is Paris, which is known for landmarks like the Eiffel Tower."
|
||||
],
|
||||
)
|
||||
|
||||
print("moderation_score:", moderation_score)
|
||||
|
||||
# Answer Relevance metric example
|
||||
if True:
|
||||
print("\n\nAnswer Relevance metric example:")
|
||||
|
||||
answer_relevance_metric = metrics.AnswerRelevance()
|
||||
answer_relevance_score = answer_relevance_metric.score(
|
||||
input="What is the capital of France?",
|
||||
output="The capital of France is Paris. It is famous for its iconic Eiffel Tower and rich cultural heritage.",
|
||||
context=[
|
||||
"France is a country in Western Europe. Its capital is Paris, which is known for landmarks like the Eiffel Tower."
|
||||
],
|
||||
)
|
||||
print("answer_relevance_score:", answer_relevance_score)
|
||||
|
||||
# ContextPrecision metric example
|
||||
if True:
|
||||
print("\n\nContextPrecision metric example:")
|
||||
|
||||
context_precision_metric = metrics.ContextPrecision()
|
||||
context_precision_score = context_precision_metric.score(
|
||||
input="What is the capital of France?",
|
||||
output="The capital of France is Paris. It is famous for its iconic Eiffel Tower and rich cultural heritage.",
|
||||
expected_output="Paris",
|
||||
context=[
|
||||
"France is a country in Western Europe. Its capital is Paris, which is known for landmarks like the Eiffel Tower."
|
||||
],
|
||||
)
|
||||
print("context_precision_score:", context_precision_score)
|
||||
|
||||
# ContextRecall metric example
|
||||
if True:
|
||||
print("\n\nContextRecall metric example:")
|
||||
|
||||
context_recall_metric = metrics.ContextRecall()
|
||||
context_recall_score = context_recall_metric.score(
|
||||
input="What is the capital of France?",
|
||||
output="The capital of France is Paris. It is famous for its iconic Eiffel Tower and rich cultural heritage.",
|
||||
expected_output="Paris",
|
||||
context=[
|
||||
"France is a country in Western Europe. Its capital is Paris, which is known for landmarks like the Eiffel Tower."
|
||||
],
|
||||
)
|
||||
print("context_recall_score:", context_recall_score)
|
||||
|
||||
|
||||
# Structured Output Compliance metric example
|
||||
if True:
|
||||
print("\n\nStructured Output Compliance metric example:")
|
||||
|
||||
structured_output_metric = metrics.StructuredOutputCompliance()
|
||||
|
||||
structured_output_score = structured_output_metric.score(
|
||||
output='{"name": "Alice", "age": 30}',
|
||||
schema='{"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, "required": ["name", "age"]}',
|
||||
)
|
||||
|
||||
print("structured_output_score:", structured_output_score)
|
||||
|
||||
# TrajectoryAccuracy metric example
|
||||
if True:
|
||||
print("\n\nTrajectoryAccuracy metric example:")
|
||||
|
||||
trajectory_accuracy_metric = metrics.TrajectoryAccuracy()
|
||||
|
||||
# Example 1: High-quality ReAct-style agent trajectory
|
||||
print("Example 1: High-quality trajectory")
|
||||
trajectory_accuracy_score = trajectory_accuracy_metric.score(
|
||||
goal="Find the weather in Paris",
|
||||
trajectory=[
|
||||
{
|
||||
"thought": "I need to search for weather information in Paris",
|
||||
"action": "search_weather(location='Paris')",
|
||||
"observation": "Found weather data for Paris: 22°C, sunny",
|
||||
},
|
||||
{
|
||||
"thought": "I have the weather data, now I should summarize it",
|
||||
"action": "summarize_result()",
|
||||
"observation": "Summary created: The weather in Paris is 22°C and sunny",
|
||||
},
|
||||
],
|
||||
final_result="The weather in Paris is 22°C and sunny",
|
||||
)
|
||||
print("trajectory_accuracy_score:", trajectory_accuracy_score)
|
||||
|
||||
# Example 2: Poor-quality trajectory with wrong actions
|
||||
print("\nExample 2: Poor-quality trajectory")
|
||||
poor_trajectory_score = trajectory_accuracy_metric.score(
|
||||
goal="Find the capital of France",
|
||||
trajectory=[
|
||||
{
|
||||
"thought": "I need to find France's capital",
|
||||
"action": "search('weather in France')", # Wrong action
|
||||
"observation": "Found weather information for various French cities",
|
||||
},
|
||||
{
|
||||
"thought": "This doesn't help, let me try something else",
|
||||
"action": "search('French cuisine')", # Still wrong
|
||||
"observation": "Found information about French food",
|
||||
},
|
||||
],
|
||||
final_result="Paris is the capital of France", # Result doesn't match trajectory
|
||||
)
|
||||
print("poor_trajectory_score:", poor_trajectory_score)
|
||||
|
||||
# SycEval metric example
|
||||
# Progressive Sycophancy
|
||||
if True:
|
||||
print("\n\nSycEval metric example (Progressive Sycophancy):")
|
||||
|
||||
syceval_metric = metrics.SycEval(
|
||||
model="gemini/gemini-1.5-pro",
|
||||
rebuttal_model="gpt-3.5-turbo", # Use a widely available model from a different family than the model being tested to prevent evaluation leakage
|
||||
rebuttal_type="simple",
|
||||
context_mode="in_context",
|
||||
)
|
||||
|
||||
syceval_score = syceval_metric.score(
|
||||
input="What is the square root of 16?",
|
||||
output="5", # Intentionally wrong to test progressive sycophancy
|
||||
ground_truth="4",
|
||||
)
|
||||
print("syceval_score:", syceval_score)
|
||||
print("sycophancy_type:", syceval_score.metadata.get("sycophancy_type"))
|
||||
|
||||
# SycEval metric example 2
|
||||
# Regressive Sycophancy Test
|
||||
if True:
|
||||
print("\n\nSycEval metric example (Regressive Sycophancy Test):")
|
||||
|
||||
syceval_citation_metric = metrics.SycEval(
|
||||
model="gemini/gemini-1.5-pro",
|
||||
rebuttal_model="gpt-3.5-turbo", # widely available model from a different family than the model being tested to prevent evaluation leakage
|
||||
rebuttal_type="citation", # Strong rebuttal type
|
||||
context_mode="preemptive",
|
||||
)
|
||||
|
||||
syceval_citation_score = syceval_citation_metric.score(
|
||||
input="What is the capital of France?",
|
||||
output="Paris", # Correct answer to test regressive sycophancy
|
||||
ground_truth="Paris",
|
||||
)
|
||||
print("syceval_citation_score:", syceval_citation_score)
|
||||
print("sycophancy_type:", syceval_citation_score.metadata.get("sycophancy_type"))
|
||||
|
||||
# SycEval metric example 3
|
||||
# Medical Domain
|
||||
if True:
|
||||
print("\n\nSycEval metric example (Medical Domain):")
|
||||
|
||||
syceval_medical_metric = metrics.SycEval(
|
||||
model="gemini/gemini-1.5-pro",
|
||||
rebuttal_model="gpt-3.5-turbo", # widely available model from a different family than the model being tested to prevent evaluation leakage
|
||||
rebuttal_type="justification",
|
||||
context_mode="in_context",
|
||||
)
|
||||
|
||||
syceval_medical_score = syceval_medical_metric.score(
|
||||
input="Should I take antibiotics for a viral infection?",
|
||||
output="No, antibiotics are not effective against viral infections. They only work against bacterial infections.",
|
||||
ground_truth="No, antibiotics are not effective against viral infections.",
|
||||
)
|
||||
print("syceval_medical_score:", syceval_medical_score)
|
||||
print("sycophancy_type:", syceval_medical_score.metadata.get("sycophancy_type"))
|
||||
@@ -0,0 +1,85 @@
|
||||
from openai import OpenAI
|
||||
from opik import flush_tracker, track
|
||||
from opik.integrations.openai import opik_tracker
|
||||
from pydantic import BaseModel
|
||||
|
||||
# os.environ["OPENAI_ORG_ID"] = "YOUR OPENAI ORG ID"
|
||||
# os.environ["OPENAI_API_KEY"] = "YOUR OPENAI API KEY"
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
client = opik_tracker.track_openai(client)
|
||||
|
||||
|
||||
@track()
|
||||
def f_with_structured_output_openai_call():
|
||||
class CalendarEvent(BaseModel):
|
||||
name: str
|
||||
date: str
|
||||
participants: list[str]
|
||||
|
||||
completion = client.beta.chat.completions.parse(
|
||||
model="gpt-4o-2024-08-06",
|
||||
messages=[
|
||||
{"role": "system", "content": "Extract the event information."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Alice and Bob are going to a science fair on Friday.",
|
||||
},
|
||||
],
|
||||
response_format=CalendarEvent,
|
||||
)
|
||||
|
||||
print(completion)
|
||||
|
||||
|
||||
@track()
|
||||
def f_with_streamed_openai_call():
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Tell a fact"},
|
||||
]
|
||||
|
||||
# will create one more nested span, its output will
|
||||
# be updated once stream generator is exhausted
|
||||
stream = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
max_tokens=10,
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
)
|
||||
|
||||
for item in stream:
|
||||
print(item)
|
||||
|
||||
|
||||
@track()
|
||||
def f_with_usual_chat_completion_call():
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Tell a fact"},
|
||||
]
|
||||
|
||||
# will create one more nested span
|
||||
_ = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
|
||||
f_with_streamed_openai_call() # trace 1
|
||||
f_with_usual_chat_completion_call() # trace 2
|
||||
f_with_structured_output_openai_call() # trace 3
|
||||
|
||||
_ = client.chat.completions.create( # trace 4
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Tell a fact"},
|
||||
],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
flush_tracker()
|
||||
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
Local demo for ``opik.evaluate_resume``.
|
||||
|
||||
Run it (against any backend you have configured for ``opik``):
|
||||
|
||||
python examples/resume_evaluation.py
|
||||
|
||||
What it does, top to bottom:
|
||||
|
||||
1. Creates a small sentiment-classification dataset.
|
||||
2. Runs ``opik.evaluate()`` with a task that intentionally crashes
|
||||
halfway — to simulate a real production interruption (network blip,
|
||||
LLM rate limit, instance restart, ...).
|
||||
3. Catches the crash and reports what got done.
|
||||
4. Calls ``opik.evaluate_resume()`` with the now-healthy task — picks up
|
||||
where the original run left off, runs only the remaining items.
|
||||
5. Prints the final converged state.
|
||||
|
||||
No real LLM calls are made; ``classify_review`` is a deterministic stand-in
|
||||
with a small sleep so the run feels like real work.
|
||||
"""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict
|
||||
|
||||
import opik
|
||||
from opik.evaluation import metrics
|
||||
|
||||
|
||||
DATASET_NAME = "resume-demo-dataset"
|
||||
# ``get_experiments_by_name`` is a case-insensitive substring search, so a
|
||||
# fixed name would also match experiments from prior demo runs (or any
|
||||
# experiment whose name happens to contain "resume-demo-experiment"). Pin
|
||||
# a per-process unique suffix so the lookup in stage 3 picks exactly the
|
||||
# experiment this run just created.
|
||||
EXPERIMENT_NAME = f"resume-demo-experiment-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# (review text, expected sentiment) pairs — drive both the dataset and the
|
||||
# fake classifier. Twenty items so a partial run leaves a meaningful chunk
|
||||
# pending for resume to pick up.
|
||||
REVIEWS = [
|
||||
("I love this product!", "positive"),
|
||||
("Worst experience ever.", "negative"),
|
||||
("It was okay, nothing special.", "neutral"),
|
||||
("Absolutely fantastic, highly recommend!", "positive"),
|
||||
("Total waste of money.", "negative"),
|
||||
("Mediocre at best.", "neutral"),
|
||||
("Amazing quality and great service!", "positive"),
|
||||
("I want a refund.", "negative"),
|
||||
("Pretty good but room for improvement.", "neutral"),
|
||||
("Five stars, no complaints.", "positive"),
|
||||
("Returned it within a week.", "negative"),
|
||||
("Does what it says on the tin.", "neutral"),
|
||||
("Best purchase I've made all year!", "positive"),
|
||||
("Stopped working after two days.", "negative"),
|
||||
("Average product, average price.", "neutral"),
|
||||
("Highly impressed by the build quality.", "positive"),
|
||||
("Customer support was unhelpful.", "negative"),
|
||||
("Acceptable for the price point.", "neutral"),
|
||||
("Genuinely delighted with this.", "positive"),
|
||||
("Misleading description, do not buy.", "negative"),
|
||||
]
|
||||
|
||||
# Item index where the original run will crash (simulates a real outage
|
||||
# part-way through). With 20 items, 12 leaves 8 pending for resume to do.
|
||||
CRASH_ON_INDEX = 12
|
||||
|
||||
|
||||
CRASH_REVIEW_TEXT = REVIEWS[CRASH_ON_INDEX][0]
|
||||
|
||||
|
||||
def make_dataset(opik_client: opik.Opik) -> opik.Dataset:
|
||||
"""Recreate the demo dataset from scratch so the script is idempotent."""
|
||||
try:
|
||||
opik_client.delete_dataset(DATASET_NAME)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
dataset = opik_client.create_dataset(DATASET_NAME)
|
||||
dataset.insert(
|
||||
[
|
||||
{
|
||||
"input": {"review": text},
|
||||
"expected_sentiment": expected,
|
||||
}
|
||||
for text, expected in REVIEWS
|
||||
]
|
||||
)
|
||||
return dataset
|
||||
|
||||
|
||||
def classify_review(review_text: str) -> str:
|
||||
"""Pretend to call an LLM; deterministic lookup against REVIEWS."""
|
||||
time.sleep(0.3)
|
||||
for text, sentiment in REVIEWS:
|
||||
if text == review_text:
|
||||
return sentiment
|
||||
raise ValueError(f"Unknown review: {review_text!r}")
|
||||
|
||||
|
||||
def flaky_task(item):
|
||||
"""Original task: crashes on a specific review to simulate an outage.
|
||||
|
||||
We trigger off the review text (each review is unique) rather than the
|
||||
dataset item id — ``id`` is reserved on dataset items, so we let the
|
||||
framework generate ids and key the crash off content instead.
|
||||
"""
|
||||
if item["input"]["review"] == CRASH_REVIEW_TEXT:
|
||||
raise RuntimeError(
|
||||
f"Simulated outage processing {item['input']['review']!r} "
|
||||
"(imagine an LLM rate limit or a network blip)"
|
||||
)
|
||||
return {"output": classify_review(item["input"]["review"])}
|
||||
|
||||
|
||||
def healthy_task(item):
|
||||
"""Same as ``flaky_task`` but with the simulated bug fixed."""
|
||||
return {"output": classify_review(item["input"]["review"])}
|
||||
|
||||
|
||||
def completed_count(experiment) -> int:
|
||||
"""Number of experiment items with at least one successful run."""
|
||||
return sum(
|
||||
1 for item in experiment.get_items() if item.evaluation_task_output is not None
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
opik_client = opik.Opik()
|
||||
|
||||
# ----- 1. Setup ------------------------------------------------------
|
||||
print("=" * 60)
|
||||
print("STAGE 1 — building the dataset")
|
||||
print("=" * 60)
|
||||
dataset = make_dataset(opik_client)
|
||||
print(f"Created dataset '{DATASET_NAME}' with {len(REVIEWS)} items")
|
||||
|
||||
# ----- 2. Initial evaluation (crashes mid-way) -----------------------
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("STAGE 2 — running evaluate() with a flaky task")
|
||||
print("=" * 60)
|
||||
print(f"Task will crash on review #{CRASH_ON_INDEX}: {CRASH_REVIEW_TEXT!r} ...")
|
||||
try:
|
||||
opik.evaluate(
|
||||
dataset=dataset,
|
||||
task=flaky_task,
|
||||
scoring_metrics=[metrics.Equals()],
|
||||
scoring_key_mapping={"reference": "expected_sentiment"},
|
||||
experiment_name=EXPERIMENT_NAME,
|
||||
task_threads=1,
|
||||
verbose=0,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(f"Evaluation interrupted (as expected): {exc}")
|
||||
|
||||
# ----- 3. Inspect the partial state ----------------------------------
|
||||
# ``_evaluate_task`` re-raises the task exception before reaching its
|
||||
# own ``client.flush()``; experiment items / traces produced before
|
||||
# the crash may still be queued. Flush so the inspection below sees
|
||||
# the converged state rather than an under-count.
|
||||
opik_client.flush()
|
||||
|
||||
experiments = opik_client.get_experiments_by_name(EXPERIMENT_NAME)
|
||||
assert len(experiments) == 1, (
|
||||
f"Expected exactly one experiment named {EXPERIMENT_NAME!r}; "
|
||||
f"got {len(experiments)} — the unique suffix collided or a prior "
|
||||
"run left stale state."
|
||||
)
|
||||
experiment_id = experiments[0].id
|
||||
experiment = opik_client.get_experiment_by_id(experiment_id)
|
||||
|
||||
print()
|
||||
print(f"Experiment id : {experiment_id}")
|
||||
print(f"Completed so far : {completed_count(experiment)}/{len(REVIEWS)} items")
|
||||
|
||||
# ----- 4. Resume -----------------------------------------------------
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("STAGE 3 — calling evaluate_resume() with the healthy task")
|
||||
print("=" * 60)
|
||||
resume_result = opik.evaluate_resume(
|
||||
experiment_id=experiment_id,
|
||||
task=healthy_task,
|
||||
scoring_metrics=[metrics.Equals()],
|
||||
scoring_key_mapping={"reference": "expected_sentiment"},
|
||||
verbose=0,
|
||||
)
|
||||
|
||||
# ``resume_result.test_results`` is the FULL experiment after resume:
|
||||
# previously-completed items reconstructed from their stored scores +
|
||||
# items freshly executed by this resume call.
|
||||
print(
|
||||
f"Resume returned {len(resume_result.test_results)} test results "
|
||||
f"(reconstructed previous + freshly executed)."
|
||||
)
|
||||
score_counts: Dict[str, int] = {}
|
||||
for test_result in resume_result.test_results:
|
||||
score_value = test_result.score_results[0].value
|
||||
bucket = "1.0" if score_value == 1.0 else f"{score_value}"
|
||||
score_counts[bucket] = score_counts.get(bucket, 0) + 1
|
||||
for bucket, count in sorted(score_counts.items()):
|
||||
print(f" equals_metric={bucket}: {count} items")
|
||||
|
||||
# ----- 5. Verify convergence -----------------------------------------
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("STAGE 4 — final state")
|
||||
print("=" * 60)
|
||||
experiment = opik_client.get_experiment_by_id(experiment_id)
|
||||
print(f"Completed now : {completed_count(experiment)}/{len(REVIEWS)} items")
|
||||
print(f"Experiment URL: {resume_result.experiment_url}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,10 @@
|
||||
import opik
|
||||
|
||||
opik_client = opik.Opik()
|
||||
|
||||
spans = opik_client.search_spans(
|
||||
project_name="Demo Project",
|
||||
filter_string='input contains "How many unique albums"',
|
||||
)
|
||||
|
||||
print(spans)
|
||||
@@ -0,0 +1,296 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,37 @@
|
||||
import threading
|
||||
from opik import track, flush_tracker
|
||||
from opik import opik_context
|
||||
|
||||
|
||||
@track()
|
||||
def f3(x, thread_name):
|
||||
# creates trace1 with span2_1 in thread 1
|
||||
# creates trace2 with span2_2 in thread 2
|
||||
print(f"Done f3 from {thread_name}")
|
||||
opik_context.update_current_span(tags=[f"f3-thread-{thread_name}"])
|
||||
return f"f3 output from {thread_name}"
|
||||
|
||||
|
||||
@track()
|
||||
def f2(x):
|
||||
# creates span 1 attached to trace 0 and parent span0
|
||||
t1 = threading.Thread(target=f3, args=("f3-input-1", "thread-1"))
|
||||
t2 = threading.Thread(target=f3, args=("f3-input-2", "thread-2"))
|
||||
t1.start()
|
||||
t1.join()
|
||||
t2.start()
|
||||
t2.join()
|
||||
print("Done f2")
|
||||
return "f2 output"
|
||||
|
||||
|
||||
@track()
|
||||
def f1(x):
|
||||
# creates trace 0 with span 0
|
||||
f2("f2 input")
|
||||
print("Done f1")
|
||||
return "f1 output"
|
||||
|
||||
|
||||
f1("f1 input")
|
||||
flush_tracker()
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trajectory Accuracy Evaluation Example
|
||||
|
||||
This example demonstrates how to use Opik's TrajectoryAccuracy metric
|
||||
to evaluate ReAct-style agent trajectories within the evaluation framework.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from opik.evaluation.metrics import TrajectoryAccuracy
|
||||
from opik.evaluation import evaluate
|
||||
from opik import Opik, track
|
||||
import json
|
||||
|
||||
|
||||
def create_trajectory_dataset():
|
||||
"""Create a dataset with ReAct-style trajectories for evaluation."""
|
||||
|
||||
client = Opik()
|
||||
dataset = client.get_or_create_dataset(
|
||||
name="trajectory_evaluation_dataset",
|
||||
description="Dataset for evaluating ReAct-style agent trajectories",
|
||||
)
|
||||
|
||||
# Sample trajectory data
|
||||
trajectory_data = [
|
||||
{
|
||||
"trajectory_input": {
|
||||
"goal": "Find the weather in Paris",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to search for weather information in Paris",
|
||||
"action": "search_weather(location='Paris')",
|
||||
"observation": "Found weather data for Paris: 22°C, sunny",
|
||||
},
|
||||
{
|
||||
"thought": "I have the weather data, now I should summarize it",
|
||||
"action": "summarize_result()",
|
||||
"observation": "Summary created: The weather in Paris is 22°C and sunny",
|
||||
},
|
||||
],
|
||||
"final_result": "The weather in Paris is 22°C and sunny",
|
||||
}
|
||||
},
|
||||
{
|
||||
"trajectory_input": {
|
||||
"goal": "Calculate the sum of 15 and 27",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to add 15 and 27 together",
|
||||
"action": "calculate(15 + 27)",
|
||||
"observation": "Result: 42",
|
||||
}
|
||||
],
|
||||
"final_result": "The sum of 15 and 27 is 42",
|
||||
}
|
||||
},
|
||||
{
|
||||
"trajectory_input": {
|
||||
"goal": "Find the capital of France",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to find France's capital",
|
||||
"action": "search('weather in France')", # Poor action choice
|
||||
"observation": "Found weather information for various French cities",
|
||||
},
|
||||
{
|
||||
"thought": "This doesn't help, let me try something else",
|
||||
"action": "search('French cuisine')", # Still poor choice
|
||||
"observation": "Found information about French food",
|
||||
},
|
||||
],
|
||||
"final_result": "Paris is the capital of France", # Result doesn't match trajectory
|
||||
}
|
||||
},
|
||||
{
|
||||
"trajectory_input": {
|
||||
"goal": "Research the population of Tokyo",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to search for Tokyo population data",
|
||||
"action": "search('Tokyo population 2024')",
|
||||
"observation": "Tokyo has approximately 14 million people in the city, 37 million in metro area",
|
||||
},
|
||||
{
|
||||
"thought": "I found the data, let me verify with another source",
|
||||
"action": "search('Tokyo metropolitan area population')",
|
||||
"observation": "Confirmed: Tokyo metro area has about 37-38 million residents",
|
||||
},
|
||||
{
|
||||
"thought": "Now I should summarize this information clearly",
|
||||
"action": "summarize_findings()",
|
||||
"observation": "Summary prepared with population figures",
|
||||
},
|
||||
],
|
||||
"final_result": "Tokyo city has about 14 million people, while the greater Tokyo metropolitan area has approximately 37-38 million residents, making it the world's largest urban agglomeration.",
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
# Insert data into dataset
|
||||
dataset.insert_from_json(
|
||||
json_array=json.dumps(trajectory_data),
|
||||
keys_mapping={"trajectory_input": "input"},
|
||||
)
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
@track()
|
||||
def trajectory_evaluation_task(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Task that simulates evaluating an agent trajectory.
|
||||
In practice, this would be where your agent generates the trajectory.
|
||||
"""
|
||||
# Extract the trajectory components
|
||||
trajectory_data = item["input"]
|
||||
|
||||
# For this example, we're just passing through the pre-made trajectory
|
||||
# In a real scenario, this is where your agent would generate the trajectory
|
||||
return {
|
||||
"goal": trajectory_data["goal"],
|
||||
"trajectory": trajectory_data["trajectory"],
|
||||
"final_result": trajectory_data["final_result"],
|
||||
"metadata": {
|
||||
"trajectory_steps": len(trajectory_data["trajectory"]),
|
||||
"evaluation_type": "react_agent_trajectory",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the trajectory accuracy evaluation example."""
|
||||
|
||||
print("🚀 Starting Trajectory Accuracy Evaluation with Opik")
|
||||
print("=" * 60)
|
||||
|
||||
# Create dataset
|
||||
print("📊 Creating trajectory dataset...")
|
||||
dataset = create_trajectory_dataset()
|
||||
print(f"✅ Dataset '{dataset.name}' created with trajectory examples")
|
||||
|
||||
# Create trajectory accuracy metric
|
||||
trajectory_metric = TrajectoryAccuracy(
|
||||
name="trajectory_accuracy_evaluation", track=True
|
||||
)
|
||||
|
||||
print("\n🎯 Running evaluation...")
|
||||
|
||||
# Run evaluation
|
||||
evaluation_result = evaluate(
|
||||
experiment_name="trajectory_accuracy_experiment",
|
||||
dataset=dataset,
|
||||
task=trajectory_evaluation_task,
|
||||
scoring_metrics=[trajectory_metric],
|
||||
experiment_config={
|
||||
"model": "gpt-4o-mini", # Following user rules
|
||||
"evaluation_type": "react_agent_trajectory",
|
||||
"metric": "trajectory_accuracy",
|
||||
},
|
||||
)
|
||||
|
||||
print("\n✅ Evaluation completed!")
|
||||
print(f"📊 Experiment: {evaluation_result.experiment_name}")
|
||||
print("📈 Results available in Opik dashboard")
|
||||
|
||||
# Display summary
|
||||
print("\n📋 Summary:")
|
||||
print(f" Total test cases: {len(evaluation_result.test_results)}")
|
||||
print(" Metric used: TrajectoryAccuracy")
|
||||
print(
|
||||
" Evaluation assesses: reasoning quality, action appropriateness, goal achievement"
|
||||
)
|
||||
|
||||
return evaluation_result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
result = main()
|
||||
print("\n🎉 Trajectory Accuracy evaluation completed successfully!")
|
||||
print("📊 View detailed results in your Opik dashboard")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Evaluation failed: {e}")
|
||||
print("💡 Make sure you have:")
|
||||
print(" - OPENAI_API_KEY set in environment")
|
||||
print(" - Opik properly configured")
|
||||
print(" - Network connectivity for LLM calls")
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example script for the TrajectoryAccuracy metric.
|
||||
|
||||
This script demonstrates how to use the TrajectoryAccuracy metric
|
||||
with sample ReAct-style agent trajectories.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from opik.evaluation.metrics import TrajectoryAccuracy
|
||||
|
||||
# Add the parent directory to the Python path to ensure the 'opik' module can be found.
|
||||
sys.path.insert(
|
||||
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
)
|
||||
|
||||
|
||||
def run_basic_example(metric: TrajectoryAccuracy):
|
||||
"""Demonstrates the TrajectoryAccuracy metric with a basic example."""
|
||||
print("Running TrajectoryAccuracy with a basic example...")
|
||||
print("=" * 60)
|
||||
|
||||
example = {
|
||||
"goal": "Find the weather in Paris",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to search for weather information in Paris",
|
||||
"action": "search_weather(location='Paris')",
|
||||
"observation": "Found weather data for Paris: 22°C, sunny",
|
||||
},
|
||||
{
|
||||
"thought": "I found the weather, now summarizing",
|
||||
"action": "summarize_weather()",
|
||||
"observation": "The weather in Paris is 22°C and sunny",
|
||||
},
|
||||
],
|
||||
"final_result": "The weather in Paris is 22°C and sunny",
|
||||
}
|
||||
|
||||
try:
|
||||
result = metric.score(**example)
|
||||
|
||||
print("INPUT:")
|
||||
print(f"Goal: {example['goal']}")
|
||||
print(f"Number of trajectory steps: {len(example['trajectory'])}")
|
||||
print(f"Final result: {example['final_result']}")
|
||||
print()
|
||||
|
||||
print("OUTPUT:")
|
||||
print(f"Score: {result.value}")
|
||||
print(f"Explanation: {result.reason}")
|
||||
print()
|
||||
|
||||
# Validate result format
|
||||
assert isinstance(result.value, float), "Score should be a float"
|
||||
assert 0.0 <= result.value <= 1.0, (
|
||||
f"Score {result.value} should be between 0.0 and 1.0"
|
||||
)
|
||||
assert isinstance(result.reason, str), "Explanation should be a string"
|
||||
assert len(result.reason) > 0, "Explanation should not be empty"
|
||||
|
||||
print("✅ Example completed successfully!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Example failed with error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def run_edge_cases_example(metric: TrajectoryAccuracy):
|
||||
"""Demonstrates the TrajectoryAccuracy metric with various edge cases."""
|
||||
print("\nRunning edge cases...")
|
||||
print("=" * 60)
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "Empty trajectory",
|
||||
"example": {
|
||||
"goal": "Do something",
|
||||
"trajectory": [],
|
||||
"final_result": "Nothing was done",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "Missing goal",
|
||||
"example": {
|
||||
"goal": "",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to do something",
|
||||
"action": "do_action()",
|
||||
"observation": "Action completed",
|
||||
}
|
||||
],
|
||||
"final_result": "Task completed",
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "Incomplete trajectory step",
|
||||
"example": {
|
||||
"goal": "Find information",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to search",
|
||||
}
|
||||
],
|
||||
"final_result": "Search completed",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
passed_count = 0
|
||||
for case in test_cases:
|
||||
print(f"\nRunning case: {case['name']}")
|
||||
try:
|
||||
result = metric.score(**case["example"])
|
||||
print(f" Score: {result.value}")
|
||||
print(f" Explanation: {result.reason[:100]}...")
|
||||
|
||||
# Basic validation
|
||||
assert isinstance(result.value, float)
|
||||
assert 0.0 <= result.value <= 1.0
|
||||
assert isinstance(result.reason, str)
|
||||
|
||||
print(" ✅ Passed")
|
||||
passed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Failed: {e}")
|
||||
|
||||
print(f"\nEdge case examples: {passed_count}/{len(test_cases)} completed")
|
||||
return passed_count == len(test_cases)
|
||||
|
||||
|
||||
def run_complex_trajectory_example(metric: TrajectoryAccuracy):
|
||||
"""Demonstrates the metric with a more complex multi-step trajectory."""
|
||||
print("\nRunning complex trajectory example...")
|
||||
print("=" * 60)
|
||||
|
||||
example = {
|
||||
"goal": "Research and summarize the population of the top 3 largest cities in France",
|
||||
"trajectory": [
|
||||
{
|
||||
"thought": "I need to find information about the largest cities in France first",
|
||||
"action": "search(query='largest cities in France')",
|
||||
"observation": "Found that Paris, Marseille, and Lyon are the top 3 largest cities",
|
||||
},
|
||||
{
|
||||
"thought": "Now I need to get population data for Paris",
|
||||
"action": "search(query='Paris France population 2024')",
|
||||
"observation": "Paris population is approximately 2.16 million",
|
||||
},
|
||||
{
|
||||
"thought": "Next, I need population data for Marseille",
|
||||
"action": "search(query='Marseille France population 2024')",
|
||||
"observation": "Marseille population is approximately 870,000",
|
||||
},
|
||||
{
|
||||
"thought": "Finally, I need population data for Lyon",
|
||||
"action": "search(query='Lyon France population 2024')",
|
||||
"observation": "Lyon population is approximately 520,000",
|
||||
},
|
||||
{
|
||||
"thought": "Now I have all the data, I should summarize it",
|
||||
"action": "summarize(data='Paris: 2.16M, Marseille: 870K, Lyon: 520K')",
|
||||
"observation": "Summary created with population data for top 3 French cities",
|
||||
},
|
||||
],
|
||||
"final_result": "The top 3 largest cities in France by population are: 1) Paris (2.16 million), 2) Marseille (870,000), 3) Lyon (520,000)",
|
||||
}
|
||||
|
||||
try:
|
||||
result = metric.score(**example)
|
||||
|
||||
print("COMPLEX TRAJECTORY EXAMPLE:")
|
||||
print(f"Goal: {example['goal']}")
|
||||
print(f"Steps: {len(example['trajectory'])}")
|
||||
print(f"Score: {result.value}")
|
||||
print(f"Explanation: {result.reason}")
|
||||
|
||||
assert isinstance(result.value, float)
|
||||
assert 0.0 <= result.value <= 1.0
|
||||
assert isinstance(result.reason, str)
|
||||
|
||||
print("✅ Complex trajectory example completed!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Complex trajectory example failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Trajectory Accuracy Metric Example Suite")
|
||||
print("=" * 60)
|
||||
|
||||
# Instantiate the metric
|
||||
trajectory_metric = TrajectoryAccuracy()
|
||||
|
||||
# Run all examples
|
||||
success_count = 0
|
||||
total_examples = 3
|
||||
|
||||
if run_basic_example(trajectory_metric):
|
||||
success_count += 1
|
||||
|
||||
if run_edge_cases_example(trajectory_metric):
|
||||
success_count += 1
|
||||
|
||||
if run_complex_trajectory_example(trajectory_metric):
|
||||
success_count += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"FINAL RESULTS: {success_count}/{total_examples} example suites ran")
|
||||
|
||||
if success_count == total_examples:
|
||||
print("🎉 All examples ran successfully!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("⚠️ Some examples failed. Please check the output above.")
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user