Back to Opik

Agent Config API Demo

sdks/python/examples/agent_config_demo.ipynb

2.0.24-52627.4 KB
Original Source

Agent Config API Demo

This notebook walks through the Opik Agent Config API:

  1. get_or_create_config — fetch from the backend, auto-creating from a fallback when nothing exists yet
  2. get_or_create_config with fallback only (returns fallback when backend is unreachable or empty)
  3. create_config — unconditionally write a new config version
  4. set_config_env — tag a version with an environment name (e.g. "prod", "staging")
  5. Fetching by env and by explicit version name

Setup

python
%pip install opik --quiet
python
import uuid
from typing import Optional

import opik
from opik.api_objects.agent_config.cache import get_global_registry

# Configure once — reads OPIK_API_KEY and OPIK_URL_OVERRIDE from env if set.
# opik.configure(use_local=True)  # swap for opik.configure() when using Opik Cloud
python
client = opik.Opik()

# Give each demo run its own project so configs don't bleed between runs.
PROJECT = f"agent-config-demo-{uuid.uuid4().hex[:8]}"
print(f"Project: {PROJECT}")

Define a typed Config subclass

opik.Config is a Pydantic-based model. Subclass it to declare the fields your agent needs.

python
class AgentConfig(opik.Config):
    temperature: float
    model: str
    system_prompt: Optional[str] = None

1. get_or_create_config — first call auto-creates from fallback

The project has no config yet. get_or_create_config detects this and writes the fallback values as the first version. The backend automatically tags the first version as "prod".

python
FALLBACK_V1 = AgentConfig(
    temperature=0.5,
    model="gpt-3.5-turbo",
    system_prompt="You are a helpful assistant.",
)


# get_or_create_config must be called from inside an @opik.track function.
@opik.track(project_name=PROJECT)
def run_agent(user_message: str):
    cfg = client.get_or_create_config(
        fallback=AgentConfig(
            temperature=0.54,
            model="gpt-3.5aaaa-turbo",
            system_prompt="You are a helpful assistant.",
        ),  # optional, but preferred
        project_name=PROJECT,
    )
    # cfg is an AgentConfig instance because we passed a typed fallback.
    print(f"  is_fallback : {cfg.is_fallback}")
    print(f"  temperature : {cfg.temperature}")
    print(f"  model       : {cfg.model}")
    print(f"  system_prompt: {cfg.system_prompt}")
    return cfg


print("=== First call — auto-creates from fallback ===")
cfg_v1 = run_agent("Hello!")
assert cfg_v1.is_fallback is False, (
    "auto-created config should NOT be marked as fallback"
)

2. get_or_create_config — returns fallback when backend unreachable

If the backend times out or is unreachable and a fallback is provided, get_or_create_config returns the fallback with is_fallback=True instead of raising an error. We simulate this by passing an unreachable URL.

python
# Point a second client at a non-existent host to force a timeout.
unreachable_client = opik.Opik(
    host="http://127.0.0.1:19999",  # nothing listening here
    api_key="demo",
)


@opik.track(project_name=PROJECT)
def run_agent_offline(user_message: str):
    cfg = unreachable_client.get_or_create_config(
        fallback=FALLBACK_V1,
        project_name=PROJECT,
        timeout_in_seconds=2,
    )
    print(f"  is_fallback : {cfg.is_fallback}")
    print(f"  temperature : {cfg.temperature}  (from local fallback)")
    print(f"  model       : {cfg.model}")
    return cfg


print("=== Call against unreachable backend — returns fallback ===")
offline_cfg = run_agent_offline("Hello offline!")
assert offline_cfg.is_fallback is True, (
    "should be marked as fallback when backend is unreachable"
)

3. create_config — write a new version unconditionally

create_config does not require a @opik.track context and always creates a new version. It returns the version name (a string) that you can use later with set_config_env or to fetch by explicit version.

python
v2 = AgentConfig(
    temperature=0.8,
    model="gpt-4o",
    system_prompt="You are an expert assistant. Think step by step.",
)

v2_name = client.create_config(
    v2,
    project_name=PROJECT,
    description="Upgraded to gpt-4o with chain-of-thought prompt",
)

print(f"Created version: {v2_name!r}")
assert isinstance(v2_name, str) and v2_name != ""

4. set_config_env — tag a version with an environment

Right now "prod" still points to the v1 values (auto-tagged by the backend on first write). We promote v2 to "prod" with set_config_env.

python
client.set_config_env(
    project_name=PROJECT,
    version=v2_name,
    env="prod",
)
print(f"Version {v2_name!r} is now tagged as 'prod'")

# Also tag the same version as 'staging' to show multi-env support.
client.set_config_env(
    project_name=PROJECT,
    version=v2_name,
    env="staging",
)
print(f"Version {v2_name!r} is now also tagged as 'staging'")

5. Fetch by env — confirm prod now returns v2 values

python
# Clear cache so we hit the backend, not a locally cached copy.
# get_global_registry().clear()


@opik.track(project_name=PROJECT)
def fetch_prod():
    return client.get_or_create_config(
        fallback=FALLBACK_V1,
        project_name=PROJECT,
        env="prod",
    )


print("=== Fetch env='prod' (should return v2 after set_config_env) ===")
prod_cfg = fetch_prod()
print(f"  temperature : {prod_cfg.temperature}")
print(f"  model       : {prod_cfg.model}")
print(f"  system_prompt: {prod_cfg.system_prompt}")

assert prod_cfg.temperature == 0.8
assert prod_cfg.model == "gpt-4o"

6. Fetch by explicit version name

python
@opik.track(project_name=PROJECT)
def fetch_by_version(version_name: str):
    return client.get_or_create_config(
        fallback=FALLBACK_V1,
        project_name=PROJECT,
        version=version_name,
    )


print(f"=== Fetch by explicit version {v2_name!r} ===")
by_name_cfg = fetch_by_version(v2_name)
print(f"  temperature : {by_name_cfg.temperature}")
print(f"  model       : {by_name_cfg.model}")

assert by_name_cfg.temperature == 0.8
assert by_name_cfg.model == "gpt-4o"

7. Fetch without a fallback (generic Config return type)

Omitting fallback returns a base opik.Config instance. Typed field access still works through attribute lookup, but you lose static type-checking of the subclass.

python
get_global_registry().clear()


@opik.track(project_name=PROJECT)
def fetch_no_fallback():
    # No fallback — returns opik.Config, raises ConfigNotFound if project is empty.
    return client.get_or_create_config(project_name=PROJECT)


print("=== Fetch without fallback ===")
no_fallback_cfg = fetch_no_fallback()
print(f"  type        : {type(no_fallback_cfg).__name__}")
print(f"  is_fallback : {no_fallback_cfg.is_fallback}")
print(f"  temperature : {no_fallback_cfg.temperature}")
print(f"  model       : {no_fallback_cfg.model}")

assert type(no_fallback_cfg) is opik.Config
assert no_fallback_cfg.is_fallback is False
# Values come from the prod version (v2).
assert no_fallback_cfg.temperature == by_name_cfg.temperature
assert no_fallback_cfg.model == by_name_cfg.model

Cleanup

Delete the demo project so it doesn't clutter the workspace.

python
from opik.rest_api import core as rest_api_core

try:
    project_id = client.rest_client.projects.retrieve_project(name=PROJECT).id
    client.rest_client.projects.delete_project_by_id(project_id)
    print(f"Deleted project {PROJECT!r}")
except rest_api_core.ApiError as e:
    print(f"Could not delete project: {e}")