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
@@ -1,15 +1,6 @@
1
- from __future__ import annotations
2
-
3
1
  from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Tuple, Union
4
2
 
5
- from .types import (
6
- ContentPart,
7
- MessageContent,
8
- ModalityName,
9
- RendererFn,
10
- SupportedModalities,
11
- PromptType,
12
- )
3
+ from .. import types as prompt_types
13
4
 
14
5
 
15
6
  class ChatContentRendererRegistry:
@@ -18,9 +9,13 @@ class ChatContentRendererRegistry:
18
9
  """
19
10
 
20
11
  def __init__(self) -> None:
21
- self._part_renderers: MutableMapping[str, RendererFn] = {}
22
- self._part_modalities: MutableMapping[str, Optional[ModalityName]] = {}
23
- self._modality_placeholders: MutableMapping[ModalityName, Tuple[str, str]] = {
12
+ self._part_renderers: MutableMapping[str, prompt_types.RendererFn] = {}
13
+ self._part_modalities: MutableMapping[
14
+ str, Optional[prompt_types.ModalityName]
15
+ ] = {}
16
+ self._modality_placeholders: MutableMapping[
17
+ prompt_types.ModalityName, Tuple[str, str]
18
+ ] = {
24
19
  "vision": ("<<<image>>>", "<<</image>>>"),
25
20
  "video": ("<<<video>>>", "<<</video>>>"),
26
21
  }
@@ -30,9 +25,9 @@ class ChatContentRendererRegistry:
30
25
  def register_part_renderer(
31
26
  self,
32
27
  part_type: str,
33
- renderer: RendererFn,
28
+ renderer: prompt_types.RendererFn,
34
29
  *,
35
- modality: Optional[ModalityName] = None,
30
+ modality: Optional[prompt_types.ModalityName] = None,
36
31
  placeholder: Optional[Tuple[str, str]] = None,
37
32
  ) -> None:
38
33
  """
@@ -47,12 +42,12 @@ class ChatContentRendererRegistry:
47
42
 
48
43
  def render_content(
49
44
  self,
50
- content: MessageContent,
45
+ content: prompt_types.MessageContent,
51
46
  variables: Dict[str, Any],
52
- template_type: PromptType,
47
+ template_type: prompt_types.PromptType,
53
48
  *,
54
- supported_modalities: Optional[SupportedModalities] = None,
55
- ) -> MessageContent:
49
+ supported_modalities: Optional[prompt_types.SupportedModalities] = None,
50
+ ) -> prompt_types.MessageContent:
56
51
  if supported_modalities is None:
57
52
  modality_flags: Dict[str, bool] = {}
58
53
  else:
@@ -84,19 +79,21 @@ class ChatContentRendererRegistry:
84
79
  return rendered_parts
85
80
 
86
81
  def normalize_template_type(
87
- self, template_type: Union[str, PromptType]
88
- ) -> PromptType:
89
- if isinstance(template_type, PromptType):
82
+ self, template_type: Union[str, prompt_types.PromptType]
83
+ ) -> prompt_types.PromptType:
84
+ if isinstance(template_type, prompt_types.PromptType):
90
85
  return template_type
91
86
  try:
92
- return PromptType(template_type)
87
+ return prompt_types.PromptType(template_type)
93
88
  except ValueError:
94
- return PromptType.MUSTACHE
89
+ return prompt_types.PromptType.MUSTACHE
95
90
 
96
- def infer_modalities(self, content: MessageContent) -> set[ModalityName]:
91
+ def infer_modalities(
92
+ self, content: prompt_types.MessageContent
93
+ ) -> set[prompt_types.ModalityName]:
97
94
  if not isinstance(content, list):
98
95
  return set()
99
- modalities: set[ModalityName] = set()
96
+ modalities: set[prompt_types.ModalityName] = set()
100
97
  for part in content:
101
98
  if not isinstance(part, dict):
102
99
  continue
@@ -110,9 +107,9 @@ class ChatContentRendererRegistry:
110
107
  self,
111
108
  content: List[Any],
112
109
  variables: Dict[str, Any],
113
- template_type: PromptType,
114
- ) -> List[ContentPart]:
115
- rendered_parts: List[ContentPart] = []
110
+ template_type: prompt_types.PromptType,
111
+ ) -> List[prompt_types.ContentPart]:
112
+ rendered_parts: List[prompt_types.ContentPart] = []
116
113
  for part in content:
117
114
  if not isinstance(part, dict):
118
115
  continue
@@ -127,7 +124,7 @@ class ChatContentRendererRegistry:
127
124
  return rendered_parts
128
125
 
129
126
  def _should_flatten(
130
- self, parts: List[ContentPart], modality_flags: Mapping[str, bool]
127
+ self, parts: List[prompt_types.ContentPart], modality_flags: Mapping[str, bool]
131
128
  ) -> bool:
132
129
  for part in parts:
133
130
  modality = self._part_modalities.get(part.get("type", "").lower())
@@ -136,7 +133,7 @@ class ChatContentRendererRegistry:
136
133
  return False
137
134
 
138
135
  def _flatten_parts_to_text(
139
- self, parts: List[ContentPart], modality_flags: Mapping[str, bool]
136
+ self, parts: List[prompt_types.ContentPart], modality_flags: Mapping[str, bool]
140
137
  ) -> str:
141
138
  segments: List[str] = []
142
139
  for part in parts:
@@ -161,7 +158,7 @@ class ChatContentRendererRegistry:
161
158
  return "\n\n".join(segment for segment in segments if segment)
162
159
 
163
160
  @staticmethod
164
- def _extract_placeholder_value(part: ContentPart) -> str:
161
+ def _extract_placeholder_value(part: prompt_types.ContentPart) -> str:
165
162
  part_type = part.get("type", "").lower()
166
163
  if part_type == "image_url":
167
164
  image_dict = part.get("image_url", {})
@@ -186,9 +183,9 @@ DEFAULT_CHAT_RENDERER_REGISTRY = ChatContentRendererRegistry()
186
183
 
187
184
  def register_default_chat_part_renderer(
188
185
  part_type: str,
189
- renderer: RendererFn,
186
+ renderer: prompt_types.RendererFn,
190
187
  *,
191
- modality: Optional[ModalityName] = None,
188
+ modality: Optional[prompt_types.ModalityName] = None,
192
189
  placeholder: Optional[Tuple[str, str]] = None,
193
190
  ) -> None:
194
191
  DEFAULT_CHAT_RENDERER_REGISTRY.register_part_renderer(
@@ -1,11 +1,21 @@
1
1
  from typing import Any, Dict, List, Optional, Tuple
2
2
  import json
3
+ import dataclasses
3
4
 
5
+ import opik.exceptions
4
6
  from opik.rest_api import client as rest_client
5
7
  from opik.rest_api import core as rest_api_core
6
- from opik.rest_api.types import prompt_version_detail, PromptVersionDetailType
8
+ from opik.rest_api.types import prompt_version_detail
9
+ from . import types as prompt_types
7
10
 
8
- from .prompt import PromptType
11
+
12
+ @dataclasses.dataclass
13
+ class PromptSearchResult:
14
+ """Result from searching prompts, containing name, template structure, and latest version details."""
15
+
16
+ name: str
17
+ template_structure: str
18
+ prompt_version_detail: prompt_version_detail.PromptVersionDetail
9
19
 
10
20
 
11
21
  class PromptClient:
@@ -17,7 +27,8 @@ class PromptClient:
17
27
  name: str,
18
28
  prompt: str,
19
29
  metadata: Optional[Dict[str, Any]],
20
- type: PromptType = PromptType.MUSTACHE,
30
+ type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
31
+ template_structure: str = "text",
21
32
  ) -> prompt_version_detail.PromptVersionDetail:
22
33
  """
23
34
  Creates the prompt detail for the given prompt name and template.
@@ -25,20 +36,59 @@ class PromptClient:
25
36
  Parameters:
26
37
  - name: The name of the prompt.
27
38
  - prompt: The template content for the prompt.
39
+ - metadata: Optional metadata for the prompt.
40
+ - type: The template type (MUSTACHE or JINJA2).
41
+ - template_structure: Either "text" (default) or "chat".
28
42
 
29
43
  Returns:
30
44
  - A Prompt object for the provided prompt name and template.
45
+
46
+ Raises:
47
+ - PromptTemplateStructureMismatch: If a prompt with the same name already exists but has a different
48
+ template_structure (e.g., trying to create a text prompt when a chat prompt exists, or vice versa).
49
+ Template structure is immutable after prompt creation.
31
50
  """
32
51
  prompt_version = self._get_latest_version(name)
33
52
 
53
+ # For chat prompts, compare parsed JSON to avoid formatting differences
54
+ templates_equal = False
55
+
56
+ if prompt_version is not None:
57
+ if prompt_version.template_structure != template_structure:
58
+ raise opik.exceptions.PromptTemplateStructureMismatch(
59
+ prompt_name=name,
60
+ existing_structure=prompt_version.template_structure,
61
+ attempted_structure=template_structure,
62
+ )
63
+
64
+ if template_structure == "chat":
65
+ try:
66
+ existing_messages = json.loads(prompt_version.template)
67
+ new_messages = json.loads(prompt)
68
+ templates_equal = existing_messages == new_messages
69
+ except (json.JSONDecodeError, TypeError):
70
+ templates_equal = prompt_version.template == prompt
71
+ else:
72
+ templates_equal = prompt_version.template == prompt
73
+
74
+ # Create a new version if:
75
+ # - No version exists yet (new prompt)
76
+ # - Template content has changed
77
+ # - Metadata has changed
78
+ # - Type has changed
79
+ # Note: template_structure is immutable and used by the backend only if it is the first prompt version.
34
80
  if (
35
81
  prompt_version is None
36
- or prompt_version.template != prompt
82
+ or not templates_equal
37
83
  or prompt_version.metadata != metadata
38
84
  or prompt_version.type != type.value
39
85
  ):
40
86
  prompt_version = self._create_new_version(
41
- name=name, prompt=prompt, type=type, metadata=metadata
87
+ name=name,
88
+ prompt=prompt,
89
+ type=type,
90
+ metadata=metadata,
91
+ template_structure=template_structure,
42
92
  )
43
93
 
44
94
  return prompt_version
@@ -47,8 +97,9 @@ class PromptClient:
47
97
  self,
48
98
  name: str,
49
99
  prompt: str,
50
- type: PromptVersionDetailType,
100
+ type: prompt_version_detail.PromptVersionDetailType,
51
101
  metadata: Optional[Dict[str, Any]],
102
+ template_structure: str = "text",
52
103
  ) -> prompt_version_detail.PromptVersionDetail:
53
104
  new_prompt_version_detail_data = prompt_version_detail.PromptVersionDetail(
54
105
  template=prompt,
@@ -59,6 +110,7 @@ class PromptClient:
59
110
  self._rest_client.prompts.create_prompt_version(
60
111
  name=name,
61
112
  version=new_prompt_version_detail_data,
113
+ template_structure=template_structure,
62
114
  )
63
115
  )
64
116
  return new_prompt_version_detail
@@ -66,20 +118,13 @@ class PromptClient:
66
118
  def _get_latest_version(
67
119
  self, name: str
68
120
  ) -> Optional[prompt_version_detail.PromptVersionDetail]:
69
- try:
70
- prompt_latest_version = self._rest_client.prompts.retrieve_prompt_version(
71
- name=name
72
- )
73
- return prompt_latest_version
74
- except rest_api_core.ApiError as e:
75
- if e.status_code != 404:
76
- raise e
77
- return None
121
+ return self.get_prompt(name=name, commit=None)
78
122
 
79
123
  def get_prompt(
80
124
  self,
81
125
  name: str,
82
126
  commit: Optional[str] = None,
127
+ raise_if_not_template_structure: Optional[str] = None,
83
128
  ) -> Optional[prompt_version_detail.PromptVersionDetail]:
84
129
  """
85
130
  Retrieve the prompt detail for a given prompt name and commit version.
@@ -87,6 +132,7 @@ class PromptClient:
87
132
  Parameters:
88
133
  name: The name of the prompt.
89
134
  commit: An optional commit version of the prompt. If not provided, the latest version is retrieved.
135
+ raise_if_not_template_structure: Optional template structure validation. If provided and doesn't match, raises PromptTemplateStructureMismatch.
90
136
 
91
137
  Returns:
92
138
  Prompt: The details of the specified prompt.
@@ -96,12 +142,23 @@ class PromptClient:
96
142
  name=name,
97
143
  commit=commit,
98
144
  )
99
- return prompt_version
100
145
 
146
+ # Client-side validation for template_structure if requested
147
+ if (
148
+ raise_if_not_template_structure is not None
149
+ and prompt_version.template_structure != raise_if_not_template_structure
150
+ ):
151
+ raise opik.exceptions.PromptTemplateStructureMismatch(
152
+ prompt_name=name,
153
+ existing_structure=prompt_version.template_structure,
154
+ attempted_structure=raise_if_not_template_structure,
155
+ )
156
+
157
+ return prompt_version
101
158
  except rest_api_core.ApiError as e:
102
159
  if e.status_code != 404:
103
160
  raise e
104
-
161
+ # 400, 404 - not found
105
162
  return None
106
163
 
107
164
  # TODO: Need to add support for prompt name in the BE so we don't
@@ -184,7 +241,7 @@ class PromptClient:
184
241
  *,
185
242
  name: Optional[str] = None,
186
243
  parsed_filters: Optional[List[Dict[str, Any]]] = None,
187
- ) -> List[Tuple[str, prompt_version_detail.PromptVersionDetail]]:
244
+ ) -> List[PromptSearchResult]:
188
245
  """
189
246
  Search prompt containers by optional name substring and filters, then
190
247
  return the latest version detail for each matched prompt container.
@@ -194,17 +251,17 @@ class PromptClient:
194
251
  parsed_filters: List of parsed filters (OQL) that will be stringified for the backend.
195
252
 
196
253
  Returns:
197
- List[Tuple[str, PromptVersionDetail]]: (prompt name, latest version) for each matched prompt container.
254
+ List[PromptSearchResult]: Each result contains name, template_structure, and prompt_version_detail.
198
255
  """
199
256
  try:
200
257
  filters_str = (
201
258
  json.dumps(parsed_filters) if parsed_filters is not None else None
202
259
  )
203
260
 
204
- # Page through all prompt containers
261
+ # Page through all prompt containers and collect name + template_structure
205
262
  page = 1
206
- size = 100
207
- all_prompt_names: List[str] = []
263
+ size = 1000
264
+ prompt_info: List[Tuple[str, str]] = [] # (name, template_structure)
208
265
  while True:
209
266
  prompts_page = self._rest_client.prompts.get_prompts(
210
267
  page=page,
@@ -215,21 +272,35 @@ class PromptClient:
215
272
  content = prompts_page.content or []
216
273
  if len(content) == 0:
217
274
  break
218
- all_prompt_names.extend([p.name for p in content])
275
+ prompt_info.extend(
276
+ [(p.name, p.template_structure or "text") for p in content]
277
+ )
219
278
  if len(content) < size:
220
279
  break
221
280
  page += 1
222
281
 
223
- if len(all_prompt_names) == 0:
282
+ if len(prompt_info) == 0:
224
283
  return []
225
284
 
226
285
  # Retrieve latest version for each container name
227
- results: List[Tuple[str, prompt_version_detail.PromptVersionDetail]] = []
228
- for prompt_name in all_prompt_names:
229
- latest_version = self._rest_client.prompts.retrieve_prompt_version(
230
- name=prompt_name
231
- )
232
- results.append((prompt_name, latest_version))
286
+ results: List[PromptSearchResult] = []
287
+ for prompt_name, template_structure in prompt_info:
288
+ try:
289
+ latest_version = self._rest_client.prompts.retrieve_prompt_version(
290
+ name=prompt_name,
291
+ )
292
+ results.append(
293
+ PromptSearchResult(
294
+ name=prompt_name,
295
+ template_structure=template_structure,
296
+ prompt_version_detail=latest_version,
297
+ )
298
+ )
299
+ except rest_api_core.ApiError as e:
300
+ # Skip prompts that can't be retrieved (e.g., deleted between search and retrieval)
301
+ if e.status_code == 404:
302
+ continue
303
+ raise e
233
304
 
234
305
  return results
235
306
 
@@ -0,0 +1 @@
1
+ # Empty - all exports handled by parent __init__.py
@@ -2,16 +2,17 @@ import copy
2
2
  import json
3
3
  import logging
4
4
  from typing import Any, Dict, Optional, Union, List
5
-
6
- from opik.rest_api.types import PromptVersionDetail
7
- from .prompt_template import PromptTemplate
8
- from .types import PromptType
9
- from opik.api_objects.prompt import client as prompt_client
5
+ from typing_extensions import override
6
+ from opik.rest_api import types as rest_api_types
7
+ from . import prompt_template
8
+ from .. import types as prompt_types
9
+ from .. import client as prompt_client
10
+ from .. import base_prompt
10
11
 
11
12
  LOGGER = logging.getLogger(__name__)
12
13
 
13
14
 
14
- class Prompt:
15
+ class Prompt(base_prompt.BasePrompt):
15
16
  """
16
17
  Prompt class represents a prompt with a name, prompt text/template and commit hash.
17
18
  """
@@ -21,18 +22,27 @@ class Prompt:
21
22
  name: str,
22
23
  prompt: str,
23
24
  metadata: Optional[Dict[str, Any]] = None,
24
- type: PromptType = PromptType.MUSTACHE,
25
+ type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
26
+ validate_placeholders: bool = True,
25
27
  ) -> None:
26
28
  """
27
29
  Initializes a new instance of the class with the given parameters.
28
- Creates a new prompt using the opik client and sets the initial state of the instance attributes based on the created prompt.
30
+ Creates a new text prompt using the opik client and sets the initial state of the instance attributes based on the created prompt.
29
31
 
30
32
  Parameters:
31
33
  name: The name for the prompt.
32
34
  prompt: The template for the prompt.
35
+ metadata: Optional metadata for the prompt.
36
+ type: The template type (MUSTACHE or JINJA2).
37
+ validate_placeholders: Whether to validate template placeholders.
38
+
39
+ Raises:
40
+ PromptTemplateStructureMismatch: If a chat prompt with the same name already exists (template structure is immutable).
33
41
  """
34
42
 
35
- self._template = PromptTemplate(template=prompt, type=type)
43
+ self._template = prompt_template.PromptTemplate(
44
+ template=prompt, type=type, validate_placeholders=validate_placeholders
45
+ )
36
46
  self._name = name
37
47
  self._metadata = metadata
38
48
  self._type = type
@@ -56,6 +66,7 @@ class Prompt:
56
66
  self.__internal_api__version_id__ = prompt_version.id
57
67
 
58
68
  @property
69
+ @override
59
70
  def name(self) -> str:
60
71
  """The name of the prompt."""
61
72
  return self._name
@@ -66,20 +77,24 @@ class Prompt:
66
77
  return str(self._template)
67
78
 
68
79
  @property
80
+ @override
69
81
  def commit(self) -> Optional[str]:
70
82
  """The commit hash of the prompt."""
71
83
  return self._commit
72
84
 
73
85
  @property
86
+ @override
74
87
  def metadata(self) -> Optional[Dict[str, Any]]:
75
88
  """The metadata dictionary associated with the prompt"""
76
89
  return copy.deepcopy(self._metadata)
77
90
 
78
91
  @property
79
- def type(self) -> PromptType:
92
+ @override
93
+ def type(self) -> prompt_types.PromptType:
80
94
  """The prompt type of the prompt."""
81
95
  return self._type
82
96
 
97
+ @override
83
98
  def format(self, **kwargs: Any) -> Union[str, List[Dict[str, Any]]]:
84
99
  """
85
100
  Replaces placeholders in the template with provided keyword arguments.
@@ -109,11 +124,37 @@ class Prompt:
109
124
 
110
125
  return formatted_string
111
126
 
127
+ @override
128
+ def __internal_api__to_info_dict__(self) -> Dict[str, Any]:
129
+ """
130
+ Convert the prompt to an info dictionary for serialization.
131
+
132
+ Returns:
133
+ Dictionary containing prompt metadata and version information.
134
+ """
135
+ info_dict: Dict[str, Any] = {
136
+ "name": self.name,
137
+ "version": {
138
+ "template": self.prompt,
139
+ },
140
+ }
141
+
142
+ if self.__internal_api__prompt_id__ is not None:
143
+ info_dict["id"] = self.__internal_api__prompt_id__
144
+
145
+ if self.commit is not None:
146
+ info_dict["version"]["commit"] = self.commit
147
+
148
+ if self.__internal_api__version_id__ is not None:
149
+ info_dict["version"]["id"] = self.__internal_api__version_id__
150
+
151
+ return info_dict
152
+
112
153
  @classmethod
113
154
  def from_fern_prompt_version(
114
155
  cls,
115
156
  name: str,
116
- prompt_version: PromptVersionDetail,
157
+ prompt_version: rest_api_types.PromptVersionDetail,
117
158
  ) -> "Prompt":
118
159
  # will not call __init__ to avoid API calls, create new instance with __new__
119
160
  prompt = cls.__new__(cls)
@@ -122,30 +163,12 @@ class Prompt:
122
163
  prompt.__internal_api__prompt_id__ = prompt_version.prompt_id
123
164
 
124
165
  prompt._name = name
125
- prompt._template = PromptTemplate(
166
+ prompt._template = prompt_template.PromptTemplate(
126
167
  template=prompt_version.template,
127
- type=PromptType(prompt_version.type) or PromptType.MUSTACHE,
168
+ type=prompt_types.PromptType(prompt_version.type)
169
+ or prompt_types.PromptType.MUSTACHE,
128
170
  )
129
171
  prompt._commit = prompt_version.commit
130
172
  prompt._metadata = prompt_version.metadata
131
173
  prompt._type = prompt_version.type
132
174
  return prompt
133
-
134
-
135
- def to_info_dict(prompt: Prompt) -> Dict[str, Any]:
136
- info_dict: Dict[str, Any] = {
137
- "name": prompt.name,
138
- "version": {
139
- "template": prompt.prompt,
140
- },
141
- }
142
- if prompt.__internal_api__prompt_id__ is not None:
143
- info_dict["id"] = prompt.__internal_api__prompt_id__
144
-
145
- if prompt.commit is not None:
146
- info_dict["version"]["commit"] = prompt.commit
147
-
148
- if prompt.__internal_api__version_id__ is not None:
149
- info_dict["version"]["id"] = prompt.__internal_api__version_id__
150
-
151
- return info_dict
@@ -1,17 +1,19 @@
1
1
  import re
2
2
  from typing import Any, Set
3
+ from typing_extensions import override
3
4
  import jinja2
4
5
 
5
6
  import opik.exceptions as exceptions
6
- from .types import PromptType
7
+ from .. import types as prompt_types
8
+ from .. import base_prompt_template
7
9
 
8
10
 
9
- class PromptTemplate:
11
+ class PromptTemplate(base_prompt_template.BasePromptTemplate):
10
12
  def __init__(
11
13
  self,
12
14
  template: str,
13
15
  validate_placeholders: bool = True,
14
- type: PromptType = PromptType.MUSTACHE,
16
+ type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
15
17
  ) -> None:
16
18
  self._template = template
17
19
  self._type = type
@@ -21,8 +23,9 @@ class PromptTemplate:
21
23
  def text(self) -> str:
22
24
  return self._template
23
25
 
26
+ @override
24
27
  def format(self, **kwargs: Any) -> str:
25
- if self._type == PromptType.MUSTACHE:
28
+ if self._type == prompt_types.PromptType.MUSTACHE:
26
29
  template = self._template
27
30
  placeholders = _extract_mustache_placeholder_keys(self._template)
28
31
  kwargs_keys: Set[str] = set(kwargs.keys())
@@ -36,7 +39,7 @@ class PromptTemplate:
36
39
  replacement = "" if value is None else str(value)
37
40
  template = template.replace(f"{{{{{key}}}}}", replacement)
38
41
 
39
- elif self._type == PromptType.JINJA2:
42
+ elif self._type == prompt_types.PromptType.JINJA2:
40
43
  template = jinja2.Template(self._template).render(**kwargs)
41
44
  else:
42
45
  template = self._template
opik/cli/export.py CHANGED
@@ -457,14 +457,18 @@ def _export_prompts(
457
457
  prompt_data = {
458
458
  "name": prompt.name,
459
459
  "current_version": {
460
- "prompt": prompt.prompt,
460
+ "prompt": prompt.prompt
461
+ if isinstance(prompt, opik.Prompt)
462
+ else None, # TODO: add support for chat prompts
461
463
  "metadata": prompt.metadata,
462
464
  "type": prompt.type if prompt.type else None,
463
465
  "commit": prompt.commit,
464
466
  },
465
467
  "history": [
466
468
  {
467
- "prompt": version.prompt,
469
+ "prompt": version.prompt
470
+ if isinstance(version, opik.Prompt)
471
+ else None, # TODO: add support for chat prompts
468
472
  "metadata": version.metadata,
469
473
  "type": version.type if version.type else None,
470
474
  "commit": version.commit,
opik/config.py CHANGED
@@ -7,7 +7,6 @@ import pathlib
7
7
  import urllib.parse
8
8
  from typing import Any, Dict, Final, List, Literal, Optional, Tuple, Type, Union
9
9
 
10
- import opik.decorator.tracing_runtime_config as tracing_runtime_config
11
10
  import pydantic
12
11
  import pydantic_settings
13
12
  from pydantic_settings import BaseSettings, InitSettingsSource
@@ -257,10 +256,6 @@ class OpikConfig(pydantic_settings.BaseSettings):
257
256
  def guardrails_backend_host(self) -> str:
258
257
  return url_helpers.get_base_url(self.url_override) + "guardrails/"
259
258
 
260
- @property
261
- def runtime(self) -> tracing_runtime_config.TracingRuntimeConfig:
262
- return tracing_runtime_config.runtime_cfg
263
-
264
259
  @pydantic.model_validator(mode="after")
265
260
  def _set_url_override_from_api_key(self) -> "OpikConfig":
266
261
  url_was_not_provided = (