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.
Files changed (178) hide show
  1. opik/__init__.py +10 -3
  2. opik/api_objects/dataset/rest_operations.py +2 -0
  3. opik/api_objects/experiment/experiment.py +31 -5
  4. opik/api_objects/experiment/helpers.py +34 -10
  5. opik/api_objects/local_recording.py +8 -3
  6. opik/api_objects/opik_client.py +218 -46
  7. opik/api_objects/opik_query_language.py +9 -0
  8. opik/api_objects/prompt/__init__.py +11 -3
  9. opik/api_objects/prompt/base_prompt.py +69 -0
  10. opik/api_objects/prompt/base_prompt_template.py +29 -0
  11. opik/api_objects/prompt/chat/__init__.py +1 -0
  12. opik/api_objects/prompt/chat/chat_prompt.py +193 -0
  13. opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
  14. opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +31 -34
  15. opik/api_objects/prompt/client.py +101 -30
  16. opik/api_objects/prompt/text/__init__.py +1 -0
  17. opik/api_objects/prompt/{prompt.py → text/prompt.py} +55 -32
  18. opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +8 -5
  19. opik/cli/export.py +6 -2
  20. opik/config.py +0 -5
  21. opik/decorator/base_track_decorator.py +37 -40
  22. opik/evaluation/__init__.py +13 -2
  23. opik/evaluation/engine/engine.py +195 -223
  24. opik/evaluation/engine/helpers.py +8 -7
  25. opik/evaluation/engine/metrics_evaluator.py +237 -0
  26. opik/evaluation/evaluation_result.py +35 -1
  27. opik/evaluation/evaluator.py +309 -23
  28. opik/evaluation/models/litellm/util.py +78 -6
  29. opik/evaluation/report.py +14 -2
  30. opik/evaluation/rest_operations.py +6 -9
  31. opik/evaluation/test_case.py +2 -2
  32. opik/evaluation/types.py +9 -1
  33. opik/exceptions.py +17 -0
  34. opik/id_helpers.py +18 -0
  35. opik/integrations/adk/helpers.py +16 -7
  36. opik/integrations/adk/legacy_opik_tracer.py +7 -4
  37. opik/integrations/adk/opik_tracer.py +3 -1
  38. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
  39. opik/integrations/dspy/callback.py +1 -4
  40. opik/integrations/haystack/opik_connector.py +2 -2
  41. opik/integrations/haystack/opik_tracer.py +2 -4
  42. opik/integrations/langchain/opik_tracer.py +1 -4
  43. opik/integrations/llama_index/callback.py +2 -4
  44. opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
  45. opik/integrations/openai/opik_tracker.py +1 -1
  46. opik/opik_context.py +7 -7
  47. opik/rest_api/__init__.py +123 -11
  48. opik/rest_api/dashboards/client.py +65 -2
  49. opik/rest_api/dashboards/raw_client.py +82 -0
  50. opik/rest_api/datasets/client.py +441 -2
  51. opik/rest_api/datasets/raw_client.py +1225 -505
  52. opik/rest_api/experiments/client.py +30 -2
  53. opik/rest_api/experiments/raw_client.py +26 -0
  54. opik/rest_api/optimizations/client.py +302 -0
  55. opik/rest_api/optimizations/raw_client.py +463 -0
  56. opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
  57. opik/rest_api/prompts/__init__.py +2 -2
  58. opik/rest_api/prompts/client.py +34 -4
  59. opik/rest_api/prompts/raw_client.py +32 -2
  60. opik/rest_api/prompts/types/__init__.py +3 -1
  61. opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
  62. opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
  63. opik/rest_api/traces/client.py +6 -6
  64. opik/rest_api/traces/raw_client.py +4 -4
  65. opik/rest_api/types/__init__.py +121 -11
  66. opik/rest_api/types/aggregation_data.py +1 -0
  67. opik/rest_api/types/automation_rule_evaluator.py +23 -1
  68. opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
  69. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
  70. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
  71. opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
  72. opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
  73. opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
  74. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
  75. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
  76. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
  77. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
  78. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
  79. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
  80. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
  81. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
  82. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
  83. opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
  84. opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
  85. opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
  86. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
  87. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
  88. opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
  89. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
  90. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
  91. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
  92. opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
  93. opik/rest_api/types/dashboard_page_public.py +1 -0
  94. opik/rest_api/types/dataset.py +2 -0
  95. opik/rest_api/types/dataset_item.py +1 -0
  96. opik/rest_api/types/dataset_item_compare.py +1 -0
  97. opik/rest_api/types/dataset_item_page_compare.py +1 -0
  98. opik/rest_api/types/dataset_item_page_public.py +1 -0
  99. opik/rest_api/types/dataset_item_public.py +1 -0
  100. opik/rest_api/types/dataset_public.py +2 -0
  101. opik/rest_api/types/dataset_public_status.py +5 -0
  102. opik/rest_api/types/dataset_status.py +5 -0
  103. opik/rest_api/types/dataset_version_diff.py +22 -0
  104. opik/rest_api/types/dataset_version_diff_stats.py +24 -0
  105. opik/rest_api/types/dataset_version_page_public.py +23 -0
  106. opik/rest_api/types/dataset_version_public.py +49 -0
  107. opik/rest_api/types/experiment.py +2 -0
  108. opik/rest_api/types/experiment_public.py +2 -0
  109. opik/rest_api/types/experiment_score.py +20 -0
  110. opik/rest_api/types/experiment_score_public.py +20 -0
  111. opik/rest_api/types/experiment_score_write.py +20 -0
  112. opik/rest_api/types/feedback_score_public.py +4 -0
  113. opik/rest_api/types/optimization.py +2 -0
  114. opik/rest_api/types/optimization_public.py +2 -0
  115. opik/rest_api/types/optimization_public_status.py +3 -1
  116. opik/rest_api/types/optimization_status.py +3 -1
  117. opik/rest_api/types/optimization_studio_config.py +27 -0
  118. opik/rest_api/types/optimization_studio_config_public.py +27 -0
  119. opik/rest_api/types/optimization_studio_config_write.py +27 -0
  120. opik/rest_api/types/optimization_studio_log.py +22 -0
  121. opik/rest_api/types/optimization_write.py +2 -0
  122. opik/rest_api/types/optimization_write_status.py +3 -1
  123. opik/rest_api/types/prompt.py +6 -0
  124. opik/rest_api/types/prompt_detail.py +6 -0
  125. opik/rest_api/types/prompt_detail_template_structure.py +5 -0
  126. opik/rest_api/types/prompt_public.py +6 -0
  127. opik/rest_api/types/prompt_public_template_structure.py +5 -0
  128. opik/rest_api/types/prompt_template_structure.py +5 -0
  129. opik/rest_api/types/prompt_version.py +2 -0
  130. opik/rest_api/types/prompt_version_detail.py +2 -0
  131. opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
  132. opik/rest_api/types/prompt_version_public.py +2 -0
  133. opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
  134. opik/rest_api/types/prompt_version_template_structure.py +5 -0
  135. opik/rest_api/types/score_name.py +1 -0
  136. opik/rest_api/types/service_toggles_config.py +5 -0
  137. opik/rest_api/types/span_filter.py +23 -0
  138. opik/rest_api/types/span_filter_operator.py +21 -0
  139. opik/rest_api/types/span_filter_write.py +23 -0
  140. opik/rest_api/types/span_filter_write_operator.py +21 -0
  141. opik/rest_api/types/span_llm_as_judge_code.py +27 -0
  142. opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
  143. opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
  144. opik/rest_api/types/studio_evaluation.py +20 -0
  145. opik/rest_api/types/studio_evaluation_public.py +20 -0
  146. opik/rest_api/types/studio_evaluation_write.py +20 -0
  147. opik/rest_api/types/studio_llm_model.py +21 -0
  148. opik/rest_api/types/studio_llm_model_public.py +21 -0
  149. opik/rest_api/types/studio_llm_model_write.py +21 -0
  150. opik/rest_api/types/studio_message.py +20 -0
  151. opik/rest_api/types/studio_message_public.py +20 -0
  152. opik/rest_api/types/studio_message_write.py +20 -0
  153. opik/rest_api/types/studio_metric.py +21 -0
  154. opik/rest_api/types/studio_metric_public.py +21 -0
  155. opik/rest_api/types/studio_metric_write.py +21 -0
  156. opik/rest_api/types/studio_optimizer.py +21 -0
  157. opik/rest_api/types/studio_optimizer_public.py +21 -0
  158. opik/rest_api/types/studio_optimizer_write.py +21 -0
  159. opik/rest_api/types/studio_prompt.py +20 -0
  160. opik/rest_api/types/studio_prompt_public.py +20 -0
  161. opik/rest_api/types/studio_prompt_write.py +20 -0
  162. opik/rest_api/types/trace.py +6 -0
  163. opik/rest_api/types/trace_public.py +6 -0
  164. opik/rest_api/types/trace_thread_filter_write.py +23 -0
  165. opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
  166. opik/rest_api/types/value_entry.py +2 -0
  167. opik/rest_api/types/value_entry_compare.py +2 -0
  168. opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
  169. opik/rest_api/types/value_entry_public.py +2 -0
  170. opik/synchronization.py +5 -6
  171. opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
  172. {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/METADATA +2 -1
  173. {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/RECORD +177 -119
  174. opik/api_objects/prompt/chat_prompt_template.py +0 -200
  175. {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
  176. {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
  177. {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
  178. {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
+ )