opik 1.9.5__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 (248) hide show
  1. opik/__init__.py +10 -3
  2. opik/anonymizer/__init__.py +5 -0
  3. opik/anonymizer/anonymizer.py +12 -0
  4. opik/anonymizer/factory.py +80 -0
  5. opik/anonymizer/recursive_anonymizer.py +64 -0
  6. opik/anonymizer/rules.py +56 -0
  7. opik/anonymizer/rules_anonymizer.py +35 -0
  8. opik/api_objects/dataset/rest_operations.py +5 -0
  9. opik/api_objects/experiment/experiment.py +46 -49
  10. opik/api_objects/experiment/helpers.py +34 -10
  11. opik/api_objects/local_recording.py +8 -3
  12. opik/api_objects/opik_client.py +230 -48
  13. opik/api_objects/opik_query_language.py +9 -0
  14. opik/api_objects/prompt/__init__.py +11 -3
  15. opik/api_objects/prompt/base_prompt.py +69 -0
  16. opik/api_objects/prompt/base_prompt_template.py +29 -0
  17. opik/api_objects/prompt/chat/__init__.py +1 -0
  18. opik/api_objects/prompt/chat/chat_prompt.py +193 -0
  19. opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
  20. opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +37 -35
  21. opik/api_objects/prompt/client.py +101 -30
  22. opik/api_objects/prompt/text/__init__.py +1 -0
  23. opik/api_objects/prompt/text/prompt.py +174 -0
  24. opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
  25. opik/api_objects/prompt/types.py +1 -1
  26. opik/cli/export.py +6 -2
  27. opik/cli/usage_report/charts.py +39 -10
  28. opik/cli/usage_report/cli.py +164 -45
  29. opik/cli/usage_report/pdf.py +14 -1
  30. opik/config.py +0 -5
  31. opik/decorator/base_track_decorator.py +37 -40
  32. opik/decorator/context_manager/span_context_manager.py +9 -0
  33. opik/decorator/context_manager/trace_context_manager.py +5 -0
  34. opik/dict_utils.py +3 -3
  35. opik/evaluation/__init__.py +13 -2
  36. opik/evaluation/engine/engine.py +195 -223
  37. opik/evaluation/engine/helpers.py +8 -7
  38. opik/evaluation/engine/metrics_evaluator.py +237 -0
  39. opik/evaluation/evaluation_result.py +35 -1
  40. opik/evaluation/evaluator.py +318 -30
  41. opik/evaluation/models/litellm/util.py +78 -6
  42. opik/evaluation/models/model_capabilities.py +33 -0
  43. opik/evaluation/report.py +14 -2
  44. opik/evaluation/rest_operations.py +36 -33
  45. opik/evaluation/test_case.py +2 -2
  46. opik/evaluation/types.py +9 -1
  47. opik/exceptions.py +17 -0
  48. opik/hooks/__init__.py +17 -1
  49. opik/hooks/anonymizer_hook.py +36 -0
  50. opik/id_helpers.py +18 -0
  51. opik/integrations/adk/helpers.py +16 -7
  52. opik/integrations/adk/legacy_opik_tracer.py +7 -4
  53. opik/integrations/adk/opik_tracer.py +3 -1
  54. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
  55. opik/integrations/adk/recursive_callback_injector.py +1 -6
  56. opik/integrations/dspy/callback.py +1 -4
  57. opik/integrations/haystack/opik_connector.py +2 -2
  58. opik/integrations/haystack/opik_tracer.py +2 -4
  59. opik/integrations/langchain/opik_tracer.py +273 -82
  60. opik/integrations/llama_index/callback.py +110 -108
  61. opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
  62. opik/integrations/openai/opik_tracker.py +1 -1
  63. opik/message_processing/batching/batchers.py +11 -7
  64. opik/message_processing/encoder_helpers.py +79 -0
  65. opik/message_processing/messages.py +25 -1
  66. opik/message_processing/online_message_processor.py +23 -8
  67. opik/opik_context.py +7 -7
  68. opik/rest_api/__init__.py +188 -12
  69. opik/rest_api/client.py +3 -0
  70. opik/rest_api/dashboards/__init__.py +4 -0
  71. opik/rest_api/dashboards/client.py +462 -0
  72. opik/rest_api/dashboards/raw_client.py +648 -0
  73. opik/rest_api/datasets/client.py +893 -89
  74. opik/rest_api/datasets/raw_client.py +1328 -87
  75. opik/rest_api/experiments/client.py +30 -2
  76. opik/rest_api/experiments/raw_client.py +26 -0
  77. opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
  78. opik/rest_api/optimizations/client.py +302 -0
  79. opik/rest_api/optimizations/raw_client.py +463 -0
  80. opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
  81. opik/rest_api/prompts/__init__.py +2 -2
  82. opik/rest_api/prompts/client.py +34 -4
  83. opik/rest_api/prompts/raw_client.py +32 -2
  84. opik/rest_api/prompts/types/__init__.py +3 -1
  85. opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
  86. opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
  87. opik/rest_api/spans/__init__.py +0 -2
  88. opik/rest_api/spans/client.py +148 -64
  89. opik/rest_api/spans/raw_client.py +210 -83
  90. opik/rest_api/spans/types/__init__.py +0 -2
  91. opik/rest_api/traces/client.py +241 -73
  92. opik/rest_api/traces/raw_client.py +344 -90
  93. opik/rest_api/types/__init__.py +200 -15
  94. opik/rest_api/types/aggregation_data.py +1 -0
  95. opik/rest_api/types/alert_trigger_config_public_type.py +6 -1
  96. opik/rest_api/types/alert_trigger_config_type.py +6 -1
  97. opik/rest_api/types/alert_trigger_config_write_type.py +6 -1
  98. opik/rest_api/types/automation_rule_evaluator.py +23 -1
  99. opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
  100. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
  101. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
  102. opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
  103. opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
  104. opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
  105. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
  106. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
  107. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
  108. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
  109. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
  110. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
  111. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
  112. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
  113. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
  114. opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
  115. opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
  116. opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
  117. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
  118. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
  119. opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
  120. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
  121. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
  122. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
  123. opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
  124. opik/rest_api/types/boolean_feedback_definition.py +25 -0
  125. opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
  126. opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
  127. opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
  128. opik/rest_api/types/boolean_feedback_detail.py +29 -0
  129. opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
  130. opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
  131. opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
  132. opik/rest_api/types/dashboard_page_public.py +24 -0
  133. opik/rest_api/types/dashboard_public.py +30 -0
  134. opik/rest_api/types/dataset.py +2 -0
  135. opik/rest_api/types/dataset_item.py +2 -0
  136. opik/rest_api/types/dataset_item_compare.py +2 -0
  137. opik/rest_api/types/dataset_item_filter.py +23 -0
  138. opik/rest_api/types/dataset_item_filter_operator.py +21 -0
  139. opik/rest_api/types/dataset_item_page_compare.py +1 -0
  140. opik/rest_api/types/dataset_item_page_public.py +1 -0
  141. opik/rest_api/types/dataset_item_public.py +2 -0
  142. opik/rest_api/types/dataset_item_update.py +39 -0
  143. opik/rest_api/types/dataset_item_write.py +1 -0
  144. opik/rest_api/types/dataset_public.py +2 -0
  145. opik/rest_api/types/dataset_public_status.py +5 -0
  146. opik/rest_api/types/dataset_status.py +5 -0
  147. opik/rest_api/types/dataset_version_diff.py +22 -0
  148. opik/rest_api/types/dataset_version_diff_stats.py +24 -0
  149. opik/rest_api/types/dataset_version_page_public.py +23 -0
  150. opik/rest_api/types/dataset_version_public.py +49 -0
  151. opik/rest_api/types/experiment.py +2 -0
  152. opik/rest_api/types/experiment_public.py +2 -0
  153. opik/rest_api/types/experiment_score.py +20 -0
  154. opik/rest_api/types/experiment_score_public.py +20 -0
  155. opik/rest_api/types/experiment_score_write.py +20 -0
  156. opik/rest_api/types/feedback.py +20 -1
  157. opik/rest_api/types/feedback_create.py +16 -1
  158. opik/rest_api/types/feedback_object_public.py +22 -1
  159. opik/rest_api/types/feedback_public.py +20 -1
  160. opik/rest_api/types/feedback_score_public.py +4 -0
  161. opik/rest_api/types/feedback_update.py +16 -1
  162. opik/rest_api/types/image_url.py +20 -0
  163. opik/rest_api/types/image_url_public.py +20 -0
  164. opik/rest_api/types/image_url_write.py +20 -0
  165. opik/rest_api/types/llm_as_judge_message.py +5 -1
  166. opik/rest_api/types/llm_as_judge_message_content.py +24 -0
  167. opik/rest_api/types/llm_as_judge_message_content_public.py +24 -0
  168. opik/rest_api/types/llm_as_judge_message_content_write.py +24 -0
  169. opik/rest_api/types/llm_as_judge_message_public.py +5 -1
  170. opik/rest_api/types/llm_as_judge_message_write.py +5 -1
  171. opik/rest_api/types/llm_as_judge_model_parameters.py +2 -0
  172. opik/rest_api/types/llm_as_judge_model_parameters_public.py +2 -0
  173. opik/rest_api/types/llm_as_judge_model_parameters_write.py +2 -0
  174. opik/rest_api/types/optimization.py +2 -0
  175. opik/rest_api/types/optimization_public.py +2 -0
  176. opik/rest_api/types/optimization_public_status.py +3 -1
  177. opik/rest_api/types/optimization_status.py +3 -1
  178. opik/rest_api/types/optimization_studio_config.py +27 -0
  179. opik/rest_api/types/optimization_studio_config_public.py +27 -0
  180. opik/rest_api/types/optimization_studio_config_write.py +27 -0
  181. opik/rest_api/types/optimization_studio_log.py +22 -0
  182. opik/rest_api/types/optimization_write.py +2 -0
  183. opik/rest_api/types/optimization_write_status.py +3 -1
  184. opik/rest_api/types/prompt.py +6 -0
  185. opik/rest_api/types/prompt_detail.py +6 -0
  186. opik/rest_api/types/prompt_detail_template_structure.py +5 -0
  187. opik/rest_api/types/prompt_public.py +6 -0
  188. opik/rest_api/types/prompt_public_template_structure.py +5 -0
  189. opik/rest_api/types/prompt_template_structure.py +5 -0
  190. opik/rest_api/types/prompt_version.py +2 -0
  191. opik/rest_api/types/prompt_version_detail.py +2 -0
  192. opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
  193. opik/rest_api/types/prompt_version_public.py +2 -0
  194. opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
  195. opik/rest_api/types/prompt_version_template_structure.py +5 -0
  196. opik/rest_api/types/score_name.py +1 -0
  197. opik/rest_api/types/service_toggles_config.py +6 -0
  198. opik/rest_api/types/span_enrichment_options.py +31 -0
  199. opik/rest_api/types/span_filter.py +23 -0
  200. opik/rest_api/types/span_filter_operator.py +21 -0
  201. opik/rest_api/types/span_filter_write.py +23 -0
  202. opik/rest_api/types/span_filter_write_operator.py +21 -0
  203. opik/rest_api/types/span_llm_as_judge_code.py +27 -0
  204. opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
  205. opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
  206. opik/rest_api/types/span_update.py +46 -0
  207. opik/rest_api/types/studio_evaluation.py +20 -0
  208. opik/rest_api/types/studio_evaluation_public.py +20 -0
  209. opik/rest_api/types/studio_evaluation_write.py +20 -0
  210. opik/rest_api/types/studio_llm_model.py +21 -0
  211. opik/rest_api/types/studio_llm_model_public.py +21 -0
  212. opik/rest_api/types/studio_llm_model_write.py +21 -0
  213. opik/rest_api/types/studio_message.py +20 -0
  214. opik/rest_api/types/studio_message_public.py +20 -0
  215. opik/rest_api/types/studio_message_write.py +20 -0
  216. opik/rest_api/types/studio_metric.py +21 -0
  217. opik/rest_api/types/studio_metric_public.py +21 -0
  218. opik/rest_api/types/studio_metric_write.py +21 -0
  219. opik/rest_api/types/studio_optimizer.py +21 -0
  220. opik/rest_api/types/studio_optimizer_public.py +21 -0
  221. opik/rest_api/types/studio_optimizer_write.py +21 -0
  222. opik/rest_api/types/studio_prompt.py +20 -0
  223. opik/rest_api/types/studio_prompt_public.py +20 -0
  224. opik/rest_api/types/studio_prompt_write.py +20 -0
  225. opik/rest_api/types/trace.py +6 -0
  226. opik/rest_api/types/trace_public.py +6 -0
  227. opik/rest_api/types/trace_thread_filter_write.py +23 -0
  228. opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
  229. opik/rest_api/types/trace_thread_update.py +19 -0
  230. opik/rest_api/types/trace_update.py +39 -0
  231. opik/rest_api/types/value_entry.py +2 -0
  232. opik/rest_api/types/value_entry_compare.py +2 -0
  233. opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
  234. opik/rest_api/types/value_entry_public.py +2 -0
  235. opik/rest_api/types/video_url.py +19 -0
  236. opik/rest_api/types/video_url_public.py +19 -0
  237. opik/rest_api/types/video_url_write.py +19 -0
  238. opik/synchronization.py +5 -6
  239. opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
  240. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/METADATA +5 -4
  241. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/RECORD +246 -151
  242. opik/api_objects/prompt/chat_prompt_template.py +0 -164
  243. opik/api_objects/prompt/prompt.py +0 -131
  244. /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
  245. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
  246. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
  247. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
  248. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,174 @@
1
+ import copy
2
+ import json
3
+ import logging
4
+ from typing import Any, Dict, Optional, Union, List
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
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class Prompt(base_prompt.BasePrompt):
16
+ """
17
+ Prompt class represents a prompt with a name, prompt text/template and commit hash.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ name: str,
23
+ prompt: str,
24
+ metadata: Optional[Dict[str, Any]] = None,
25
+ type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
26
+ validate_placeholders: bool = True,
27
+ ) -> None:
28
+ """
29
+ Initializes a new instance of the class with the given parameters.
30
+ Creates a new text prompt using the opik client and sets the initial state of the instance attributes based on the created prompt.
31
+
32
+ Parameters:
33
+ name: The name for the prompt.
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).
41
+ """
42
+
43
+ self._template = prompt_template.PromptTemplate(
44
+ template=prompt, type=type, validate_placeholders=validate_placeholders
45
+ )
46
+ self._name = name
47
+ self._metadata = metadata
48
+ self._type = type
49
+
50
+ self._sync_with_backend()
51
+
52
+ def _sync_with_backend(self) -> None:
53
+ from opik.api_objects import opik_client
54
+
55
+ opik_client_ = opik_client.get_client_cached()
56
+ prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)
57
+ prompt_version = prompt_client_.create_prompt(
58
+ name=self._name,
59
+ prompt=self._template.text,
60
+ metadata=self._metadata,
61
+ type=self._type,
62
+ )
63
+
64
+ self._commit = prompt_version.commit
65
+ self.__internal_api__prompt_id__ = prompt_version.prompt_id
66
+ self.__internal_api__version_id__ = prompt_version.id
67
+
68
+ @property
69
+ @override
70
+ def name(self) -> str:
71
+ """The name of the prompt."""
72
+ return self._name
73
+
74
+ @property
75
+ def prompt(self) -> str:
76
+ """The latest template of the prompt."""
77
+ return str(self._template)
78
+
79
+ @property
80
+ @override
81
+ def commit(self) -> Optional[str]:
82
+ """The commit hash of the prompt."""
83
+ return self._commit
84
+
85
+ @property
86
+ @override
87
+ def metadata(self) -> Optional[Dict[str, Any]]:
88
+ """The metadata dictionary associated with the prompt"""
89
+ return copy.deepcopy(self._metadata)
90
+
91
+ @property
92
+ @override
93
+ def type(self) -> prompt_types.PromptType:
94
+ """The prompt type of the prompt."""
95
+ return self._type
96
+
97
+ @override
98
+ def format(self, **kwargs: Any) -> Union[str, List[Dict[str, Any]]]:
99
+ """
100
+ Replaces placeholders in the template with provided keyword arguments.
101
+
102
+ Args:
103
+ **kwargs: Arbitrary keyword arguments where the key represents the placeholder
104
+ in the template and the value is the value to replace the placeholder with.
105
+
106
+ Returns:
107
+ A string with all placeholders replaced by their corresponding values from kwargs.
108
+ """
109
+ is_playground_chat_prompt = (
110
+ self._metadata is not None
111
+ and self._metadata.get("created_from") == "opik_ui"
112
+ and self._metadata.get("type") == "messages_json"
113
+ )
114
+ formatted_string = self._template.format(**kwargs)
115
+
116
+ if is_playground_chat_prompt:
117
+ try:
118
+ return json.loads(formatted_string)
119
+ except json.JSONDecodeError:
120
+ LOGGER.error(
121
+ f"Failed to parse JSON string: {formatted_string}. Make sure chat prompt is valid JSON. Returning the raw string."
122
+ )
123
+ return formatted_string
124
+
125
+ return formatted_string
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
+
153
+ @classmethod
154
+ def from_fern_prompt_version(
155
+ cls,
156
+ name: str,
157
+ prompt_version: rest_api_types.PromptVersionDetail,
158
+ ) -> "Prompt":
159
+ # will not call __init__ to avoid API calls, create new instance with __new__
160
+ prompt = cls.__new__(cls)
161
+
162
+ prompt.__internal_api__version_id__ = prompt_version.id
163
+ prompt.__internal_api__prompt_id__ = prompt_version.prompt_id
164
+
165
+ prompt._name = name
166
+ prompt._template = prompt_template.PromptTemplate(
167
+ template=prompt_version.template,
168
+ type=prompt_types.PromptType(prompt_version.type)
169
+ or prompt_types.PromptType.MUSTACHE,
170
+ )
171
+ prompt._commit = prompt_version.commit
172
+ prompt._metadata = prompt_version.metadata
173
+ prompt._type = prompt_version.type
174
+ return prompt
@@ -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())
@@ -33,9 +36,10 @@ class PromptTemplate:
33
36
  )
34
37
 
35
38
  for key, value in kwargs.items():
36
- template = template.replace(f"{{{{{key}}}}}", str(value))
39
+ replacement = "" if value is None else str(value)
40
+ template = template.replace(f"{{{{{key}}}}}", replacement)
37
41
 
38
- elif self._type == PromptType.JINJA2:
42
+ elif self._type == prompt_types.PromptType.JINJA2:
39
43
  template = jinja2.Template(self._template).render(**kwargs)
40
44
  else:
41
45
  template = self._template
@@ -14,7 +14,7 @@ class PromptType(str, enum.Enum):
14
14
  MessageContent = Union[str, List[Dict[str, Any]]]
15
15
  ContentPart = Dict[str, Any]
16
16
  RendererFn = Callable[[ContentPart, Dict[str, Any], PromptType], Optional[ContentPart]]
17
- ModalityName = Literal["vision"]
17
+ ModalityName = Literal["vision", "video"]
18
18
  SupportedModalities = Mapping[ModalityName, bool]
19
19
  ModalitySet = Set[ModalityName]
20
20
 
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,
@@ -513,7 +513,7 @@ def create_individual_chart(
513
513
  else:
514
514
  period_labels.append(period)
515
515
 
516
- # Create figure - use same size as reference file for consistency
516
+ # Create figure with consistent size for all charts (same as reference implementation)
517
517
  fig, ax = plt.subplots(figsize=(14, 8))
518
518
  unit_label = unit.capitalize()
519
519
  x = range(n_periods)
@@ -681,19 +681,44 @@ def create_individual_chart(
681
681
  ax.set_xlabel(unit_label)
682
682
  ax.set_xticks(x)
683
683
  ax.set_xticklabels(period_labels, rotation=45, ha="right")
684
- if chart_type in ["trace_count", "token_count", "cost", "span_count"]:
685
- # Use compact legend with top projects only (max 19 items: 18 top + Others)
684
+ # Set x-axis limits to use full width, with small padding on edges
685
+ ax.set_xlim(-0.5, n_periods - 0.5)
686
+
687
+ ax.grid(axis="y", alpha=0.3)
688
+
689
+ # Configure legend for charts that need it - place inside figure bounds
690
+ has_legend = chart_type in ["trace_count", "token_count", "cost", "span_count"]
691
+ if has_legend:
692
+ # Truncate legend labels to maximum length to prevent overly wide legends
693
+ handles, labels = ax.get_legend_handles_labels()
694
+ max_label_length = 40 # Maximum characters per legend label
695
+ truncated_labels = []
696
+ for label in labels:
697
+ if len(label) > max_label_length:
698
+ truncated_labels.append(label[: max_label_length - 3] + "...")
699
+ else:
700
+ truncated_labels.append(label)
701
+
702
+ # Position legend inside the plot area at the bottom, with more space below
703
+ # This allows us to use bbox_inches=None for fixed image sizes
704
+ # Use 3 columns to ensure items wrap into multiple rows
686
705
  ax.legend(
687
- bbox_to_anchor=(0.5, -0.25),
706
+ handles,
707
+ truncated_labels,
688
708
  loc="upper center",
689
- ncol=4,
690
- fontsize=9,
709
+ bbox_to_anchor=(0.5, -0.35), # Lower in plot area, ~1.5 inches below chart
710
+ ncol=3, # 3 columns ensures wrapping into multiple rows
711
+ fontsize=8,
691
712
  framealpha=0.9,
692
713
  )
693
- ax.grid(axis="y", alpha=0.3)
694
714
 
695
- # Use rect parameter to make room for legends below charts (more space for lower legends)
696
- plt.tight_layout(rect=[0, 0.05, 1, 1])
715
+ # Explicitly set margins to ensure chart uses full width consistently
716
+ # Left margin (10%) accounts for y-axis labels (including formatted labels like "500.00M" or "$350.00")
717
+ # Right margin (5%) is minimal to maximize chart width
718
+ # Bottom margin (42.5%) accommodates legend positioned below the plot area (outside axes bounds) with ~1 inch of space below
719
+ # Top margin (8%) for title
720
+ # This ensures ALL charts have identical dimensions regardless of y-axis formatter
721
+ fig.subplots_adjust(left=0.10, right=0.95, top=0.92, bottom=0.425)
697
722
 
698
723
  # Save chart to temporary file (use absolute path)
699
724
  chart_filename = os.path.join(
@@ -707,7 +732,11 @@ def create_individual_chart(
707
732
  os.makedirs(chart_dir, exist_ok=True)
708
733
 
709
734
  try:
710
- plt.savefig(chart_filename, dpi=300, bbox_inches="tight")
735
+ # Use bbox_inches=None to preserve exact figure size (14x8 inches)
736
+ # Since legend is now inside figure bounds, we can use fixed dimensions
737
+ # This ensures ALL charts have identical dimensions (4200x2400 pixels at 300 DPI)
738
+ # regardless of y-axis label widths or content
739
+ plt.savefig(chart_filename, dpi=300, bbox_inches=None)
711
740
  plt.close()
712
741
 
713
742
  # Ensure file is fully written to disk using file system sync operations