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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:44 +08:00
commit 5a558eb09e
11579 changed files with 1795921 additions and 0 deletions
@@ -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| 23 | 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| 1112 | 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` (1100) 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
}
+30
View File
@@ -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
+167
View File
@@ -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")],
)
+24
View File
@@ -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()
+215
View File
@@ -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()
+217
View File
@@ -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)