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,587 @@
import pytest
from opik.api_objects.prompt import ChatPromptTemplate, PromptType
from opik import exceptions
def test_chat_prompt_template__format__simple_text_message__happyflow():
"""Test basic formatting of a simple text message."""
messages = [
{
"role": "user",
"content": "Hi, my name is {{name}} and I live in {{city}}.",
}
]
tested = ChatPromptTemplate(messages)
result = tested.format({"name": "Harry", "city": "London"})
assert result == [
{"role": "user", "content": "Hi, my name is Harry and I live in London."}
]
def test_chat_prompt_template__format__multiple_messages__happyflow():
"""Test formatting multiple messages with different roles."""
messages = [
{"role": "system", "content": "You are a helpful assistant in {{location}}."},
{"role": "user", "content": "What is the capital of {{country}}?"},
{"role": "assistant", "content": "The capital is {{capital}}."},
]
tested = ChatPromptTemplate(messages)
result = tested.format(
{
"location": "London",
"country": "France",
"capital": "Paris",
}
)
assert result == [
{"role": "system", "content": "You are a helpful assistant in London."},
{"role": "user", "content": "What is the capital of France?"},
{"role": "assistant", "content": "The capital is Paris."},
]
def test_chat_prompt_template__format__multimodal_content__happyflow():
"""Test formatting messages with multimodal content (text + image) when vision is supported."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this {{object}}:"},
{"type": "image_url", "image_url": {"url": "{{image_url}}"}},
],
}
]
tested = ChatPromptTemplate(messages)
# Format with vision supported to get structured content
result = tested.format(
{"object": "painting", "image_url": "https://example.com/image.jpg"},
supported_modalities={"vision": True},
)
assert result == [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this painting:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/image.jpg"},
},
],
}
]
def test_chat_prompt_template__format__jinja2_template__happyflow():
"""Test formatting with Jinja2 template type."""
messages = [
{
"role": "user",
"content": "Hi, my name is {{ name }} and I live in {{ city }}.",
}
]
tested = ChatPromptTemplate(messages, template_type=PromptType.JINJA2)
result = tested.format({"name": "Harry", "city": "London"})
assert result == [
{"role": "user", "content": "Hi, my name is Harry and I live in London."}
]
def test_chat_prompt_template__format__jinja2_with_control_flow():
"""Test Jinja2 formatting with control flow."""
messages = [
{
"role": "user",
"content": """
{% if is_wizard %}
{{ name }} is a wizard who lives in {{ city }}.
{% else %}
{{ name }} is a muggle who lives in {{ city }}.
{% endif %}
""",
}
]
tested = ChatPromptTemplate(messages, template_type=PromptType.JINJA2)
wizard_result = tested.format(
{"name": "Harry", "city": "London", "is_wizard": True}
)
assert (
"Harry is a wizard who lives in London." in wizard_result[0]["content"].strip()
)
muggle_result = tested.format(
{"name": "Dudley", "city": "Surrey", "is_wizard": False}
)
assert (
"Dudley is a muggle who lives in Surrey." in muggle_result[0]["content"].strip()
)
def test_chat_prompt_template__format__jinja2_with_loops():
"""Test Jinja2 formatting with loops."""
messages = [
{
"role": "user",
"content": """
{{ name }}'s friends are:
{% for friend in friends %}
- {{ friend }}
{% endfor %}
""",
}
]
tested = ChatPromptTemplate(messages, template_type=PromptType.JINJA2)
result = tested.format({"name": "Harry", "friends": ["Ron", "Hermione", "Neville"]})
content = result[0]["content"]
assert "Harry's friends are:" in content
assert "- Ron" in content
assert "- Hermione" in content
assert "- Neville" in content
def test_chat_prompt_template__format__empty_content():
"""Test formatting with empty content."""
messages = [
{"role": "user", "content": ""},
]
tested = ChatPromptTemplate(messages)
result = tested.format({})
assert result == [{"role": "user", "content": ""}]
def test_chat_prompt_template__format__message_without_role__skipped():
"""Test that messages without a role are skipped."""
messages = [
{"role": "user", "content": "Hello {{name}}"},
{"content": "This message has no role"},
{"role": "assistant", "content": "Hi there!"},
]
tested = ChatPromptTemplate(messages)
result = tested.format({"name": "Harry"})
# Only messages with roles should be included
assert result == [
{"role": "user", "content": "Hello Harry"},
{"role": "assistant", "content": "Hi there!"},
]
def test_chat_prompt_template__required_modalities__text_only():
"""Test required_modalities returns empty set for text-only messages."""
messages = [
{"role": "user", "content": "Simple text message"},
]
tested = ChatPromptTemplate(messages)
result = tested.required_modalities()
assert result == set()
def test_chat_prompt_template__required_modalities__with_vision():
"""Test required_modalities detects vision modality."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/img.jpg"},
},
],
}
]
tested = ChatPromptTemplate(messages)
result = tested.required_modalities()
assert "vision" in result
def test_chat_prompt_template__required_modalities__multiple_messages():
"""Test required_modalities across multiple messages."""
messages = [
{"role": "user", "content": "Text only"},
{
"role": "user",
"content": [
{"type": "text", "text": "With image:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/img.jpg"},
},
],
},
]
tested = ChatPromptTemplate(messages)
result = tested.required_modalities()
assert "vision" in result
def test_chat_prompt_template__format__unsupported_modality_replaced_with_placeholder():
"""Test that unsupported modalities are replaced with placeholders."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/img.jpg"},
},
],
}
]
tested = ChatPromptTemplate(messages)
# Format with vision not supported
result = tested.format({}, supported_modalities={"vision": False})
assert len(result) == 1
# When vision is not supported, image should be replaced with placeholder
content = result[0]["content"]
assert isinstance(content, str)
assert "<<<image>>>" in content
assert "<<</image>>>" in content
assert "Describe this image:" in content
def test_chat_prompt_template__format__supported_modality_preserved():
"""Test that supported modalities are preserved as structured content."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/img.jpg"},
},
],
}
]
tested = ChatPromptTemplate(messages)
# Format with vision supported
result = tested.format({}, supported_modalities={"vision": True})
assert result == [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/img.jpg"},
},
],
}
]
def test_chat_prompt_template__format__multimodal_with_template_variables():
"""Test multimodal content with template variables in both text and image URL."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Analyze this {{object}}:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/{{filename}}"},
},
],
}
]
tested = ChatPromptTemplate(messages)
result = tested.format(
{"object": "diagram", "filename": "diagram.png"},
supported_modalities={"vision": True},
)
assert result == [
{
"role": "user",
"content": [
{"type": "text", "text": "Analyze this diagram:"},
{
"type": "image_url",
"image_url": {"url": "https://example.com/diagram.png"},
},
],
}
]
def test_chat_prompt_template__format__image_with_detail_parameter():
"""Test that image detail parameter is preserved during formatting."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Analyze:"},
{
"type": "image_url",
"image_url": {"url": "{{url}}", "detail": "high"},
},
],
}
]
tested = ChatPromptTemplate(messages)
result = tested.format(
{"url": "https://example.com/img.jpg"}, supported_modalities={"vision": True}
)
assert result == [
{
"role": "user",
"content": [
{"type": "text", "text": "Analyze:"},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/img.jpg",
"detail": "high",
},
},
],
}
]
def test_chat_prompt_template__messages_property():
"""Test that messages property returns the original messages."""
messages = [
{"role": "user", "content": "Hello {{name}}"},
{"role": "assistant", "content": "Hi there!"},
]
tested = ChatPromptTemplate(messages)
assert tested.messages == messages
def test_chat_prompt_template__format__override_template_type():
"""Test that template_type can be overridden in format call."""
messages = [
{"role": "user", "content": "Name: {{name}}, City: {{city}}"},
]
# Create with Mustache default
tested = ChatPromptTemplate(messages, template_type=PromptType.MUSTACHE)
# Override with Jinja2 in format
result = tested.format(
{"name": "Harry", "city": "London"},
template_type=PromptType.JINJA2,
)
assert result == [{"role": "user", "content": "Name: Harry, City: London"}]
def test_chat_prompt_template__format__one_placeholder_used_multiple_times():
"""Test formatting with the same placeholder used multiple times."""
messages = [
{
"role": "user",
"content": "My name is {{name}}. I repeat, my name is {{name}}.",
}
]
tested = ChatPromptTemplate(messages)
result = tested.format({"name": "Harry"})
assert result == [
{"role": "user", "content": "My name is Harry. I repeat, my name is Harry."}
]
def test_chat_prompt_template__format__empty_messages_list():
"""Test formatting with empty messages list."""
messages = []
tested = ChatPromptTemplate(messages)
result = tested.format({})
assert result == []
def test_chat_prompt_template__format__message_with_missing_content():
"""Test formatting when message has no content field."""
messages = [
{"role": "user"}, # No content field
]
tested = ChatPromptTemplate(messages)
result = tested.format({})
assert result == [{"role": "user", "content": ""}]
def test_chat_prompt_template__format__passed_arguments_not_in_template__error_raised():
"""Test that extra format arguments not in template raise an error."""
messages = [
{"role": "user", "content": "Hi, my name is {{name}}, I live in {{city}}."}
]
tested = ChatPromptTemplate(messages, validate_placeholders=True)
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format({"name": "Harry", "city": "London", "nemesis_name": "Voldemort"})
assert exc_info.value.format_arguments == set(["name", "city", "nemesis_name"])
assert exc_info.value.prompt_placeholders == set(["name", "city"])
assert exc_info.value.symmetric_difference == set(["nemesis_name"])
def test_chat_prompt_template__format__some_placeholders_missing__error_raised():
"""Test that missing required placeholders raise an error."""
messages = [
{"role": "user", "content": "Hi, my name is {{name}}, I live in {{city}}."}
]
tested = ChatPromptTemplate(messages, validate_placeholders=True)
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format({"name": "Harry"})
assert exc_info.value.format_arguments == set(["name"])
assert exc_info.value.prompt_placeholders == set(["name", "city"])
assert exc_info.value.symmetric_difference == set(["city"])
def test_chat_prompt_template__format__placeholders_mismatch_both_ways__error_raised():
"""Test error when some placeholders are missing AND extra arguments provided."""
messages = [
{"role": "user", "content": "Hi, my name is {{name}}, I live in {{city}}."}
]
tested = ChatPromptTemplate(messages, validate_placeholders=True)
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format({"name": "Harry", "nemesis_name": "Voldemort"})
assert exc_info.value.format_arguments == set(["name", "nemesis_name"])
assert exc_info.value.prompt_placeholders == set(["name", "city"])
assert exc_info.value.symmetric_difference == set(["city", "nemesis_name"])
def test_chat_prompt_template__format__multimodal_placeholders__validates_all():
"""Test that placeholders in multimodal content are validated."""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Describe this {{object}}:"},
{"type": "image_url", "image_url": {"url": "{{image_url}}"}},
],
}
]
tested = ChatPromptTemplate(messages, validate_placeholders=True)
# Missing image_url placeholder
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format({"object": "painting"})
assert exc_info.value.format_arguments == set(["object"])
assert exc_info.value.prompt_placeholders == set(["object", "image_url"])
assert exc_info.value.symmetric_difference == set(["image_url"])
def test_chat_prompt_template__format__multiple_messages_placeholders__validates_all():
"""Test that placeholders across multiple messages are validated."""
messages = [
{"role": "system", "content": "You are an assistant in {{location}}."},
{"role": "user", "content": "What is the capital of {{country}}?"},
]
tested = ChatPromptTemplate(messages, validate_placeholders=True)
# Missing country placeholder
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format({"location": "London"})
assert exc_info.value.format_arguments == set(["location"])
assert exc_info.value.prompt_placeholders == set(["location", "country"])
assert exc_info.value.symmetric_difference == set(["country"])
def test_chat_prompt_template__format__validation_disabled__no_error():
"""Test that validation can be disabled."""
messages = [
{"role": "user", "content": "Hi, my name is {{name}}, I live in {{city}}."}
]
tested = ChatPromptTemplate(messages, validate_placeholders=False)
# Should not raise an error even though city is missing
result = tested.format({"name": "Harry"})
# The template will leave unformatted placeholders
assert result == [
{"role": "user", "content": "Hi, my name is Harry, I live in {{city}}."}
]
def test_chat_prompt_template__format__jinja2_no_validation():
"""Test that Jinja2 templates don't validate placeholders."""
messages = [
{"role": "user", "content": "Hi, my name is {{ name }}, I live in {{ city }}."}
]
tested = ChatPromptTemplate(messages, template_type=PromptType.JINJA2)
# Jinja2 templates don't validate, so this should not raise an error
# Jinja2 will just render missing variables as empty
result = tested.format({"name": "Harry"})
# Should render successfully (Jinja2 handles missing variables gracefully)
assert len(result) == 1
assert result[0]["role"] == "user"
@@ -0,0 +1,59 @@
from opik.api_objects.prompt.mask_context import (
get_active_prompt_masks,
get_mask_for_prompt,
prompt_mask_context,
)
class TestPromptMaskContext:
def test_no_context__returns_none(self):
assert get_active_prompt_masks() is None
def test_inside_context__returns_masks_dict(self):
masks = {"prompt-1": "mask-a", "prompt-2": "mask-b"}
with prompt_mask_context(masks):
assert get_active_prompt_masks() == masks
def test_get_mask_for_prompt__prompt_in_dict__returns_mask_id(self):
masks = {"prompt-uuid": "mask-uuid"}
with prompt_mask_context(masks):
assert get_mask_for_prompt("prompt-uuid") == "mask-uuid"
def test_get_mask_for_prompt__prompt_not_in_dict__returns_none(self):
masks = {"other-prompt": "mask-abc"}
with prompt_mask_context(masks):
assert get_mask_for_prompt("unknown-prompt") is None
def test_get_mask_for_prompt__no_context__returns_none(self):
assert get_mask_for_prompt("any-prompt-id") is None
def test_after_exit__resets_to_none(self):
with prompt_mask_context({"prompt-1": "mask-1"}):
pass
assert get_active_prompt_masks() is None
def test_context_with_none_masks__returns_none(self):
with prompt_mask_context(None):
assert get_active_prompt_masks() is None
assert get_mask_for_prompt("any-id") is None
def test_exception_inside__resets(self):
try:
with prompt_mask_context({"p": "m"}):
assert get_active_prompt_masks() == {"p": "m"}
raise ValueError("boom")
except ValueError:
pass
assert get_active_prompt_masks() is None
def test_nested_contexts__inner_wins_then_restores_outer(self):
outer = {"p1": "m1"}
inner = {"p2": "m2"}
with prompt_mask_context(outer):
assert get_active_prompt_masks() == outer
with prompt_mask_context(inner):
assert get_active_prompt_masks() == inner
assert get_mask_for_prompt("p2") == "m2"
assert get_mask_for_prompt("p1") is None
assert get_active_prompt_masks() == outer
assert get_active_prompt_masks() is None
@@ -0,0 +1,500 @@
"""Unit tests for the prompt client-side cache."""
import time
from typing import Optional
from unittest import mock
import pytest
from opik import config as opik_config
from opik.api_objects.prompt import prompt_cache
from opik.api_objects.prompt.prompt_cache import (
PromptCache,
get_global_cache,
)
def _get_ttl() -> int:
return opik_config.OpikConfig().prompt_cache_ttl_seconds
@pytest.fixture(autouse=True)
def clear_caches_after_test():
yield
get_global_cache().clear()
@pytest.fixture
def cache():
c = PromptCache()
yield c
c.clear()
def _make_mock_prompt(
name: str = "my-prompt",
commit: str = "abc123",
prompt_id: Optional[str] = None,
) -> mock.Mock:
p = mock.Mock()
p.name = name
p.commit = commit
p.__internal_api__prompt_id__ = prompt_id
return p
class TestPromptCache:
def test_get__missing_key__returns_none(self, cache):
assert cache.get(("p", None, None, "text")) is None
def test_get_or_fetch__cache_miss__fetches_and_caches(self, cache):
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
result = cache.get_or_fetch(
("p", None, None, "text"), fetch_fn, ttl_seconds=300
)
assert result is p
fetch_fn.assert_called_once()
def test_get_or_fetch__cache_hit__does_not_call_fetch_fn(self, cache):
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
cache.get_or_fetch(("p", None, None, "text"), fetch_fn, ttl_seconds=300)
fetch_fn.reset_mock()
result = cache.get_or_fetch(
("p", None, None, "text"), fetch_fn, ttl_seconds=300
)
assert result is p
fetch_fn.assert_not_called()
def test_get_or_fetch__fetch_returns_none__returns_none(self, cache):
fetch_fn = mock.Mock(return_value=None)
result = cache.get_or_fetch(
("missing", None, None, "text"), fetch_fn, ttl_seconds=300
)
assert result is None
def test_clear__populated_cache__removes_all_entries(self, cache):
p = _make_mock_prompt()
cache.get_or_fetch(
("p", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=None
)
cache.clear()
assert cache.get(("p", None, None, "text")) is None
def test_clear__running_refresh_thread__stops_thread(self, cache):
p = _make_mock_prompt()
cache.get_or_fetch(
("p", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=300
)
assert cache._thread is not None and cache._thread.is_alive()
cache.clear()
assert cache._thread is None
def test_refresh_thread__unpinned_entry__starts_thread(self, cache):
p = _make_mock_prompt()
cache.get_or_fetch(
("p", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=300
)
assert cache._thread is not None
assert cache._thread.is_alive()
def test_refresh_thread__pinned_entry__does_not_start_thread(self, cache):
p = _make_mock_prompt()
cache.get_or_fetch(
("p", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=None
)
assert cache._thread is None
def test_get_or_fetch__different_keys__returns_separate_entries(self, cache):
p1 = _make_mock_prompt(commit="c1")
p2 = _make_mock_prompt(commit="c2")
cache.get_or_fetch(
("p", "c1", None, "text"), mock.Mock(return_value=p1), ttl_seconds=None
)
cache.get_or_fetch(
("p", "c2", None, "text"), mock.Mock(return_value=p2), ttl_seconds=None
)
assert cache.get(("p", "c1", None, "text")) is p1
assert cache.get(("p", "c2", None, "text")) is p2
class TestBackgroundRefresh:
def test_refresh__stale_entry__updates_prompt(self):
new_prompt = _make_mock_prompt(commit=None)
callback = mock.Mock(return_value=new_prompt)
cache = get_global_cache()
base = time.monotonic()
with mock.patch("opik.api_objects.prompt.prompt_cache.time") as mock_time:
mock_time.monotonic.return_value = base
prompt_cache.get_or_fetch("p", None, None, "text", callback)
callback.reset_mock()
callback.return_value = new_prompt
mock_time.monotonic.return_value = base + _get_ttl() + 1
mock_time.sleep = mock.Mock()
cache._refresh_stale_entries()
assert callback.call_count >= 1
assert cache.get(("p", None, None, "text", None, None)) is new_prompt
def test_refresh__non_stale_entry__skips_callback(self):
p = _make_mock_prompt(commit=None)
callback = mock.Mock(return_value=p)
cache = get_global_cache()
base = time.monotonic()
with mock.patch("opik.api_objects.prompt.prompt_cache.time") as mock_time:
mock_time.monotonic.return_value = base
prompt_cache.get_or_fetch("p", None, None, "text", callback)
callback.reset_mock()
mock_time.monotonic.return_value = base + 0.2
cache._refresh_stale_entries()
callback.assert_not_called()
def test_refresh__callback_raises__thread_survives(self):
p = _make_mock_prompt(commit=None)
callback = mock.Mock(side_effect=[p, RuntimeError("boom")])
cache = get_global_cache()
base = time.monotonic()
with mock.patch("opik.api_objects.prompt.prompt_cache.time") as mock_time:
mock_time.monotonic.return_value = base
prompt_cache.get_or_fetch("p", None, None, "text", callback)
mock_time.monotonic.return_value = base + _get_ttl() + 1
cache._refresh_stale_entries()
assert cache._thread.is_alive()
class TestGetOrFetch:
def test_get_or_fetch__cache_miss__calls_fetch_fn(self):
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
result = prompt_cache.get_or_fetch("p", None, None, "text", fetch_fn)
assert result is p
fetch_fn.assert_called_once()
def test_get_or_fetch__fetch_returns_none__returns_none(self):
fetch_fn = mock.Mock(return_value=None)
result = prompt_cache.get_or_fetch("missing", None, None, "text", fetch_fn)
assert result is None
def test_get_or_fetch__cache_hit__does_not_call_fetch_fn(self):
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
prompt_cache.get_or_fetch("p", None, None, "text", fetch_fn)
fetch_fn.reset_mock()
result = prompt_cache.get_or_fetch("p", None, None, "text", fetch_fn)
assert result is p
fetch_fn.assert_not_called()
def test_get_or_fetch__pinned_commit__no_refresh_thread(self):
p = _make_mock_prompt(commit="abc")
fetch_fn = mock.Mock(return_value=p)
prompt_cache.get_or_fetch("p", "abc", None, "text", fetch_fn)
cache = get_global_cache()
assert cache._thread is None
def test_get_or_fetch__unpinned_commit__starts_refresh_thread(self):
p = _make_mock_prompt(commit=None)
fetch_fn = mock.Mock(return_value=p)
prompt_cache.get_or_fetch("p", None, None, "text", fetch_fn)
cache = get_global_cache()
assert cache._thread is not None
assert cache._thread.is_alive()
def test_get_or_fetch__cache_hit__returns_same_object(self):
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
prompt_cache.get_or_fetch("p", None, "proj", "text", fetch_fn)
first = prompt_cache.get_or_fetch("p", None, "proj", "text", fetch_fn)
second = prompt_cache.get_or_fetch("p", None, "proj", "text", fetch_fn)
assert first is second is p
def test_get_or_fetch__different_keys__returns_separate_entries(self):
p1 = _make_mock_prompt(commit="c1")
p2 = _make_mock_prompt(commit="c2")
prompt_cache.get_or_fetch("p", "c1", None, "text", mock.Mock(return_value=p1))
prompt_cache.get_or_fetch("p", "c2", None, "text", mock.Mock(return_value=p2))
r1 = prompt_cache.get_or_fetch("p", "c1", None, "text", mock.Mock())
r2 = prompt_cache.get_or_fetch("p", "c2", None, "text", mock.Mock())
assert r1 is p1
assert r2 is p2
class TestGetOrFetchVersionSelector:
"""Tests for the ``version`` parameter on the module-level ``get_or_fetch``."""
def test_get_or_fetch__different_versions__return_separate_entries(self):
p1 = _make_mock_prompt(commit="commitA")
p2 = _make_mock_prompt(commit="commitB")
fetch1 = mock.Mock(return_value=p1)
fetch2 = mock.Mock(return_value=p2)
first = prompt_cache.get_or_fetch("p", None, None, "text", fetch1, version="v1")
second = prompt_cache.get_or_fetch(
"p", None, None, "text", fetch2, version="v2"
)
assert first is p1
assert second is p2
fetch1.assert_called_once()
fetch2.assert_called_once()
def test_get_or_fetch__same_version__second_call_hits_cache(self):
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
first = prompt_cache.get_or_fetch(
"p", None, None, "text", fetch_fn, version="v2"
)
second = prompt_cache.get_or_fetch(
"p", None, None, "text", fetch_fn, version="v2"
)
assert first is second is p
fetch_fn.assert_called_once()
def test_get_or_fetch__commit_pin_and_version_pin__do_not_collide(self):
p_commit = _make_mock_prompt(commit="abc12345")
p_version = _make_mock_prompt(commit="def67890")
fetch_commit = mock.Mock(return_value=p_commit)
fetch_version = mock.Mock(return_value=p_version)
from_commit = prompt_cache.get_or_fetch(
"p", "abc12345", None, "text", fetch_commit
)
from_version = prompt_cache.get_or_fetch(
"p", None, None, "text", fetch_version, version="v3"
)
assert from_commit is p_commit
assert from_version is p_version
fetch_commit.assert_called_once()
fetch_version.assert_called_once()
def test_get_or_fetch__version_selector__starts_refresh_thread(self):
# Sequential versions can be reassigned by the backend if the underlying
# version is deleted and recreated, so they must follow the normal TTL
# refresh (not pinned indefinitely).
p = _make_mock_prompt()
fetch_fn = mock.Mock(return_value=p)
prompt_cache.get_or_fetch("p", None, None, "text", fetch_fn, version="v1")
cache = get_global_cache()
assert cache._thread is not None
assert cache._thread.is_alive()
class TestPromptCacheEdgeCases:
def test_get_or_fetch__multiple_unpinned_inserts__reuses_single_refresh_thread(
self, cache
):
p = _make_mock_prompt()
cache.get_or_fetch(
("a", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=300
)
thread1 = cache._thread
cache.get_or_fetch(
("b", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=300
)
assert cache._thread is thread1
def test_clear__on_empty_cache__is_noop(self, cache):
cache.clear()
assert cache.get(("any", None, None, "text")) is None
def test_refresh__pinned_entry__not_refreshed_even_after_ttl(self, cache):
p = _make_mock_prompt()
callback = mock.Mock(return_value=p)
base = time.monotonic()
with mock.patch("opik.api_objects.prompt.prompt_cache.time") as mock_time:
mock_time.monotonic.return_value = base
cache.get_or_fetch(("p", "v1", None, "text"), callback, ttl_seconds=None)
callback.reset_mock()
mock_time.monotonic.return_value = base + 0.2
cache._refresh_stale_entries()
callback.assert_not_called()
assert cache.get(("p", "v1", None, "text")) is p
def test_get_or_fetch__fetch_returns_none__not_cached(self, cache):
fetch_fn = mock.Mock(return_value=None)
cache.get_or_fetch(("p", None, None, "text"), fetch_fn, ttl_seconds=300)
assert cache.get(("p", None, None, "text")) is None
def test_refresh__callback_returns_none__preserves_original_prompt(self):
original = _make_mock_prompt(commit=None)
callback = mock.Mock(side_effect=[original, None])
cache = get_global_cache()
base = time.monotonic()
with mock.patch("opik.api_objects.prompt.prompt_cache.time") as mock_time:
mock_time.monotonic.return_value = base
prompt_cache.get_or_fetch("p", None, None, "text", callback)
mock_time.monotonic.return_value = base + _get_ttl() + 1
cache._refresh_stale_entries()
assert cache.get(("p", None, None, "text", None, None)) is original
def test_lru_eviction__oldest_entry_removed_when_max_size_exceeded(self):
cache = PromptCache(max_size=3)
prompts = [_make_mock_prompt(commit=f"c{i}") for i in range(4)]
for i, p in enumerate(prompts):
cache.get_or_fetch(
(f"p{i}", f"c{i}", None, "text"),
mock.Mock(return_value=p),
ttl_seconds=None,
)
assert cache.get(("p0", "c0", None, "text")) is None
assert cache.get(("p1", "c1", None, "text")) is prompts[1]
assert cache.get(("p2", "c2", None, "text")) is prompts[2]
assert cache.get(("p3", "c3", None, "text")) is prompts[3]
cache.clear()
def test_lru_eviction__access_refreshes_position(self):
cache = PromptCache(max_size=3)
prompts = [_make_mock_prompt(commit=f"c{i}") for i in range(3)]
for i, p in enumerate(prompts):
cache.get_or_fetch(
(f"p{i}", f"c{i}", None, "text"),
mock.Mock(return_value=p),
ttl_seconds=None,
)
# Access p0 so it becomes most-recently-used
cache.get(("p0", "c0", None, "text"))
# Insert a 4th entry — p1 (the actual LRU) should be evicted, not p0
p3 = _make_mock_prompt(commit="c3")
cache.get_or_fetch(
("p3", "c3", None, "text"),
mock.Mock(return_value=p3),
ttl_seconds=None,
)
assert cache.get(("p0", "c0", None, "text")) is prompts[0]
assert cache.get(("p1", "c1", None, "text")) is None
assert cache.get(("p2", "c2", None, "text")) is prompts[2]
assert cache.get(("p3", "c3", None, "text")) is p3
cache.clear()
def test_clear__called_twice__is_safe(self, cache):
p = _make_mock_prompt()
cache.get_or_fetch(
("p", None, None, "text"), mock.Mock(return_value=p), ttl_seconds=300
)
cache.clear()
cache.clear()
assert cache._thread is None
class TestPromptAutoInjection:
"""Test that get_prompt injects prompts into the active trace/span context via opik_prompts."""
@staticmethod
def _make_prompt_with_info_dict(name="my-prompt", commit="abc123", info_dict=None):
p = _make_mock_prompt(name=name, commit=commit)
p.__internal_api__to_info_dict__ = mock.Mock(
return_value=info_dict or {"name": name, "version": {"commit": commit}}
)
return p
def _call_get_prompt(self, cache_return_value):
from opik.api_objects import opik_client
client = opik_client.Opik()
with mock.patch(
"opik.api_objects.prompt.prompt_cache.get_or_fetch",
return_value=cache_return_value,
):
return client.get_prompt(
name="my-prompt", commit="abc123", project_name=None
)
def test_get_prompt__in_track_context__injects_into_metadata(self):
info_dict = {"name": "my-prompt", "version": {"commit": "abc123"}}
mock_prompt = self._make_prompt_with_info_dict(info_dict=info_dict)
mock_trace_data = mock.Mock()
mock_trace_data.metadata = None
mock_span_data = mock.Mock()
mock_span_data.metadata = None
with (
mock.patch(
"opik.context_storage.get_trace_data", return_value=mock_trace_data
),
mock.patch(
"opik.context_storage.top_span_data", return_value=mock_span_data
),
):
self._call_get_prompt(mock_prompt)
mock_trace_data.update.assert_called_once_with(
metadata={"opik_prompts": [info_dict]}
)
mock_span_data.update.assert_called_once_with(
metadata={"opik_prompts": [info_dict]}
)
def test_get_prompt__appends_to_existing_prompts(self):
existing_prompt_info = {"name": "old-prompt", "version": {"commit": "old123"}}
new_info_dict = {"name": "my-prompt", "version": {"commit": "abc123"}}
mock_prompt = self._make_prompt_with_info_dict(info_dict=new_info_dict)
mock_trace_data = mock.Mock()
mock_trace_data.metadata = {
"opik_prompts": [existing_prompt_info],
"other_key": "value",
}
mock_span_data = mock.Mock()
mock_span_data.metadata = {"opik_prompts": [existing_prompt_info]}
with (
mock.patch(
"opik.context_storage.get_trace_data", return_value=mock_trace_data
),
mock.patch(
"opik.context_storage.top_span_data", return_value=mock_span_data
),
):
self._call_get_prompt(mock_prompt)
mock_trace_data.update.assert_called_once_with(
metadata={"opik_prompts": [existing_prompt_info, new_info_dict]}
)
mock_span_data.update.assert_called_once_with(
metadata={"opik_prompts": [existing_prompt_info, new_info_dict]}
)
def test_get_prompt__no_track_context__no_error(self):
"""When there is no active trace context, injection silently does nothing."""
mock_prompt = self._make_prompt_with_info_dict()
with (
mock.patch("opik.context_storage.get_trace_data", return_value=None),
mock.patch("opik.context_storage.top_span_data", return_value=None),
):
result = self._call_get_prompt(mock_prompt)
assert result is mock_prompt
def test_get_prompt__none_result__no_injection(self):
with (
mock.patch("opik.context_storage.get_trace_data") as mock_get_trace,
mock.patch("opik.context_storage.top_span_data") as mock_top_span,
):
result = self._call_get_prompt(None)
assert result is None
mock_get_trace.assert_not_called()
mock_top_span.assert_not_called()
@@ -0,0 +1,793 @@
"""Unit tests for PromptClient to verify API endpoint selection logic."""
from unittest import mock
from typing import Optional
import pytest
from opik import exceptions
from opik.api_objects import opik_client as opik_client_module
from opik.api_objects.prompt import client as prompt_client
from opik.api_objects.prompt import prompt_cache
from opik.api_objects.prompt import types as prompt_types
from opik.rest_api import core as rest_api_core
from opik.rest_api.types import prompt_version_detail
@pytest.fixture
def mock_rest_client():
"""Create a mock REST client."""
client = mock.Mock()
client.prompts = mock.Mock()
return client
@pytest.fixture
def client(mock_rest_client):
"""Create a PromptClient with a mock REST client."""
return prompt_client.PromptClient(mock_rest_client)
def _make_mock_version(
template: str = "test template",
version_type: str = "mustache",
metadata: Optional[dict] = None,
version_number: Optional[str] = "v1",
tags: Optional[list] = None,
environments: Optional[list] = None,
) -> prompt_version_detail.PromptVersionDetail:
"""Helper to create a mock PromptVersionDetail."""
return prompt_version_detail.PromptVersionDetail(
id="version-id",
prompt_id="prompt-id",
template=template,
type=version_type,
metadata=metadata,
commit="abc123",
version_number=version_number,
tags=tags,
environments=environments,
template_structure="text",
)
def _make_404_error() -> rest_api_core.ApiError:
"""Helper to create a 404 ApiError."""
error = rest_api_core.ApiError(status_code=404, body=None)
return error
class TestPromptClientEndpointSelection:
"""Tests to verify that PromptClient calls the correct REST endpoint based on parameters."""
def test_create_new_prompt_with_container_params_calls_create_prompt(
self, client, mock_rest_client
):
"""When creating a new prompt with id/description/tags, should call create_prompt endpoint.
Also asserts that version_number survives the tag-rebuild branch."""
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
_make_404_error(),
_make_mock_version(tags=None),
]
result = client.create_prompt(
name="test-prompt",
prompt="test template",
metadata=None,
type=prompt_types.PromptType.MUSTACHE,
id="custom-id",
description="A test prompt",
tags=["test", "unit"],
)
mock_rest_client.prompts.create_prompt.assert_called_once()
mock_rest_client.prompts.create_prompt_version.assert_not_called()
call_kwargs = mock_rest_client.prompts.create_prompt.call_args[1]
assert call_kwargs["name"] == "test-prompt"
assert call_kwargs["id"] == "custom-id"
assert call_kwargs["description"] == "A test prompt"
assert call_kwargs["tags"] == ["test", "unit"]
assert result.version_number == "v1"
assert result.tags == ["test", "unit"]
def test_create_new_prompt_without_container_params_calls_create_version(
self, client, mock_rest_client
):
"""When creating a new prompt without id/description/tags, should call create_prompt_version.
Also asserts that version_number from the create response is surfaced."""
mock_rest_client.prompts.retrieve_prompt_version.side_effect = _make_404_error()
mock_rest_client.prompts.create_prompt_version.return_value = (
_make_mock_version()
)
result = client.create_prompt(
name="test-prompt",
prompt="test template",
metadata=None,
type=prompt_types.PromptType.MUSTACHE,
)
mock_rest_client.prompts.create_prompt_version.assert_called_once()
mock_rest_client.prompts.create_prompt.assert_not_called()
assert result.version_number == "v1"
def test_update_existing_prompt_always_calls_create_version(
self, client, mock_rest_client
):
"""When updating an existing prompt, should always call create_prompt_version regardless of params."""
existing_version = _make_mock_version(template="old template")
mock_rest_client.prompts.retrieve_prompt_version.return_value = existing_version
mock_rest_client.prompts.create_prompt_version.return_value = (
_make_mock_version(template="new template")
)
client.create_prompt(
name="test-prompt",
prompt="new template",
metadata=None,
type=prompt_types.PromptType.MUSTACHE,
id="custom-id",
description="Updated description",
tags=["updated"],
)
mock_rest_client.prompts.create_prompt_version.assert_called_once()
mock_rest_client.prompts.create_prompt.assert_not_called()
def test_create_new_prompt_with_only_id_calls_create_prompt(
self, client, mock_rest_client
):
"""When creating a new prompt with only id parameter, should call create_prompt."""
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
_make_404_error(),
_make_mock_version(),
]
client.create_prompt(
name="test-prompt",
prompt="test template",
metadata=None,
type=prompt_types.PromptType.MUSTACHE,
id="custom-id",
)
mock_rest_client.prompts.create_prompt.assert_called_once()
mock_rest_client.prompts.create_prompt_version.assert_not_called()
def test_create_new_prompt_with_only_description_calls_create_prompt(
self, client, mock_rest_client
):
"""When creating a new prompt with only description parameter, should call create_prompt."""
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
_make_404_error(),
_make_mock_version(),
]
client.create_prompt(
name="test-prompt",
prompt="test template",
metadata=None,
type=prompt_types.PromptType.MUSTACHE,
description="A test prompt",
)
mock_rest_client.prompts.create_prompt.assert_called_once()
mock_rest_client.prompts.create_prompt_version.assert_not_called()
def test_create_new_prompt_with_only_tags_calls_create_prompt(
self, client, mock_rest_client
):
"""When creating a new prompt with only tags parameter, should call create_prompt.
Tags-only takes the rebuild branch — assert version_number survives it
(regression for the bug where tag injection dropped version_number)."""
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
_make_404_error(),
_make_mock_version(tags=None),
]
result = client.create_prompt(
name="test-prompt",
prompt="test template",
metadata=None,
type=prompt_types.PromptType.MUSTACHE,
tags=["test"],
)
mock_rest_client.prompts.create_prompt.assert_called_once()
mock_rest_client.prompts.create_prompt_version.assert_not_called()
assert result.version_number == "v1"
assert result.tags == ["test"]
class TestInternalCreateMask:
"""Tests for __internal__create_mask method."""
def test_create_mask_calls_create_prompt_version_with_mask_type(
self, client, mock_rest_client
):
expected = _make_mock_version()
mock_rest_client.prompts.create_prompt_version.return_value = expected
result = client._PromptClient__internal_api__create_mask(
name="test-prompt",
prompt="masked template",
)
mock_rest_client.prompts.create_prompt_version.assert_called_once()
call_kwargs = mock_rest_client.prompts.create_prompt_version.call_args[1]
assert call_kwargs["name"] == "test-prompt"
assert call_kwargs["version"].template == "masked template"
assert call_kwargs["version"].version_type == "mask"
assert result == expected
def test_create_mask_passes_all_parameters(self, client, mock_rest_client):
mock_rest_client.prompts.create_prompt_version.return_value = (
_make_mock_version()
)
client._PromptClient__internal_api__create_mask(
name="test-prompt",
prompt="masked template",
type=prompt_types.PromptType.JINJA2,
metadata={"key": "value"},
template_structure="chat",
project_name="my-project",
change_description="mask for testing",
)
call_kwargs = mock_rest_client.prompts.create_prompt_version.call_args[1]
assert call_kwargs["name"] == "test-prompt"
assert call_kwargs["template_structure"] == "chat"
assert call_kwargs["project_name"] == "my-project"
version = call_kwargs["version"]
assert version.template == "masked template"
assert version.version_type == "mask"
assert version.type == prompt_types.PromptType.JINJA2
assert version.metadata == {"key": "value"}
assert version.change_description == "mask for testing"
def test_create_mask_defaults(self, client, mock_rest_client):
mock_rest_client.prompts.create_prompt_version.return_value = (
_make_mock_version()
)
client._PromptClient__internal_api__create_mask(
name="test-prompt",
prompt="template",
)
call_kwargs = mock_rest_client.prompts.create_prompt_version.call_args[1]
assert call_kwargs["template_structure"] == "text"
assert call_kwargs["project_name"] is None
version = call_kwargs["version"]
assert version.type == prompt_types.PromptType.MUSTACHE
assert version.metadata is None
assert version.change_description is None
class TestGetPromptByVersionSelector:
"""Tests for the new sequential ``version`` parameter (e.g. ``"v3"``)."""
@pytest.fixture(autouse=True)
def clear_global_cache(self):
yield
prompt_cache.get_global_cache().clear()
def test_get_prompt__version_arg__threads_through_as_version_number(
self, client, mock_rest_client
):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
client.get_prompt(name="my-prompt", version="v3")
mock_rest_client.prompts.retrieve_prompt_version.assert_called_once()
call_kwargs = mock_rest_client.prompts.retrieve_prompt_version.call_args[1]
assert call_kwargs["name"] == "my-prompt"
assert call_kwargs["version_number"] == "v3"
assert call_kwargs["commit"] is None
def test_get_prompt__commit_and_version_both_set__raises_value_error(
self, client, mock_rest_client
):
with pytest.raises(ValueError, match=r"Provide either `commit` or `version`"):
client.get_prompt(name="my-prompt", commit="abc12345", version="v1")
mock_rest_client.prompts.retrieve_prompt_version.assert_not_called()
def test_get_prompt_with_cache__commit_and_version_both_set__raises_value_error(
self, client, mock_rest_client
):
from opik.api_objects.prompt.text import prompt as text_prompt_module
with pytest.raises(ValueError, match=r"Provide either `commit` or `version`"):
client.get_prompt_with_cache(
name="my-prompt",
commit="abc12345",
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v1",
)
def test_get_prompt_with_cache__commit_and_environment__raises_value_error(
self, client, mock_rest_client
):
from opik.api_objects.prompt.text import prompt as text_prompt_module
with pytest.raises(ValueError, match="mutually exclusive"):
client.get_prompt_with_cache(
name="my-prompt",
commit="abc12345",
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
environment="staging",
)
def test_get_prompt_with_cache__version_and_environment__raises_value_error(
self, client, mock_rest_client
):
from opik.api_objects.prompt.text import prompt as text_prompt_module
with pytest.raises(ValueError, match="mutually exclusive"):
client.get_prompt_with_cache(
name="my-prompt",
commit=None,
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v1",
environment="staging",
)
def test_get_prompt_with_cache__different_versions__do_not_collide_in_cache(
self, client, mock_rest_client
):
from opik.api_objects.prompt.text import prompt as text_prompt_module
v1 = _make_mock_version(template="content for v1")
v2 = _make_mock_version(template="content for v2")
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [v1, v2]
first = client.get_prompt_with_cache(
name="my-prompt",
commit=None,
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v1",
)
second = client.get_prompt_with_cache(
name="my-prompt",
commit=None,
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v2",
)
assert first is not None and first.prompt == "content for v1"
assert second is not None and second.prompt == "content for v2"
assert mock_rest_client.prompts.retrieve_prompt_version.call_count == 2
def test_get_prompt_with_cache__commit_and_version_pin__do_not_collide(
self, client, mock_rest_client
):
from opik.api_objects.prompt.text import prompt as text_prompt_module
by_commit = _make_mock_version(template="content by commit")
by_version = _make_mock_version(template="content by version")
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
by_commit,
by_version,
]
commit_result = client.get_prompt_with_cache(
name="my-prompt",
commit="abc12345",
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
)
version_result = client.get_prompt_with_cache(
name="my-prompt",
commit=None,
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v3",
)
assert commit_result is not None
assert commit_result.prompt == "content by commit"
assert version_result is not None
assert version_result.prompt == "content by version"
assert mock_rest_client.prompts.retrieve_prompt_version.call_count == 2
def test_get_prompt_with_cache__same_version_twice__second_hits_cache(
self, client, mock_rest_client
):
from opik.api_objects.prompt.text import prompt as text_prompt_module
v2 = _make_mock_version(template="v2 content")
mock_rest_client.prompts.retrieve_prompt_version.return_value = v2
first = client.get_prompt_with_cache(
name="my-prompt",
commit=None,
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v2",
)
second = client.get_prompt_with_cache(
name="my-prompt",
commit=None,
project_name=None,
template_structure="text",
prompt_cls=text_prompt_module.Prompt,
version="v2",
)
assert first is second
assert mock_rest_client.prompts.retrieve_prompt_version.call_count == 1
class TestGetPromptWithCacheBypass:
"""Tests for no_cache parameter in Opik.get_prompt()."""
@pytest.fixture(autouse=True)
def clear_global_cache(self):
yield
prompt_cache.get_global_cache().clear()
@pytest.fixture
def opik_client(self, mock_rest_client):
client = opik_client_module.Opik()
client._rest_client = mock_rest_client
return client
@pytest.mark.parametrize(
"no_cache, expected_extra_calls",
[
(False, 0),
(True, 1),
],
ids=["no_cache_false__uses_cache", "no_cache_true__hits_backend"],
)
def test_get_prompt__second_call_after_cache_warm__respects_no_cache(
self, opik_client, mock_rest_client, no_cache, expected_extra_calls
):
version = _make_mock_version()
mock_rest_client.prompts.retrieve_prompt_version.return_value = version
opik_client.get_prompt(name="my-prompt", commit=None, project_name=None)
call_count_after_warm = (
mock_rest_client.prompts.retrieve_prompt_version.call_count
)
opik_client.get_prompt(
name="my-prompt", commit=None, project_name=None, no_cache=no_cache
)
assert (
mock_rest_client.prompts.retrieve_prompt_version.call_count
== call_count_after_warm + expected_extra_calls
)
def test_get_prompt__no_cache_true__returns_fresh_value_from_backend(
self, opik_client, mock_rest_client
):
old_version = _make_mock_version(template="old template")
new_version = _make_mock_version(template="new template")
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
old_version,
new_version,
]
opik_client.get_prompt(name="my-prompt", commit=None, project_name=None)
result = opik_client.get_prompt(
name="my-prompt", commit=None, project_name=None, no_cache=True
)
assert result is not None
assert result.prompt == "new template"
def test_get_prompt__no_cache_true__backend_returns_none__returns_none(
self, opik_client, mock_rest_client
):
mock_rest_client.prompts.retrieve_prompt_version.side_effect = _make_404_error()
result = opik_client.get_prompt(
name="missing-prompt", commit=None, project_name=None, no_cache=True
)
assert result is None
class TestPromptEnvironment:
"""Unit tests for the prompt ``environment`` plumbing."""
@pytest.fixture(autouse=True)
def clear_global_cache(self):
yield
prompt_cache.get_global_cache().clear()
def test_get_prompt__forwards_environment_to_retrieve(
self, client, mock_rest_client
):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
client.get_prompt(name="env-prompt", environment="staging")
mock_rest_client.prompts.retrieve_prompt_version.assert_called_once()
call_kwargs = mock_rest_client.prompts.retrieve_prompt_version.call_args[1]
assert call_kwargs["environment"] == "staging"
def test_get_prompt__commit_and_environment__raises_value_error(
self, client, mock_rest_client
):
with pytest.raises(ValueError, match="mutually exclusive"):
client.get_prompt(
name="env-prompt", commit="abc12345", environment="staging"
)
mock_rest_client.prompts.retrieve_prompt_version.assert_not_called()
def test_get_prompt__version_and_environment__raises_value_error(
self, client, mock_rest_client
):
with pytest.raises(ValueError, match="mutually exclusive"):
client.get_prompt(name="env-prompt", version="v3", environment="staging")
mock_rest_client.prompts.retrieve_prompt_version.assert_not_called()
def test_get_prompt_with_cache__different_environments__not_cached_together(
self, mock_rest_client
):
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
staging_version = prompt_version_detail.PromptVersionDetail(
id="staging-id",
prompt_id="prompt-id",
template="staging template",
type="mustache",
metadata=None,
commit="aaa",
template_structure="text",
environments=["staging"],
)
production_version = prompt_version_detail.PromptVersionDetail(
id="prod-id",
prompt_id="prompt-id",
template="prod template",
type="mustache",
metadata=None,
commit="bbb",
template_structure="text",
environments=["production"],
)
mock_rest_client.prompts.retrieve_prompt_version.side_effect = [
staging_version,
production_version,
]
staging = opik_client.get_prompt(name="env-prompt", environment="staging")
production = opik_client.get_prompt(name="env-prompt", environment="production")
assert staging is not None and production is not None
assert staging.commit == "aaa"
assert production.commit == "bbb"
assert mock_rest_client.prompts.retrieve_prompt_version.call_count == 2
def test_set_prompt_environments__sets_via_latest_version(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
opik_client.set_prompt_environments("env-prompt", ["staging"])
mock_rest_client.prompts.retrieve_prompt_version.assert_called_once()
retrieve_kwargs = mock_rest_client.prompts.retrieve_prompt_version.call_args[1]
assert retrieve_kwargs["name"] == "env-prompt"
mock_rest_client.prompts.set_prompt_version_environment.assert_called_once_with(
version_id="version-id",
environments=["staging"],
)
def test_set_prompt_environments__empty_list_clears(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
opik_client.set_prompt_environments("env-prompt", [])
mock_rest_client.prompts.set_prompt_version_environment.assert_called_once_with(
version_id="version-id",
environments=[],
)
def test_set_prompt_environments__multiple_envs_in_one_call(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
opik_client.set_prompt_environments("env-prompt", ["staging", "production"])
mock_rest_client.prompts.set_prompt_version_environment.assert_called_once_with(
version_id="version-id",
environments=["staging", "production"],
)
def test_set_prompt_environments__deduplicates_input(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
opik_client.set_prompt_environments(
"env-prompt", ["staging", "staging", "production"]
)
mock_rest_client.prompts.set_prompt_version_environment.assert_called_once_with(
version_id="version-id",
environments=["staging", "production"],
)
def test_set_prompt_environments__forwards_project_name(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
opik_client.set_prompt_environments(
"env-prompt", ["staging"], project_name="my-project"
)
mock_rest_client.prompts.retrieve_prompt_version.assert_called_once()
retrieve_kwargs = mock_rest_client.prompts.retrieve_prompt_version.call_args[1]
assert retrieve_kwargs["name"] == "env-prompt"
assert retrieve_kwargs["project_name"] == "my-project"
def test_set_prompt_environments__forwards_version(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
opik_client.set_prompt_environments("env-prompt", ["staging"], version="v3")
mock_rest_client.prompts.retrieve_prompt_version.assert_called_once()
retrieve_kwargs = mock_rest_client.prompts.retrieve_prompt_version.call_args[1]
assert retrieve_kwargs["name"] == "env-prompt"
# The SDK forwards ``version`` as the wire-level ``version_number`` field —
# ``commit`` is no longer part of this method's surface.
assert retrieve_kwargs["version_number"] == "v3"
assert "commit" not in retrieve_kwargs
mock_rest_client.prompts.set_prompt_version_environment.assert_called_once_with(
version_id="version-id",
environments=["staging"],
)
def test_set_prompt_environments__prompt_not_found__raises_prompt_not_found(
self, mock_rest_client
):
mock_rest_client.prompts.retrieve_prompt_version.side_effect = (
rest_api_core.ApiError(status_code=404, body=None)
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
with pytest.raises(exceptions.PromptNotFoundError, match="missing-prompt"):
opik_client.set_prompt_environments("missing-prompt", ["staging"])
mock_rest_client.prompts.set_prompt_version_environment.assert_not_called()
def test_set_prompt_environments__version_not_found__raises_prompt_not_found_with_version(
self, mock_rest_client
):
mock_rest_client.prompts.retrieve_prompt_version.side_effect = (
rest_api_core.ApiError(status_code=404, body=None)
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
with pytest.raises(exceptions.PromptNotFoundError, match="v7"):
opik_client.set_prompt_environments("env-prompt", ["staging"], version="v7")
mock_rest_client.prompts.set_prompt_version_environment.assert_not_called()
# The backend reports an unknown environment as 404 or 409 from the
# workspace-registry check; both must surface as EnvironmentNotFoundError.
@pytest.mark.parametrize("status_code", [404, 409])
def test_set_prompt_environments__environment_not_found__raises_environment_not_found(
self, mock_rest_client, status_code
):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
mock_rest_client.prompts.set_prompt_version_environment.side_effect = (
rest_api_core.ApiError(status_code=status_code, body=None)
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
with pytest.raises(exceptions.EnvironmentNotFoundError, match="unknown-env"):
opik_client.set_prompt_environments("env-prompt", ["unknown-env"])
def test_set_prompt_environments__other_api_error__bubbles_up(
self, mock_rest_client
):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
mock_rest_client.prompts.set_prompt_version_environment.side_effect = (
rest_api_core.ApiError(status_code=500, body=None)
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
with pytest.raises(rest_api_core.ApiError):
opik_client.set_prompt_environments("env-prompt", ["staging"])
def test_set_prompt_environments__invalidates_cache(self, mock_rest_client):
mock_rest_client.prompts.retrieve_prompt_version.return_value = (
_make_mock_version()
)
opik_client = opik_client_module.Opik()
opik_client._rest_client = mock_rest_client
resolved_project = opik_client._resolve_project_name(None)
cache = prompt_cache.get_global_cache()
cache.clear()
try:
sentinel = mock.MagicMock()
cache.get_or_fetch(
key=("env-prompt", None, resolved_project, "text", "staging"),
fetch_fn=lambda: sentinel,
ttl_seconds=None,
)
cache.get_or_fetch(
key=("other-prompt", None, resolved_project, "text", "staging"),
fetch_fn=lambda: sentinel,
ttl_seconds=None,
)
opik_client.set_prompt_environments("env-prompt", ["production"])
assert (
cache.get(("env-prompt", None, resolved_project, "text", "staging"))
is None
), "stale entry for the updated prompt should be evicted"
assert (
cache.get(("other-prompt", None, resolved_project, "text", "staging"))
is sentinel
), "entries for unrelated prompts must not be evicted"
finally:
cache.clear()
@@ -0,0 +1,180 @@
"""Tests for Prompt and ChatPrompt resilience when backend sync fails."""
from unittest.mock import patch, MagicMock
import httpx
import pytest
from opik.rest_api.core import ApiError
from opik.api_objects.prompt.text.prompt import Prompt
from opik.api_objects.prompt.chat.chat_prompt import ChatPrompt
_API_ERRORS = [
pytest.param(ApiError(status_code=500, body="Internal Server Error"), id="api_500"),
pytest.param(ApiError(status_code=503, body="Service Unavailable"), id="api_503"),
pytest.param(httpx.ConnectError("Connection refused"), id="connect_error"),
pytest.param(httpx.TimeoutException("Request timed out"), id="timeout"),
]
def _mock_opik_client():
"""Patch the opik_client.get_client_cached() used inside sync_with_backend."""
return patch(
"opik.api_objects.opik_client.get_client_cached",
return_value=MagicMock(),
)
class TestPromptSyncFailure:
"""Prompt should be created locally even when sync_with_backend fails with API errors."""
@pytest.mark.parametrize("error", _API_ERRORS)
def test_prompt_created_despite_backend_failure(self, error):
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
side_effect=error,
),
):
prompt = Prompt(name="test-prompt", prompt="Hello {{name}}")
assert prompt.name == "test-prompt"
assert prompt.prompt == "Hello {{name}}"
assert prompt.commit is None
assert prompt.synced is False
assert prompt.format(name="World") == "Hello World"
def test_sync_returns_false_on_api_error(self):
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
side_effect=ApiError(status_code=500),
),
):
prompt = Prompt(name="test-prompt", prompt="Hello {{name}}")
result = prompt.sync_with_backend()
assert result is False
assert prompt.synced is False
def test_sync_returns_true_on_success(self):
mock_version = MagicMock()
mock_version.commit = "abc123"
mock_version.prompt_id = "pid"
mock_version.id = "vid"
mock_version.change_description = None
mock_version.tags = []
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
return_value=mock_version,
),
):
prompt = Prompt(name="test-prompt", prompt="Hello {{name}}")
assert prompt.commit == "abc123"
assert prompt.synced is True
# Re-sync also succeeds
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
return_value=mock_version,
),
):
assert prompt.sync_with_backend() is True
assert prompt.synced is True
def test_non_api_error_propagates(self):
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
side_effect=ValueError("bad value"),
),
):
with pytest.raises(ValueError, match="bad value"):
Prompt(name="test-prompt", prompt="Hello {{name}}")
class TestChatPromptSyncFailure:
"""ChatPrompt should be created locally even when sync_with_backend fails with API errors."""
MESSAGES = [{"role": "user", "content": "Hello {{name}}"}]
@pytest.mark.parametrize("error", _API_ERRORS)
def test_chat_prompt_created_despite_backend_failure(self, error):
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
side_effect=error,
),
):
prompt = ChatPrompt(name="test-chat", messages=self.MESSAGES)
assert prompt.name == "test-chat"
assert prompt.template == self.MESSAGES
assert prompt.commit is None
assert prompt.synced is False
def test_sync_returns_false_on_api_error(self):
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
side_effect=ApiError(status_code=500),
),
):
prompt = ChatPrompt(name="test-chat", messages=self.MESSAGES)
result = prompt.sync_with_backend()
assert result is False
assert prompt.synced is False
def test_sync_returns_true_on_success(self):
mock_version = MagicMock()
mock_version.commit = "abc123"
mock_version.prompt_id = "pid"
mock_version.id = "vid"
mock_version.change_description = None
mock_version.tags = []
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
return_value=mock_version,
),
):
prompt = ChatPrompt(name="test-chat", messages=self.MESSAGES)
assert prompt.commit == "abc123"
assert prompt.synced is True
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
return_value=mock_version,
),
):
assert prompt.sync_with_backend() is True
assert prompt.synced is True
def test_non_api_error_propagates(self):
with (
_mock_opik_client(),
patch(
"opik.api_objects.prompt.client.PromptClient.create_prompt",
side_effect=ValueError("bad value"),
),
):
with pytest.raises(ValueError, match="bad value"):
ChatPrompt(name="test-chat", messages=self.MESSAGES)
@@ -0,0 +1,138 @@
import pytest
from opik.api_objects.prompt import PromptTemplate, PromptType
from opik import exceptions
def test_prompt__format__happyflow():
PROMPT_TEMPLATE = "Hi, my name is {{name}}, I live in {{city}}."
tested = PromptTemplate(PROMPT_TEMPLATE)
result = tested.format(name="Harry", city="London")
assert result == "Hi, my name is Harry, I live in London."
def test_prompt__format__one_placeholder_used_multiple_times():
PROMPT_TEMPLATE = (
"Hi, my name is {{name}}, I live in {{city}}. I repeat, my name is {{name}}"
)
tested = PromptTemplate(PROMPT_TEMPLATE)
result = tested.format(name="Harry", city="London")
assert (
result == "Hi, my name is Harry, I live in London. I repeat, my name is Harry"
)
def test_prompt__format__passed_arguments_that_are_not_in_template__error_raised_with_correct_report_info():
PROMPT_TEMPLATE = "Hi, my name is {{name}}, I live in {{city}}."
tested = PromptTemplate(PROMPT_TEMPLATE)
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format(name="Harry", city="London", nemesis_name="Voldemort")
assert exc_info.value.format_arguments == set(["name", "city", "nemesis_name"])
assert exc_info.value.prompt_placeholders == set(
[
"name",
"city",
]
)
assert exc_info.value.symmetric_difference == set(["nemesis_name"])
def test_prompt__format__some_placeholders_dont_have_corresponding_format_arguments__error_raised_with_correct_report_info():
PROMPT_TEMPLATE = "Hi, my name is {{name}}, I live in {{city}}."
tested = PromptTemplate(PROMPT_TEMPLATE)
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format(name="Harry")
assert exc_info.value.format_arguments == set(["name"])
assert exc_info.value.prompt_placeholders == set(["name", "city"])
assert exc_info.value.symmetric_difference == set(["city"])
def test_prompt__format__some_placeholders_dont_have_corresponding_format_arguments_AND_there_are_format_arguments_that_are_not_in_the_template__error_raised_with_correct_report_info():
PROMPT_TEMPLATE = "Hi, my name is {{name}}, I live in {{city}}."
tested = PromptTemplate(PROMPT_TEMPLATE)
with pytest.raises(
exceptions.PromptPlaceholdersDontMatchFormatArguments
) as exc_info:
tested.format(name="Harry", nemesis_name="Voldemort")
assert exc_info.value.format_arguments == set(["name", "nemesis_name"])
assert exc_info.value.prompt_placeholders == set(["name", "city"])
assert exc_info.value.symmetric_difference == set(["city", "nemesis_name"])
def test_prompt__format_jinja2__happyflow():
PROMPT_TEMPLATE = "Hi, my name is {{ name }}, I live in {{ city }}."
tested = PromptTemplate(PROMPT_TEMPLATE, type=PromptType.JINJA2)
result = tested.format(name="Harry", city="London")
assert result == "Hi, my name is Harry, I live in London."
def test_prompt__format_jinja2__with_control_flow():
PROMPT_TEMPLATE = """
{% if is_wizard %}
{{ name }} is a wizard who lives in {{ city }}.
{% else %}
{{ name }} is a muggle who lives in {{ city }}.
{% endif %}
"""
tested = PromptTemplate(PROMPT_TEMPLATE, type=PromptType.JINJA2)
wizard_result = tested.format(name="Harry", city="London", is_wizard=True)
assert "Harry is a wizard who lives in London." in wizard_result.strip()
muggle_result = tested.format(name="Dudley", city="Surrey", is_wizard=False)
assert "Dudley is a muggle who lives in Surrey." in muggle_result.strip()
def test_prompt__format_jinja2__with_loops():
PROMPT_TEMPLATE = """
{{ name }}'s friends are:
{% for friend in friends %}
- {{ friend }}
{% endfor %}
"""
tested = PromptTemplate(PROMPT_TEMPLATE, type=PromptType.JINJA2)
result = tested.format(name="Harry", friends=["Ron", "Hermione", "Neville"])
assert "Harry's friends are:" in result
assert "- Ron" in result
assert "- Hermione" in result
assert "- Neville" in result
def test_prompt__format_jinja2__with_filters():
PROMPT_TEMPLATE = "{{ name | upper }} lives in {{ city | lower }}."
tested = PromptTemplate(PROMPT_TEMPLATE, type=PromptType.JINJA2)
result = tested.format(name="Harry", city="LONDON")
assert result == "HARRY lives in london."
def test_prompt__format__none_values_render_as_empty_strings() -> None:
PROMPT_TEMPLATE = "Primary: {{primary}} Secondary: {{secondary}}"
tested = PromptTemplate(PROMPT_TEMPLATE)
result = tested.format(primary="cat", secondary=None)
assert result == "Primary: cat Secondary: "