opik 1.9.26__py3-none-any.whl → 1.9.39__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opik/__init__.py +10 -3
- opik/api_objects/dataset/rest_operations.py +2 -0
- opik/api_objects/experiment/experiment.py +31 -5
- opik/api_objects/experiment/helpers.py +34 -10
- opik/api_objects/local_recording.py +8 -3
- opik/api_objects/opik_client.py +218 -46
- opik/api_objects/opik_query_language.py +9 -0
- opik/api_objects/prompt/__init__.py +11 -3
- opik/api_objects/prompt/base_prompt.py +69 -0
- opik/api_objects/prompt/base_prompt_template.py +29 -0
- opik/api_objects/prompt/chat/__init__.py +1 -0
- opik/api_objects/prompt/chat/chat_prompt.py +193 -0
- opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
- opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +31 -34
- opik/api_objects/prompt/client.py +101 -30
- opik/api_objects/prompt/text/__init__.py +1 -0
- opik/api_objects/prompt/{prompt.py → text/prompt.py} +55 -32
- opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +8 -5
- opik/cli/export.py +6 -2
- opik/config.py +0 -5
- opik/decorator/base_track_decorator.py +37 -40
- opik/evaluation/__init__.py +13 -2
- opik/evaluation/engine/engine.py +195 -223
- opik/evaluation/engine/helpers.py +8 -7
- opik/evaluation/engine/metrics_evaluator.py +237 -0
- opik/evaluation/evaluation_result.py +35 -1
- opik/evaluation/evaluator.py +309 -23
- opik/evaluation/models/litellm/util.py +78 -6
- opik/evaluation/report.py +14 -2
- opik/evaluation/rest_operations.py +6 -9
- opik/evaluation/test_case.py +2 -2
- opik/evaluation/types.py +9 -1
- opik/exceptions.py +17 -0
- opik/id_helpers.py +18 -0
- opik/integrations/adk/helpers.py +16 -7
- opik/integrations/adk/legacy_opik_tracer.py +7 -4
- opik/integrations/adk/opik_tracer.py +3 -1
- opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
- opik/integrations/dspy/callback.py +1 -4
- opik/integrations/haystack/opik_connector.py +2 -2
- opik/integrations/haystack/opik_tracer.py +2 -4
- opik/integrations/langchain/opik_tracer.py +1 -4
- opik/integrations/llama_index/callback.py +2 -4
- opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
- opik/integrations/openai/opik_tracker.py +1 -1
- opik/opik_context.py +7 -7
- opik/rest_api/__init__.py +123 -11
- opik/rest_api/dashboards/client.py +65 -2
- opik/rest_api/dashboards/raw_client.py +82 -0
- opik/rest_api/datasets/client.py +441 -2
- opik/rest_api/datasets/raw_client.py +1225 -505
- opik/rest_api/experiments/client.py +30 -2
- opik/rest_api/experiments/raw_client.py +26 -0
- opik/rest_api/optimizations/client.py +302 -0
- opik/rest_api/optimizations/raw_client.py +463 -0
- opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
- opik/rest_api/prompts/__init__.py +2 -2
- opik/rest_api/prompts/client.py +34 -4
- opik/rest_api/prompts/raw_client.py +32 -2
- opik/rest_api/prompts/types/__init__.py +3 -1
- opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
- opik/rest_api/traces/client.py +6 -6
- opik/rest_api/traces/raw_client.py +4 -4
- opik/rest_api/types/__init__.py +121 -11
- opik/rest_api/types/aggregation_data.py +1 -0
- opik/rest_api/types/automation_rule_evaluator.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
- opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
- opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
- opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
- opik/rest_api/types/dashboard_page_public.py +1 -0
- opik/rest_api/types/dataset.py +2 -0
- opik/rest_api/types/dataset_item.py +1 -0
- opik/rest_api/types/dataset_item_compare.py +1 -0
- opik/rest_api/types/dataset_item_page_compare.py +1 -0
- opik/rest_api/types/dataset_item_page_public.py +1 -0
- opik/rest_api/types/dataset_item_public.py +1 -0
- opik/rest_api/types/dataset_public.py +2 -0
- opik/rest_api/types/dataset_public_status.py +5 -0
- opik/rest_api/types/dataset_status.py +5 -0
- opik/rest_api/types/dataset_version_diff.py +22 -0
- opik/rest_api/types/dataset_version_diff_stats.py +24 -0
- opik/rest_api/types/dataset_version_page_public.py +23 -0
- opik/rest_api/types/dataset_version_public.py +49 -0
- opik/rest_api/types/experiment.py +2 -0
- opik/rest_api/types/experiment_public.py +2 -0
- opik/rest_api/types/experiment_score.py +20 -0
- opik/rest_api/types/experiment_score_public.py +20 -0
- opik/rest_api/types/experiment_score_write.py +20 -0
- opik/rest_api/types/feedback_score_public.py +4 -0
- opik/rest_api/types/optimization.py +2 -0
- opik/rest_api/types/optimization_public.py +2 -0
- opik/rest_api/types/optimization_public_status.py +3 -1
- opik/rest_api/types/optimization_status.py +3 -1
- opik/rest_api/types/optimization_studio_config.py +27 -0
- opik/rest_api/types/optimization_studio_config_public.py +27 -0
- opik/rest_api/types/optimization_studio_config_write.py +27 -0
- opik/rest_api/types/optimization_studio_log.py +22 -0
- opik/rest_api/types/optimization_write.py +2 -0
- opik/rest_api/types/optimization_write_status.py +3 -1
- opik/rest_api/types/prompt.py +6 -0
- opik/rest_api/types/prompt_detail.py +6 -0
- opik/rest_api/types/prompt_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_public.py +6 -0
- opik/rest_api/types/prompt_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_template_structure.py +5 -0
- opik/rest_api/types/prompt_version.py +2 -0
- opik/rest_api/types/prompt_version_detail.py +2 -0
- opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_public.py +2 -0
- opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_template_structure.py +5 -0
- opik/rest_api/types/score_name.py +1 -0
- opik/rest_api/types/service_toggles_config.py +5 -0
- opik/rest_api/types/span_filter.py +23 -0
- opik/rest_api/types/span_filter_operator.py +21 -0
- opik/rest_api/types/span_filter_write.py +23 -0
- opik/rest_api/types/span_filter_write_operator.py +21 -0
- opik/rest_api/types/span_llm_as_judge_code.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
- opik/rest_api/types/studio_evaluation.py +20 -0
- opik/rest_api/types/studio_evaluation_public.py +20 -0
- opik/rest_api/types/studio_evaluation_write.py +20 -0
- opik/rest_api/types/studio_llm_model.py +21 -0
- opik/rest_api/types/studio_llm_model_public.py +21 -0
- opik/rest_api/types/studio_llm_model_write.py +21 -0
- opik/rest_api/types/studio_message.py +20 -0
- opik/rest_api/types/studio_message_public.py +20 -0
- opik/rest_api/types/studio_message_write.py +20 -0
- opik/rest_api/types/studio_metric.py +21 -0
- opik/rest_api/types/studio_metric_public.py +21 -0
- opik/rest_api/types/studio_metric_write.py +21 -0
- opik/rest_api/types/studio_optimizer.py +21 -0
- opik/rest_api/types/studio_optimizer_public.py +21 -0
- opik/rest_api/types/studio_optimizer_write.py +21 -0
- opik/rest_api/types/studio_prompt.py +20 -0
- opik/rest_api/types/studio_prompt_public.py +20 -0
- opik/rest_api/types/studio_prompt_write.py +20 -0
- opik/rest_api/types/trace.py +6 -0
- opik/rest_api/types/trace_public.py +6 -0
- opik/rest_api/types/trace_thread_filter_write.py +23 -0
- opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
- opik/rest_api/types/value_entry.py +2 -0
- opik/rest_api/types/value_entry_compare.py +2 -0
- opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
- opik/rest_api/types/value_entry_public.py +2 -0
- opik/synchronization.py +5 -6
- opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/METADATA +2 -1
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/RECORD +177 -119
- opik/api_objects/prompt/chat_prompt_template.py +0 -200
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for prompts.
|
|
3
|
+
|
|
4
|
+
Defines abstract interface that both string and chat prompt variants must implement.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from . import types as prompt_types
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BasePrompt(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Abstract base class for prompts (string and chat).
|
|
16
|
+
|
|
17
|
+
All prompt implementations must provide common properties and methods
|
|
18
|
+
for interacting with the backend API.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
"""The name of the prompt."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def commit(self) -> Optional[str]:
|
|
30
|
+
"""The commit hash of the prompt version."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def metadata(self) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""The metadata dictionary associated with the prompt."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def type(self) -> prompt_types.PromptType:
|
|
42
|
+
"""The prompt type (MUSTACHE or JINJA2)."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
# Internal API fields for backend synchronization
|
|
46
|
+
__internal_api__prompt_id__: str
|
|
47
|
+
__internal_api__version_id__: str
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def format(self, *args: Any, **kwargs: Any) -> Any:
|
|
51
|
+
"""
|
|
52
|
+
Format the prompt with the provided variables.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Formatted output. Type depends on the implementation:
|
|
56
|
+
- Prompt returns str
|
|
57
|
+
- ChatPrompt returns List[Dict[str, MessageContent]]
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def __internal_api__to_info_dict__(self) -> Dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Convert the prompt to an info dictionary for serialization.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dictionary containing prompt metadata and version information.
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for prompt templates.
|
|
3
|
+
|
|
4
|
+
Defines abstract interface that both string and chat template variants must implement.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BasePromptTemplate(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract base class for prompt templates (string and chat).
|
|
14
|
+
|
|
15
|
+
All prompt template implementations must provide a format method
|
|
16
|
+
that takes variables and returns formatted output.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def format(self, *args: Any, **kwargs: Any) -> Any:
|
|
21
|
+
"""
|
|
22
|
+
Format the template with the provided variables.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted output. Type depends on the implementation:
|
|
26
|
+
- PromptTemplate returns str
|
|
27
|
+
- ChatPromptTemplate returns List[Dict[str, MessageContent]]
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Empty - all exports handled by parent __init__.py
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
from opik.rest_api import types as rest_api_types
|
|
7
|
+
from . import chat_prompt_template
|
|
8
|
+
from .. import client as prompt_client
|
|
9
|
+
from .. import types as prompt_types
|
|
10
|
+
from .. import base_prompt
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChatPrompt(base_prompt.BasePrompt):
|
|
14
|
+
"""
|
|
15
|
+
ChatPrompt class represents a chat-style prompt with a name, message array template and commit hash.
|
|
16
|
+
Similar to Prompt but uses a list of chat messages instead of a string template.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
name: str,
|
|
22
|
+
messages: List[Dict[str, prompt_types.MessageContent]],
|
|
23
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
24
|
+
type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
|
|
25
|
+
validate_placeholders: bool = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Initializes a new instance of the ChatPrompt class.
|
|
29
|
+
Creates a new chat prompt using the opik client and sets the initial state.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
name: The name for the prompt.
|
|
33
|
+
messages: List of message dictionaries with 'role' and 'content' fields.
|
|
34
|
+
metadata: Optional metadata to be included in the prompt.
|
|
35
|
+
type: The template type (MUSTACHE or JINJA2).
|
|
36
|
+
validate_placeholders: Whether to validate template placeholders.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
PromptTemplateStructureMismatch: If a text prompt with the same name already exists (template structure is immutable).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
self._chat_template = chat_prompt_template.ChatPromptTemplate(
|
|
43
|
+
messages=messages,
|
|
44
|
+
template_type=type,
|
|
45
|
+
validate_placeholders=validate_placeholders,
|
|
46
|
+
)
|
|
47
|
+
self._name = name
|
|
48
|
+
self._metadata = metadata
|
|
49
|
+
self._type = type
|
|
50
|
+
self._messages = messages
|
|
51
|
+
self._commit: Optional[str] = None
|
|
52
|
+
self.__internal_api__prompt_id__: str
|
|
53
|
+
self.__internal_api__version_id__: str
|
|
54
|
+
|
|
55
|
+
self._sync_with_backend()
|
|
56
|
+
|
|
57
|
+
def _sync_with_backend(self) -> None:
|
|
58
|
+
from opik.api_objects import opik_client
|
|
59
|
+
|
|
60
|
+
opik_client_ = opik_client.get_client_cached()
|
|
61
|
+
prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)
|
|
62
|
+
|
|
63
|
+
# Convert messages array to JSON string for backend storage
|
|
64
|
+
messages_str = json.dumps(self._messages)
|
|
65
|
+
|
|
66
|
+
prompt_version = prompt_client_.create_prompt(
|
|
67
|
+
name=self._name,
|
|
68
|
+
prompt=messages_str,
|
|
69
|
+
metadata=self._metadata,
|
|
70
|
+
type=self._type,
|
|
71
|
+
template_structure="chat",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self._commit = prompt_version.commit
|
|
75
|
+
self.__internal_api__prompt_id__ = prompt_version.prompt_id
|
|
76
|
+
self.__internal_api__version_id__ = prompt_version.id
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
@override
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
"""The name of the prompt."""
|
|
82
|
+
return self._name
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def template(self) -> List[Dict[str, prompt_types.MessageContent]]:
|
|
86
|
+
"""The chat messages template."""
|
|
87
|
+
return copy.deepcopy(self._messages)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
@override
|
|
91
|
+
def commit(self) -> Optional[str]:
|
|
92
|
+
"""The commit hash of the prompt."""
|
|
93
|
+
return self._commit
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
@override
|
|
97
|
+
def metadata(self) -> Optional[Dict[str, Any]]:
|
|
98
|
+
"""The metadata dictionary associated with the prompt"""
|
|
99
|
+
return copy.deepcopy(self._metadata)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
@override
|
|
103
|
+
def type(self) -> prompt_types.PromptType:
|
|
104
|
+
"""The prompt type of the prompt."""
|
|
105
|
+
return self._type
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
def format(
|
|
109
|
+
self,
|
|
110
|
+
variables: Dict[str, Any],
|
|
111
|
+
supported_modalities: Optional[prompt_types.SupportedModalities] = None,
|
|
112
|
+
) -> List[Dict[str, prompt_types.MessageContent]]:
|
|
113
|
+
"""
|
|
114
|
+
Renders the chat template with provided variables.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
variables: Dictionary of variables to substitute in the template.
|
|
118
|
+
supported_modalities: Optional dictionary specifying which modalities are supported
|
|
119
|
+
by the target model. Keys are modality names ("vision" or "video") and values
|
|
120
|
+
are booleans indicating support. When a modality is not supported (False or not
|
|
121
|
+
specified), structured content parts (e.g., images, videos) are replaced with
|
|
122
|
+
text placeholders like "<<<image>>>" or "<<<video>>>". When supported (True),
|
|
123
|
+
the structured content is preserved as-is.
|
|
124
|
+
Example: {"vision": True, "video": False}
|
|
125
|
+
|
|
126
|
+
If not specified, all modalities default to SUPPORTED. Example: {"vision": True, "video": False}
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A list of rendered message dictionaries with variables substituted and multimodal
|
|
130
|
+
content either preserved or replaced with placeholders based on supported_modalities.
|
|
131
|
+
"""
|
|
132
|
+
if supported_modalities is None:
|
|
133
|
+
supported_modalities = {
|
|
134
|
+
"vision": True,
|
|
135
|
+
"video": True,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return self._chat_template.format(
|
|
139
|
+
variables=variables, supported_modalities=supported_modalities
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@override
|
|
143
|
+
def __internal_api__to_info_dict__(self) -> Dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Convert the prompt to an info dictionary for serialization.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dictionary containing prompt metadata and version information.
|
|
149
|
+
"""
|
|
150
|
+
info_dict: Dict[str, Any] = {
|
|
151
|
+
"name": self.name,
|
|
152
|
+
"version": {
|
|
153
|
+
"template": self.template,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if self.__internal_api__prompt_id__ is not None:
|
|
158
|
+
info_dict["id"] = self.__internal_api__prompt_id__
|
|
159
|
+
|
|
160
|
+
if self.commit is not None:
|
|
161
|
+
info_dict["version"]["commit"] = self.commit
|
|
162
|
+
|
|
163
|
+
if self.__internal_api__version_id__ is not None:
|
|
164
|
+
info_dict["version"]["id"] = self.__internal_api__version_id__
|
|
165
|
+
|
|
166
|
+
return info_dict
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_fern_prompt_version(
|
|
170
|
+
cls,
|
|
171
|
+
name: str,
|
|
172
|
+
prompt_version: rest_api_types.PromptVersionDetail,
|
|
173
|
+
) -> "ChatPrompt":
|
|
174
|
+
# will not call __init__ to avoid API calls, create new instance with __new__
|
|
175
|
+
chat_prompt = cls.__new__(cls)
|
|
176
|
+
|
|
177
|
+
chat_prompt.__internal_api__version_id__ = prompt_version.id
|
|
178
|
+
chat_prompt.__internal_api__prompt_id__ = prompt_version.prompt_id
|
|
179
|
+
|
|
180
|
+
chat_prompt._name = name
|
|
181
|
+
|
|
182
|
+
# Parse messages from JSON string
|
|
183
|
+
messages = json.loads(prompt_version.template)
|
|
184
|
+
chat_prompt._messages = messages
|
|
185
|
+
chat_prompt._chat_template = chat_prompt_template.ChatPromptTemplate(
|
|
186
|
+
messages=messages,
|
|
187
|
+
template_type=prompt_types.PromptType(prompt_version.type)
|
|
188
|
+
or prompt_types.PromptType.MUSTACHE,
|
|
189
|
+
)
|
|
190
|
+
chat_prompt._commit = prompt_version.commit
|
|
191
|
+
chat_prompt._metadata = prompt_version.metadata
|
|
192
|
+
chat_prompt._type = prompt_version.type
|
|
193
|
+
return chat_prompt
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for rendering chat-style prompts with multimodal content.
|
|
3
|
+
|
|
4
|
+
The template mirrors :class:`PromptTemplate` but works on a list of OpenAI-like
|
|
5
|
+
messages. Rendering is handled by a registry of part renderers so additional
|
|
6
|
+
modalities can be plugged in without changing the core implementation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, List, Optional, Set, Union, cast
|
|
11
|
+
from typing_extensions import override
|
|
12
|
+
|
|
13
|
+
import opik.exceptions as exceptions
|
|
14
|
+
|
|
15
|
+
from ..text import prompt_template
|
|
16
|
+
from .. import types as prompt_types
|
|
17
|
+
from .. import base_prompt_template
|
|
18
|
+
from . import content_renderer_registry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChatPromptTemplate(base_prompt_template.BasePromptTemplate):
|
|
22
|
+
"""
|
|
23
|
+
Prompt template for chat-style prompts with multimodal content.
|
|
24
|
+
|
|
25
|
+
This class handles OpenAI-like message formats with support for text, images,
|
|
26
|
+
and video content. Templates use Mustache syntax (``{{variable}}``) by default
|
|
27
|
+
for variable substitution.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
messages: List of message dictionaries with "role" and "content" keys.
|
|
31
|
+
Content can be a string or a list of content parts (text, image_url, video_url).
|
|
32
|
+
template_type: Template syntax to use for variable substitution.
|
|
33
|
+
Defaults to :class:`~opik.api_objects.prompt.types.PromptType.MUSTACHE`.
|
|
34
|
+
registry: Custom content renderer registry. If None, uses the default registry
|
|
35
|
+
with built-in support for text, image, and video rendering.
|
|
36
|
+
validate_placeholders: If True, raises an exception when template placeholders
|
|
37
|
+
don't match the provided variables during formatting.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
Simple text-based chat template with variables::
|
|
41
|
+
|
|
42
|
+
template = ChatPromptTemplate([
|
|
43
|
+
{
|
|
44
|
+
"role": "system",
|
|
45
|
+
"content": "You are a helpful assistant specializing in {{domain}}."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"role": "user",
|
|
49
|
+
"content": "Explain {{topic}} in simple terms."
|
|
50
|
+
}
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
messages = template.format({
|
|
54
|
+
"domain": "physics",
|
|
55
|
+
"topic": "quantum entanglement"
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
Multimodal template with image content::
|
|
59
|
+
|
|
60
|
+
template = ChatPromptTemplate([
|
|
61
|
+
{
|
|
62
|
+
"role": "user",
|
|
63
|
+
"content": [
|
|
64
|
+
{
|
|
65
|
+
"type": "text",
|
|
66
|
+
"text": "What is in this image from {{location}}?"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "image_url",
|
|
70
|
+
"image_url": {
|
|
71
|
+
"url": "{{image_path}}",
|
|
72
|
+
"detail": "high"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
messages = template.format({
|
|
80
|
+
"location": "Paris",
|
|
81
|
+
"image_path": "https://example.com/photo.jpg"
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
Template with video content::
|
|
85
|
+
|
|
86
|
+
template = ChatPromptTemplate([
|
|
87
|
+
{
|
|
88
|
+
"role": "user",
|
|
89
|
+
"content": [
|
|
90
|
+
{
|
|
91
|
+
"type": "text",
|
|
92
|
+
"text": "Analyze this video: {{description}}"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"type": "video_url",
|
|
96
|
+
"video_url": {
|
|
97
|
+
"url": "{{video_url}}",
|
|
98
|
+
"mime_type": "video/mp4"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
messages = template.format({
|
|
106
|
+
"description": "traffic analysis",
|
|
107
|
+
"video_url": "https://example.com/traffic.mp4"
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
When formatting with unsupported modalities, the content is replaced with
|
|
111
|
+
placeholders::
|
|
112
|
+
|
|
113
|
+
messages = template.format(
|
|
114
|
+
{"video_url": "https://example.com/video.mp4"},
|
|
115
|
+
supported_modalities={"text"} # video not supported
|
|
116
|
+
)
|
|
117
|
+
# Returns: [{"role": "user", "content": "Analyze this video\\n<<<video>>><<</video>>>"}]
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
messages: List[Dict[str, prompt_types.MessageContent]],
|
|
123
|
+
template_type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
|
|
124
|
+
*,
|
|
125
|
+
registry: Optional[
|
|
126
|
+
content_renderer_registry.ChatContentRendererRegistry
|
|
127
|
+
] = None,
|
|
128
|
+
validate_placeholders: bool = False,
|
|
129
|
+
) -> None:
|
|
130
|
+
self._messages = messages
|
|
131
|
+
self._template_type = template_type
|
|
132
|
+
self._registry = (
|
|
133
|
+
registry or content_renderer_registry.DEFAULT_CHAT_RENDERER_REGISTRY
|
|
134
|
+
)
|
|
135
|
+
self._validate_placeholders = validate_placeholders
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def messages(self) -> List[Dict[str, prompt_types.MessageContent]]:
|
|
139
|
+
return self._messages
|
|
140
|
+
|
|
141
|
+
def required_modalities(self) -> prompt_types.ModalitySet:
|
|
142
|
+
"""
|
|
143
|
+
Return the union of modalities referenced across all template messages.
|
|
144
|
+
"""
|
|
145
|
+
required: prompt_types.ModalitySet = set()
|
|
146
|
+
for message in self._messages:
|
|
147
|
+
content = cast(prompt_types.MessageContent, message.get("content", ""))
|
|
148
|
+
required.update(self._registry.infer_modalities(content))
|
|
149
|
+
return required
|
|
150
|
+
|
|
151
|
+
def _extract_placeholders(self, template_type: prompt_types.PromptType) -> Set[str]:
|
|
152
|
+
"""
|
|
153
|
+
Extract all placeholders from all messages.
|
|
154
|
+
"""
|
|
155
|
+
placeholders: Set[str] = set()
|
|
156
|
+
for message in self._messages:
|
|
157
|
+
content = cast(prompt_types.MessageContent, message.get("content", ""))
|
|
158
|
+
if isinstance(content, str):
|
|
159
|
+
placeholders.update(
|
|
160
|
+
_extract_placeholders_from_string(content, template_type)
|
|
161
|
+
)
|
|
162
|
+
elif isinstance(content, list):
|
|
163
|
+
for part in content:
|
|
164
|
+
if not isinstance(part, dict):
|
|
165
|
+
continue
|
|
166
|
+
# Extract from text parts
|
|
167
|
+
if "text" in part:
|
|
168
|
+
text = str(part["text"])
|
|
169
|
+
placeholders.update(
|
|
170
|
+
_extract_placeholders_from_string(text, template_type)
|
|
171
|
+
)
|
|
172
|
+
# Extract from image_url parts
|
|
173
|
+
if "image_url" in part and isinstance(part["image_url"], dict):
|
|
174
|
+
url = str(part["image_url"].get("url", ""))
|
|
175
|
+
placeholders.update(
|
|
176
|
+
_extract_placeholders_from_string(url, template_type)
|
|
177
|
+
)
|
|
178
|
+
return placeholders
|
|
179
|
+
|
|
180
|
+
@override
|
|
181
|
+
def format(
|
|
182
|
+
self,
|
|
183
|
+
variables: Dict[str, Any],
|
|
184
|
+
supported_modalities: Optional[prompt_types.SupportedModalities] = None,
|
|
185
|
+
*,
|
|
186
|
+
template_type: Optional[Union[str, prompt_types.PromptType]] = None,
|
|
187
|
+
) -> List[Dict[str, prompt_types.MessageContent]]:
|
|
188
|
+
"""
|
|
189
|
+
Render the template messages with the provided variables.
|
|
190
|
+
|
|
191
|
+
When a part declares a modality that is not supported, the registry replaces
|
|
192
|
+
it with the configured placeholder pair (for example ``<<<image>>>``) so
|
|
193
|
+
downstream consumers receive a textual anchor while unsupported structured
|
|
194
|
+
content is gracefully elided.
|
|
195
|
+
"""
|
|
196
|
+
resolved_template_type = self._registry.normalize_template_type(
|
|
197
|
+
template_type or self._template_type
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Validate placeholders if enabled and using Mustache templates
|
|
201
|
+
if (
|
|
202
|
+
self._validate_placeholders
|
|
203
|
+
and resolved_template_type == prompt_types.PromptType.MUSTACHE
|
|
204
|
+
):
|
|
205
|
+
placeholders = self._extract_placeholders(resolved_template_type)
|
|
206
|
+
variables_keys: Set[str] = set(variables.keys())
|
|
207
|
+
|
|
208
|
+
if variables_keys != placeholders:
|
|
209
|
+
raise exceptions.PromptPlaceholdersDontMatchFormatArguments(
|
|
210
|
+
prompt_placeholders=placeholders, format_arguments=variables_keys
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
rendered_messages: List[Dict[str, prompt_types.MessageContent]] = []
|
|
214
|
+
|
|
215
|
+
for message in self._messages:
|
|
216
|
+
role = message.get("role")
|
|
217
|
+
if role is None:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
content = cast(prompt_types.MessageContent, message.get("content", ""))
|
|
221
|
+
rendered_content: prompt_types.MessageContent
|
|
222
|
+
if isinstance(content, str):
|
|
223
|
+
rendered_content = _render_template_string(
|
|
224
|
+
content, variables, resolved_template_type
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
rendered_content = self._registry.render_content(
|
|
228
|
+
content=cast(prompt_types.MessageContent, content),
|
|
229
|
+
variables=variables,
|
|
230
|
+
template_type=resolved_template_type,
|
|
231
|
+
supported_modalities=supported_modalities,
|
|
232
|
+
)
|
|
233
|
+
rendered_messages.append(
|
|
234
|
+
{
|
|
235
|
+
"role": role,
|
|
236
|
+
"content": rendered_content,
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return rendered_messages
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _render_template_string(
|
|
244
|
+
template: str,
|
|
245
|
+
variables: Dict[str, Any],
|
|
246
|
+
template_type: prompt_types.PromptType,
|
|
247
|
+
) -> str:
|
|
248
|
+
if not template:
|
|
249
|
+
return ""
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
return prompt_template.PromptTemplate(
|
|
253
|
+
template,
|
|
254
|
+
validate_placeholders=False,
|
|
255
|
+
type=template_type,
|
|
256
|
+
).format(**variables)
|
|
257
|
+
except Exception:
|
|
258
|
+
# Fall back to the raw template if formatting fails so evaluation keeps running.
|
|
259
|
+
return template
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def render_text_part(
|
|
263
|
+
part: prompt_types.ContentPart,
|
|
264
|
+
variables: Dict[str, Any],
|
|
265
|
+
template_type: prompt_types.PromptType,
|
|
266
|
+
) -> Optional[prompt_types.ContentPart]:
|
|
267
|
+
text_template = part.get("text", "")
|
|
268
|
+
rendered_text = _render_template_string(text_template, variables, template_type)
|
|
269
|
+
return {"type": "text", "text": rendered_text}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def render_image_url_part(
|
|
273
|
+
part: prompt_types.ContentPart,
|
|
274
|
+
variables: Dict[str, Any],
|
|
275
|
+
template_type: prompt_types.PromptType,
|
|
276
|
+
) -> Optional[prompt_types.ContentPart]:
|
|
277
|
+
image_dict = part.get("image_url", {})
|
|
278
|
+
if not isinstance(image_dict, dict):
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
url_template = image_dict.get("url", "")
|
|
282
|
+
rendered_url = _render_template_string(url_template, variables, template_type)
|
|
283
|
+
if not rendered_url:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
rendered_image: Dict[str, Any] = {"url": rendered_url}
|
|
287
|
+
if "detail" in image_dict:
|
|
288
|
+
rendered_image["detail"] = image_dict["detail"]
|
|
289
|
+
|
|
290
|
+
return {"type": "image_url", "image_url": rendered_image}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def render_video_url_part(
|
|
294
|
+
part: prompt_types.ContentPart,
|
|
295
|
+
variables: Dict[str, Any],
|
|
296
|
+
template_type: prompt_types.PromptType,
|
|
297
|
+
) -> Optional[prompt_types.ContentPart]:
|
|
298
|
+
"""
|
|
299
|
+
Render a ``video_url`` part and preserve optional metadata.
|
|
300
|
+
|
|
301
|
+
In addition to the rendered ``url`` we keep:
|
|
302
|
+
|
|
303
|
+
- ``detail``: free-form provider hints (mirrors the image renderer semantics).
|
|
304
|
+
- ``mime_type``: the content type callers expect the downstream model to load.
|
|
305
|
+
- ``duration``: client-supplied duration in seconds to give hosts extra context.
|
|
306
|
+
- ``format``: a short format label (``mp4``, ``webm``, etc.) when known.
|
|
307
|
+
"""
|
|
308
|
+
video_dict = part.get("video_url", {})
|
|
309
|
+
if not isinstance(video_dict, dict):
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
url_template = video_dict.get("url", "")
|
|
313
|
+
rendered_url = _render_template_string(url_template, variables, template_type)
|
|
314
|
+
if not rendered_url:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
rendered_video: Dict[str, Any] = {"url": rendered_url}
|
|
318
|
+
for key in ("detail", "mime_type", "duration", "format"):
|
|
319
|
+
if key in video_dict:
|
|
320
|
+
rendered_video[key] = video_dict[key]
|
|
321
|
+
|
|
322
|
+
return {"type": "video_url", "video_url": rendered_video}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _extract_placeholders_from_string(
|
|
326
|
+
text: str, template_type: prompt_types.PromptType
|
|
327
|
+
) -> Set[str]:
|
|
328
|
+
"""
|
|
329
|
+
Extract placeholder keys from a string template.
|
|
330
|
+
Only supports Mustache templates for now.
|
|
331
|
+
"""
|
|
332
|
+
if template_type == prompt_types.PromptType.MUSTACHE:
|
|
333
|
+
pattern = r"\{\{(.*?)\}\}"
|
|
334
|
+
return set(re.findall(pattern, text))
|
|
335
|
+
return set()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
content_renderer_registry.register_default_chat_part_renderer("text", render_text_part)
|
|
339
|
+
content_renderer_registry.register_default_chat_part_renderer(
|
|
340
|
+
"image_url",
|
|
341
|
+
render_image_url_part,
|
|
342
|
+
modality="vision",
|
|
343
|
+
placeholder=("<<<image>>>", "<<</image>>>"),
|
|
344
|
+
)
|
|
345
|
+
content_renderer_registry.register_default_chat_part_renderer(
|
|
346
|
+
"video_url",
|
|
347
|
+
render_video_url_part,
|
|
348
|
+
modality="video",
|
|
349
|
+
placeholder=("<<<video>>>", "<<</video>>>"),
|
|
350
|
+
)
|