opik 1.9.39__py3-none-any.whl → 1.9.86__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 (195) hide show
  1. opik/api_objects/attachment/attachment_context.py +36 -0
  2. opik/api_objects/attachment/attachments_extractor.py +153 -0
  3. opik/api_objects/attachment/client.py +1 -0
  4. opik/api_objects/attachment/converters.py +2 -0
  5. opik/api_objects/attachment/decoder.py +18 -0
  6. opik/api_objects/attachment/decoder_base64.py +83 -0
  7. opik/api_objects/attachment/decoder_helpers.py +137 -0
  8. opik/api_objects/constants.py +2 -0
  9. opik/api_objects/dataset/dataset.py +133 -40
  10. opik/api_objects/dataset/rest_operations.py +2 -0
  11. opik/api_objects/experiment/experiment.py +6 -0
  12. opik/api_objects/helpers.py +8 -4
  13. opik/api_objects/local_recording.py +6 -5
  14. opik/api_objects/observation_data.py +101 -0
  15. opik/api_objects/opik_client.py +78 -45
  16. opik/api_objects/opik_query_language.py +9 -3
  17. opik/api_objects/prompt/chat/chat_prompt.py +18 -1
  18. opik/api_objects/prompt/client.py +8 -1
  19. opik/api_objects/span/span_data.py +3 -88
  20. opik/api_objects/threads/threads_client.py +7 -4
  21. opik/api_objects/trace/trace_data.py +3 -74
  22. opik/api_objects/validation_helpers.py +3 -3
  23. opik/cli/exports/__init__.py +131 -0
  24. opik/cli/exports/dataset.py +278 -0
  25. opik/cli/exports/experiment.py +784 -0
  26. opik/cli/exports/project.py +685 -0
  27. opik/cli/exports/prompt.py +578 -0
  28. opik/cli/exports/utils.py +406 -0
  29. opik/cli/harbor.py +39 -0
  30. opik/cli/imports/__init__.py +439 -0
  31. opik/cli/imports/dataset.py +143 -0
  32. opik/cli/imports/experiment.py +1192 -0
  33. opik/cli/imports/project.py +262 -0
  34. opik/cli/imports/prompt.py +177 -0
  35. opik/cli/imports/utils.py +280 -0
  36. opik/cli/main.py +14 -12
  37. opik/config.py +12 -1
  38. opik/datetime_helpers.py +12 -0
  39. opik/decorator/arguments_helpers.py +4 -1
  40. opik/decorator/base_track_decorator.py +111 -37
  41. opik/decorator/context_manager/span_context_manager.py +5 -1
  42. opik/decorator/generator_wrappers.py +5 -4
  43. opik/decorator/span_creation_handler.py +13 -4
  44. opik/evaluation/engine/engine.py +111 -28
  45. opik/evaluation/engine/evaluation_tasks_executor.py +71 -19
  46. opik/evaluation/evaluator.py +12 -0
  47. opik/evaluation/metrics/conversation/llm_judges/conversational_coherence/metric.py +3 -1
  48. opik/evaluation/metrics/conversation/llm_judges/session_completeness/metric.py +3 -1
  49. opik/evaluation/metrics/conversation/llm_judges/user_frustration/metric.py +3 -1
  50. opik/evaluation/metrics/heuristics/equals.py +11 -7
  51. opik/evaluation/metrics/llm_judges/answer_relevance/metric.py +3 -1
  52. opik/evaluation/metrics/llm_judges/context_precision/metric.py +3 -1
  53. opik/evaluation/metrics/llm_judges/context_recall/metric.py +3 -1
  54. opik/evaluation/metrics/llm_judges/factuality/metric.py +1 -1
  55. opik/evaluation/metrics/llm_judges/g_eval/metric.py +3 -1
  56. opik/evaluation/metrics/llm_judges/hallucination/metric.py +3 -1
  57. opik/evaluation/metrics/llm_judges/moderation/metric.py +3 -1
  58. opik/evaluation/metrics/llm_judges/structure_output_compliance/metric.py +3 -1
  59. opik/evaluation/metrics/llm_judges/syc_eval/metric.py +4 -2
  60. opik/evaluation/metrics/llm_judges/trajectory_accuracy/metric.py +3 -1
  61. opik/evaluation/metrics/llm_judges/usefulness/metric.py +3 -1
  62. opik/evaluation/metrics/ragas_metric.py +43 -23
  63. opik/evaluation/models/litellm/litellm_chat_model.py +7 -2
  64. opik/evaluation/models/litellm/util.py +4 -20
  65. opik/evaluation/models/models_factory.py +19 -5
  66. opik/evaluation/rest_operations.py +3 -3
  67. opik/evaluation/threads/helpers.py +3 -2
  68. opik/file_upload/file_uploader.py +13 -0
  69. opik/file_upload/upload_options.py +2 -0
  70. opik/integrations/adk/legacy_opik_tracer.py +9 -11
  71. opik/integrations/adk/opik_tracer.py +2 -2
  72. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +2 -2
  73. opik/integrations/dspy/callback.py +100 -14
  74. opik/integrations/dspy/parsers.py +168 -0
  75. opik/integrations/harbor/__init__.py +17 -0
  76. opik/integrations/harbor/experiment_service.py +269 -0
  77. opik/integrations/harbor/opik_tracker.py +528 -0
  78. opik/integrations/haystack/opik_tracer.py +2 -2
  79. opik/integrations/langchain/__init__.py +15 -2
  80. opik/integrations/langchain/langgraph_tracer_injector.py +88 -0
  81. opik/integrations/langchain/opik_tracer.py +258 -160
  82. opik/integrations/langchain/provider_usage_extractors/langchain_run_helpers/helpers.py +7 -4
  83. opik/integrations/llama_index/callback.py +43 -6
  84. opik/integrations/openai/agents/opik_tracing_processor.py +8 -10
  85. opik/integrations/openai/opik_tracker.py +99 -4
  86. opik/integrations/openai/videos/__init__.py +9 -0
  87. opik/integrations/openai/videos/binary_response_write_to_file_decorator.py +88 -0
  88. opik/integrations/openai/videos/videos_create_decorator.py +159 -0
  89. opik/integrations/openai/videos/videos_download_decorator.py +110 -0
  90. opik/message_processing/batching/base_batcher.py +14 -21
  91. opik/message_processing/batching/batch_manager.py +22 -10
  92. opik/message_processing/batching/batchers.py +32 -40
  93. opik/message_processing/batching/flushing_thread.py +0 -3
  94. opik/message_processing/emulation/emulator_message_processor.py +36 -1
  95. opik/message_processing/emulation/models.py +21 -0
  96. opik/message_processing/messages.py +9 -0
  97. opik/message_processing/preprocessing/__init__.py +0 -0
  98. opik/message_processing/preprocessing/attachments_preprocessor.py +70 -0
  99. opik/message_processing/preprocessing/batching_preprocessor.py +53 -0
  100. opik/message_processing/preprocessing/constants.py +1 -0
  101. opik/message_processing/preprocessing/file_upload_preprocessor.py +38 -0
  102. opik/message_processing/preprocessing/preprocessor.py +36 -0
  103. opik/message_processing/processors/__init__.py +0 -0
  104. opik/message_processing/processors/attachments_extraction_processor.py +146 -0
  105. opik/message_processing/{message_processors.py → processors/message_processors.py} +15 -1
  106. opik/message_processing/{message_processors_chain.py → processors/message_processors_chain.py} +3 -2
  107. opik/message_processing/{online_message_processor.py → processors/online_message_processor.py} +11 -9
  108. opik/message_processing/queue_consumer.py +4 -2
  109. opik/message_processing/streamer.py +71 -33
  110. opik/message_processing/streamer_constructors.py +36 -8
  111. opik/plugins/pytest/experiment_runner.py +1 -1
  112. opik/plugins/pytest/hooks.py +5 -3
  113. opik/rest_api/__init__.py +42 -0
  114. opik/rest_api/datasets/client.py +321 -123
  115. opik/rest_api/datasets/raw_client.py +470 -145
  116. opik/rest_api/experiments/client.py +26 -0
  117. opik/rest_api/experiments/raw_client.py +26 -0
  118. opik/rest_api/llm_provider_key/client.py +4 -4
  119. opik/rest_api/llm_provider_key/raw_client.py +4 -4
  120. opik/rest_api/llm_provider_key/types/provider_api_key_write_provider.py +2 -1
  121. opik/rest_api/manual_evaluation/client.py +101 -0
  122. opik/rest_api/manual_evaluation/raw_client.py +172 -0
  123. opik/rest_api/optimizations/client.py +0 -166
  124. opik/rest_api/optimizations/raw_client.py +0 -248
  125. opik/rest_api/projects/client.py +9 -0
  126. opik/rest_api/projects/raw_client.py +13 -0
  127. opik/rest_api/projects/types/project_metric_request_public_metric_type.py +4 -0
  128. opik/rest_api/prompts/client.py +130 -2
  129. opik/rest_api/prompts/raw_client.py +175 -0
  130. opik/rest_api/traces/client.py +101 -0
  131. opik/rest_api/traces/raw_client.py +120 -0
  132. opik/rest_api/types/__init__.py +50 -0
  133. opik/rest_api/types/audio_url.py +19 -0
  134. opik/rest_api/types/audio_url_public.py +19 -0
  135. opik/rest_api/types/audio_url_write.py +19 -0
  136. opik/rest_api/types/automation_rule_evaluator.py +38 -2
  137. opik/rest_api/types/automation_rule_evaluator_object_object_public.py +33 -2
  138. opik/rest_api/types/automation_rule_evaluator_public.py +33 -2
  139. opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python.py +22 -0
  140. opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_public.py +22 -0
  141. opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_write.py +22 -0
  142. opik/rest_api/types/automation_rule_evaluator_update.py +27 -1
  143. opik/rest_api/types/automation_rule_evaluator_update_span_user_defined_metric_python.py +22 -0
  144. opik/rest_api/types/automation_rule_evaluator_write.py +27 -1
  145. opik/rest_api/types/dataset.py +2 -0
  146. opik/rest_api/types/dataset_item.py +1 -1
  147. opik/rest_api/types/dataset_item_batch.py +4 -0
  148. opik/rest_api/types/dataset_item_changes_public.py +5 -0
  149. opik/rest_api/types/dataset_item_compare.py +1 -1
  150. opik/rest_api/types/dataset_item_filter.py +4 -0
  151. opik/rest_api/types/dataset_item_page_compare.py +0 -1
  152. opik/rest_api/types/dataset_item_page_public.py +0 -1
  153. opik/rest_api/types/dataset_item_public.py +1 -1
  154. opik/rest_api/types/dataset_public.py +2 -0
  155. opik/rest_api/types/dataset_version_public.py +10 -0
  156. opik/rest_api/types/dataset_version_summary.py +46 -0
  157. opik/rest_api/types/dataset_version_summary_public.py +46 -0
  158. opik/rest_api/types/experiment.py +9 -0
  159. opik/rest_api/types/experiment_public.py +9 -0
  160. opik/rest_api/types/group_content_with_aggregations.py +1 -0
  161. opik/rest_api/types/llm_as_judge_message_content.py +2 -0
  162. opik/rest_api/types/llm_as_judge_message_content_public.py +2 -0
  163. opik/rest_api/types/llm_as_judge_message_content_write.py +2 -0
  164. opik/rest_api/types/manual_evaluation_request_entity_type.py +1 -1
  165. opik/rest_api/types/project.py +1 -0
  166. opik/rest_api/types/project_detailed.py +1 -0
  167. opik/rest_api/types/project_metric_response_public_metric_type.py +4 -0
  168. opik/rest_api/types/project_reference.py +31 -0
  169. opik/rest_api/types/project_reference_public.py +31 -0
  170. opik/rest_api/types/project_stats_summary_item.py +1 -0
  171. opik/rest_api/types/prompt_version.py +1 -0
  172. opik/rest_api/types/prompt_version_detail.py +1 -0
  173. opik/rest_api/types/prompt_version_page_public.py +5 -0
  174. opik/rest_api/types/prompt_version_public.py +1 -0
  175. opik/rest_api/types/prompt_version_update.py +33 -0
  176. opik/rest_api/types/provider_api_key.py +5 -1
  177. opik/rest_api/types/provider_api_key_provider.py +2 -1
  178. opik/rest_api/types/provider_api_key_public.py +5 -1
  179. opik/rest_api/types/provider_api_key_public_provider.py +2 -1
  180. opik/rest_api/types/service_toggles_config.py +11 -1
  181. opik/rest_api/types/span_user_defined_metric_python_code.py +20 -0
  182. opik/rest_api/types/span_user_defined_metric_python_code_public.py +20 -0
  183. opik/rest_api/types/span_user_defined_metric_python_code_write.py +20 -0
  184. opik/types.py +36 -0
  185. opik/validation/chat_prompt_messages.py +241 -0
  186. opik/validation/feedback_score.py +3 -3
  187. opik/validation/validator.py +28 -0
  188. {opik-1.9.39.dist-info → opik-1.9.86.dist-info}/METADATA +7 -7
  189. {opik-1.9.39.dist-info → opik-1.9.86.dist-info}/RECORD +193 -142
  190. opik/cli/export.py +0 -791
  191. opik/cli/import_command.py +0 -575
  192. {opik-1.9.39.dist-info → opik-1.9.86.dist-info}/WHEEL +0 -0
  193. {opik-1.9.39.dist-info → opik-1.9.86.dist-info}/entry_points.txt +0 -0
  194. {opik-1.9.39.dist-info → opik-1.9.86.dist-info}/licenses/LICENSE +0 -0
  195. {opik-1.9.39.dist-info → opik-1.9.86.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,578 @@
1
+ """Prompt export functionality."""
2
+
3
+ import json
4
+ import sys
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any, List, Optional, Union
8
+
9
+ import click
10
+ from rich.console import Console
11
+
12
+ import opik
13
+ from opik.api_objects.prompt import Prompt, ChatPrompt
14
+ from .utils import (
15
+ debug_print,
16
+ prompt_to_csv_rows,
17
+ should_skip_file,
18
+ write_csv_data,
19
+ write_json_data,
20
+ print_export_summary,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ def _get_prompt_content(prompt: Any) -> Any:
27
+ """Extract prompt content based on prompt type.
28
+
29
+ Args:
30
+ prompt: A Prompt or ChatPrompt instance
31
+
32
+ Returns:
33
+ For Prompt: the prompt string
34
+ For ChatPrompt: the template (list of message dicts)
35
+ Otherwise: None
36
+ """
37
+ if isinstance(prompt, Prompt):
38
+ return prompt.prompt
39
+ elif isinstance(prompt, ChatPrompt):
40
+ return prompt.template
41
+ return None
42
+
43
+
44
+ def _get_template_structure(prompt: Any) -> str:
45
+ """Get template_structure based on prompt type.
46
+
47
+ Args:
48
+ prompt: A Prompt or ChatPrompt instance
49
+
50
+ Returns:
51
+ "text" for Prompt, "chat" for ChatPrompt, "text" as default
52
+ """
53
+ if isinstance(prompt, ChatPrompt):
54
+ return "chat"
55
+ elif isinstance(prompt, Prompt):
56
+ return "text"
57
+ return "text"
58
+
59
+
60
+ def _get_prompt_type_string(prompt: Any) -> Optional[str]:
61
+ """Get prompt type as uppercase string.
62
+
63
+ Args:
64
+ prompt: A Prompt or ChatPrompt instance
65
+
66
+ Returns:
67
+ Uppercase type string (e.g., "JINJA2", "MUSTACHE") or None
68
+ """
69
+ prompt_type = getattr(prompt, "type", None)
70
+ if prompt_type is None:
71
+ return None
72
+
73
+ # If it's an enum, get the value and convert to uppercase
74
+ if hasattr(prompt_type, "value"):
75
+ return prompt_type.value.upper()
76
+ # If it's already a string, convert to uppercase
77
+ if isinstance(prompt_type, str):
78
+ return prompt_type.upper()
79
+ # Otherwise, convert to string and uppercase
80
+ return str(prompt_type).upper()
81
+
82
+
83
+ def export_single_prompt(
84
+ client: opik.Opik,
85
+ prompt: Union[Prompt, ChatPrompt],
86
+ output_dir: Path,
87
+ max_results: Optional[int],
88
+ force: bool,
89
+ debug: bool,
90
+ format: str,
91
+ ) -> int:
92
+ """Export a single prompt."""
93
+ try:
94
+ # Check if already exists and force is not set
95
+ if format.lower() == "csv":
96
+ prompt_file = output_dir / f"prompts_{prompt.name.replace('/', '_')}.csv"
97
+ else:
98
+ prompt_file = output_dir / f"prompt_{prompt.name.replace('/', '_')}.json"
99
+
100
+ if should_skip_file(prompt_file, force):
101
+ if debug:
102
+ debug_print(f"Skipping {prompt.name} (already exists)", debug)
103
+ return 0
104
+
105
+ # Get prompt history - use appropriate method based on prompt type
106
+ prompt_history: List[Union[Prompt, ChatPrompt]]
107
+ if isinstance(prompt, ChatPrompt):
108
+ prompt_history = list(client.get_chat_prompt_history(prompt.name))
109
+ else:
110
+ prompt_history = list(client.get_prompt_history(prompt.name))
111
+
112
+ # Create prompt data structure
113
+ prompt_data = {
114
+ "name": prompt.name,
115
+ "current_version": {
116
+ "prompt": _get_prompt_content(prompt),
117
+ "metadata": getattr(prompt, "metadata", None),
118
+ "type": _get_prompt_type_string(prompt),
119
+ "commit": getattr(prompt, "commit", None),
120
+ "template_structure": _get_template_structure(prompt),
121
+ },
122
+ "history": [
123
+ {
124
+ "prompt": _get_prompt_content(version),
125
+ "metadata": getattr(version, "metadata", None),
126
+ "type": _get_prompt_type_string(version),
127
+ "commit": getattr(version, "commit", None),
128
+ "template_structure": _get_template_structure(version),
129
+ }
130
+ for version in prompt_history
131
+ ],
132
+ "downloaded_at": datetime.now().isoformat(),
133
+ }
134
+
135
+ # Save to file using the appropriate format
136
+ if format.lower() == "csv":
137
+ write_csv_data(prompt_data, prompt_file, prompt_to_csv_rows)
138
+ else:
139
+ write_json_data(prompt_data, prompt_file)
140
+
141
+ if debug:
142
+ debug_print(f"Exported prompt: {prompt.name}", debug)
143
+ return 1
144
+
145
+ except Exception as e:
146
+ console.print(f"[red]Error exporting prompt {prompt.name}: {e}[/red]")
147
+ return 0
148
+
149
+
150
+ def export_prompt_by_name(
151
+ name: str,
152
+ workspace: str,
153
+ output_path: str,
154
+ max_results: Optional[int],
155
+ force: bool,
156
+ debug: bool,
157
+ format: str,
158
+ api_key: Optional[str] = None,
159
+ ) -> None:
160
+ """Export a prompt by exact name."""
161
+ try:
162
+ if debug:
163
+ debug_print(f"Exporting prompt: {name}", debug)
164
+
165
+ # Initialize client
166
+ if api_key:
167
+ client = opik.Opik(api_key=api_key, workspace=workspace)
168
+ else:
169
+ client = opik.Opik(workspace=workspace)
170
+
171
+ # Create output directory
172
+ output_dir = Path(output_path) / workspace / "prompts"
173
+ output_dir.mkdir(parents=True, exist_ok=True)
174
+
175
+ if debug:
176
+ debug_print(f"Target directory: {output_dir}", debug)
177
+
178
+ # Try to get prompt by exact name
179
+ # Try ChatPrompt first, then regular Prompt
180
+ prompt: Optional[Union[Prompt, ChatPrompt]] = None
181
+ try:
182
+ prompt = client.get_chat_prompt(name)
183
+ if debug and prompt:
184
+ debug_print(f"Found ChatPrompt by direct lookup: {prompt.name}", debug)
185
+ except Exception:
186
+ # Not a ChatPrompt, try regular Prompt
187
+ try:
188
+ prompt = client.get_prompt(name)
189
+ if not prompt:
190
+ console.print(f"[red]Prompt '{name}' not found[/red]")
191
+ return
192
+ if debug:
193
+ debug_print(f"Found Prompt by direct lookup: {prompt.name}", debug)
194
+ except Exception as e:
195
+ console.print(f"[red]Prompt '{name}' not found: {e}[/red]")
196
+ return
197
+
198
+ if prompt is None:
199
+ console.print(f"[red]Prompt '{name}' not found[/red]")
200
+ return
201
+
202
+ # Export the prompt
203
+ exported_count = export_single_prompt(
204
+ client, prompt, output_dir, max_results, force, debug, format
205
+ )
206
+
207
+ # Collect statistics for summary
208
+ stats = {
209
+ "prompts": 1 if exported_count > 0 else 0,
210
+ "prompts_skipped": 0 if exported_count > 0 else 1,
211
+ }
212
+
213
+ # Show export summary
214
+ print_export_summary(stats, format)
215
+
216
+ if exported_count > 0:
217
+ console.print(
218
+ f"[green]Successfully exported prompt '{name}' to {output_dir}[/green]"
219
+ )
220
+ else:
221
+ console.print(
222
+ f"[yellow]Prompt '{name}' already exists (use --force to re-download)[/yellow]"
223
+ )
224
+
225
+ except Exception as e:
226
+ console.print(f"[red]Error exporting prompt: {e}[/red]")
227
+ sys.exit(1)
228
+
229
+
230
+ def export_prompts_by_ids(
231
+ client: opik.Opik,
232
+ prompt_ids: set[str],
233
+ prompts_dir: Path,
234
+ format: str,
235
+ debug: bool,
236
+ force: bool,
237
+ ) -> tuple[int, int]:
238
+ """Export prompts by their IDs.
239
+
240
+ Args:
241
+ client: Opik client instance
242
+ prompt_ids: Set of prompt IDs to export
243
+ prompts_dir: Directory to save prompts
244
+ format: Export format ('json' or 'csv')
245
+ debug: Enable debug output
246
+ force: Re-download prompts even if they already exist locally
247
+
248
+ Returns:
249
+ Tuple of (exported_count, skipped_count)
250
+ """
251
+ exported_count = 0
252
+ skipped_count = 0
253
+
254
+ for prompt_id in prompt_ids:
255
+ try:
256
+ # Get the prompt - try ChatPrompt first, then regular Prompt
257
+ prompt: Optional[Union[Prompt, ChatPrompt]] = None
258
+ try:
259
+ prompt = client.get_chat_prompt(prompt_id)
260
+ except Exception:
261
+ # Not a ChatPrompt, try regular Prompt
262
+ prompt = client.get_prompt(prompt_id)
263
+
264
+ if not prompt:
265
+ if debug:
266
+ console.print(
267
+ f"[yellow]Warning: Prompt {prompt_id} not found[/yellow]"
268
+ )
269
+ continue
270
+
271
+ # Determine file path
272
+ if format.lower() == "csv":
273
+ prompt_file = (
274
+ prompts_dir
275
+ / f"prompts_{prompt.name or getattr(prompt, 'id', 'unknown')}.csv"
276
+ )
277
+ else:
278
+ prompt_file = (
279
+ prompts_dir
280
+ / f"prompt_{prompt.name or getattr(prompt, 'id', 'unknown')}.json"
281
+ )
282
+
283
+ # Check if file already exists and should be skipped
284
+ if should_skip_file(prompt_file, force):
285
+ if debug:
286
+ debug_print(
287
+ f"Skipping prompt {prompt.name or prompt_id} (already exists)",
288
+ debug,
289
+ )
290
+ else:
291
+ console.print(
292
+ f"[yellow]Skipping prompt: {prompt.name or prompt_id} (already exists)[/yellow]"
293
+ )
294
+ skipped_count += 1
295
+ continue
296
+
297
+ # Get prompt history - use appropriate method based on prompt type
298
+ prompt_history: List[Union[Prompt, ChatPrompt]]
299
+ if isinstance(prompt, ChatPrompt):
300
+ prompt_history = list(client.get_chat_prompt_history(prompt.name))
301
+ else:
302
+ prompt_history = list(client.get_prompt_history(prompt_id))
303
+
304
+ # Create prompt data structure
305
+ prompt_data = {
306
+ "prompt": {
307
+ "id": getattr(prompt, "id", None),
308
+ "name": prompt.name,
309
+ "description": getattr(prompt, "description", None),
310
+ "created_at": (
311
+ created_at.isoformat()
312
+ if (created_at := getattr(prompt, "created_at", None))
313
+ else None
314
+ ),
315
+ "last_updated_at": (
316
+ last_updated_at.isoformat()
317
+ if (last_updated_at := getattr(prompt, "last_updated_at", None))
318
+ else None
319
+ ),
320
+ },
321
+ "current_version": {
322
+ "prompt": _get_prompt_content(prompt),
323
+ "metadata": getattr(prompt, "metadata", None),
324
+ "type": _get_prompt_type_string(prompt),
325
+ "commit": getattr(prompt, "commit", None),
326
+ "template_structure": _get_template_structure(prompt),
327
+ },
328
+ "history": [
329
+ {
330
+ "prompt": _get_prompt_content(version),
331
+ "metadata": getattr(version, "metadata", None),
332
+ "type": _get_prompt_type_string(version),
333
+ "commit": getattr(version, "commit", None),
334
+ "template_structure": _get_template_structure(version),
335
+ }
336
+ for version in prompt_history
337
+ ],
338
+ "downloaded_at": datetime.now().isoformat(),
339
+ }
340
+
341
+ # Save prompt data using the appropriate format
342
+ if format.lower() == "csv":
343
+ write_csv_data(prompt_data, prompt_file, prompt_to_csv_rows)
344
+ else:
345
+ write_json_data(prompt_data, prompt_file)
346
+
347
+ console.print(f"[green]Exported prompt: {prompt.name or prompt_id}[/green]")
348
+ exported_count += 1
349
+
350
+ except Exception as e:
351
+ if debug:
352
+ console.print(
353
+ f"[yellow]Warning: Could not export prompt {prompt_id}: {e}[/yellow]"
354
+ )
355
+ else:
356
+ console.print(f"[red]Error exporting prompt {prompt_id}: {e}[/red]")
357
+ continue
358
+
359
+ return exported_count, skipped_count
360
+
361
+
362
+ def export_related_prompts_by_name(
363
+ client: opik.Opik,
364
+ experiment: Any,
365
+ output_dir: Path,
366
+ force: bool,
367
+ debug: bool,
368
+ format: str = "json",
369
+ ) -> int:
370
+ """Export prompts explicitly related to the experiment from experiment metadata."""
371
+ try:
372
+ prompts_dir = output_dir.parent / "prompts"
373
+ prompts_dir.mkdir(parents=True, exist_ok=True)
374
+
375
+ # Get experiment data to access metadata
376
+ experiment_data = experiment.get_experiment_data()
377
+ if not experiment_data:
378
+ debug_print("Could not get experiment data", debug)
379
+ return 0
380
+
381
+ # Extract prompt names from experiment metadata
382
+ prompt_names = []
383
+ metadata = experiment_data.metadata
384
+
385
+ if metadata:
386
+ # Metadata can be a dict, list, or string (JsonListStringPublic)
387
+ # Parse if it's a string, otherwise use directly
388
+ if isinstance(metadata, str):
389
+ try:
390
+ metadata = json.loads(metadata)
391
+ except (json.JSONDecodeError, Exception) as e:
392
+ if debug:
393
+ debug_print(f"Could not parse metadata as JSON: {e}", debug)
394
+ metadata = None
395
+
396
+ # Check if metadata is a dict and has "prompts" key
397
+ if isinstance(metadata, dict) and "prompts" in metadata:
398
+ prompts_dict = metadata["prompts"]
399
+ if isinstance(prompts_dict, dict):
400
+ # Prompts are stored as a dict with prompt names as keys
401
+ prompt_names = list(prompts_dict.keys())
402
+ if debug:
403
+ debug_print(
404
+ f"Found {len(prompt_names)} prompt(s) in experiment metadata: {prompt_names}",
405
+ debug,
406
+ )
407
+ else:
408
+ if debug:
409
+ debug_print(
410
+ f"Metadata 'prompts' is not a dict, got: {type(prompts_dict)}",
411
+ debug,
412
+ )
413
+ else:
414
+ if debug:
415
+ debug_print("No 'prompts' key found in experiment metadata", debug)
416
+
417
+ if not prompt_names:
418
+ debug_print("No prompts found in experiment metadata", debug)
419
+ return 0
420
+
421
+ console.print(
422
+ f"[blue]Exporting {len(prompt_names)} prompt(s) from experiment metadata...[/blue]"
423
+ )
424
+
425
+ exported_count = 0
426
+ # Export each prompt by name from metadata
427
+ for prompt_name in prompt_names:
428
+ try:
429
+ debug_print(f"Exporting prompt: {prompt_name}", debug)
430
+
431
+ # Try to get the prompt - try ChatPrompt first, then regular Prompt
432
+ prompt: Optional[Union[Prompt, ChatPrompt]] = None
433
+ try:
434
+ prompt = client.get_chat_prompt(prompt_name)
435
+ except Exception:
436
+ # Not a ChatPrompt, try regular Prompt
437
+ try:
438
+ prompt = client.get_prompt(prompt_name)
439
+ except Exception as e:
440
+ if debug:
441
+ console.print(
442
+ f"[yellow]Warning: Could not get prompt '{prompt_name}': {e}[/yellow]"
443
+ )
444
+ continue
445
+
446
+ if not prompt:
447
+ if debug:
448
+ console.print(
449
+ f"[yellow]Warning: Prompt '{prompt_name}' not found[/yellow]"
450
+ )
451
+ continue
452
+
453
+ # Get prompt history - use appropriate method based on prompt type
454
+ prompt_history: List[Union[Prompt, ChatPrompt]]
455
+ if isinstance(prompt, ChatPrompt):
456
+ prompt_history = list(client.get_chat_prompt_history(prompt.name))
457
+ else:
458
+ prompt_history = list(client.get_prompt_history(prompt.name))
459
+
460
+ # Create prompt data structure
461
+ prompt_data = {
462
+ "prompt": {
463
+ "id": getattr(prompt, "__internal_api__prompt_id__", None),
464
+ "name": prompt.name,
465
+ "description": getattr(prompt, "description", None),
466
+ "created_at": getattr(prompt, "created_at", None),
467
+ "last_updated_at": getattr(prompt, "last_updated_at", None),
468
+ },
469
+ "current_version": {
470
+ "prompt": _get_prompt_content(prompt),
471
+ "metadata": getattr(prompt, "metadata", None),
472
+ "type": _get_prompt_type_string(prompt),
473
+ "commit": getattr(prompt, "commit", None),
474
+ "template_structure": _get_template_structure(prompt),
475
+ },
476
+ "history": [
477
+ {
478
+ "prompt": _get_prompt_content(version),
479
+ "metadata": getattr(version, "metadata", None),
480
+ "type": _get_prompt_type_string(version),
481
+ "commit": getattr(version, "commit", None),
482
+ "template_structure": _get_template_structure(version),
483
+ }
484
+ for version in prompt_history
485
+ ],
486
+ "downloaded_at": datetime.now().isoformat(),
487
+ "related_to_experiment": experiment.name or experiment.id,
488
+ }
489
+
490
+ # Save prompt data using the appropriate format
491
+ # Sanitize prompt name for filename (replace / with _)
492
+ sanitized_name = prompt.name.replace("/", "_")
493
+ if format.lower() == "csv":
494
+ prompt_file = prompts_dir / f"prompts_{sanitized_name}.csv"
495
+ else:
496
+ prompt_file = prompts_dir / f"prompt_{sanitized_name}.json"
497
+
498
+ # Check if file should be skipped using the standard utility
499
+ if should_skip_file(prompt_file, force):
500
+ debug_print(
501
+ f"Skipping prompt {prompt.name} (already exists)", debug
502
+ )
503
+ continue
504
+
505
+ # File doesn't exist or force is set, so export it
506
+ if format.lower() == "csv":
507
+ write_csv_data(prompt_data, prompt_file, prompt_to_csv_rows)
508
+ else:
509
+ write_json_data(prompt_data, prompt_file)
510
+
511
+ console.print(f"[green]Exported related prompt: {prompt.name}[/green]")
512
+ exported_count += 1
513
+
514
+ except Exception as e:
515
+ if debug:
516
+ prompt_display_name = prompt.name if prompt else prompt_name
517
+ console.print(
518
+ f"[yellow]Warning: Could not export related prompt {prompt_display_name}: {e}[/yellow]"
519
+ )
520
+ continue
521
+
522
+ return exported_count
523
+
524
+ except Exception as e:
525
+ if debug:
526
+ console.print(
527
+ f"[yellow]Warning: Could not export related prompts: {e}[/yellow]"
528
+ )
529
+ return 0
530
+
531
+
532
+ @click.command(name="prompt")
533
+ @click.argument("name", type=str)
534
+ @click.option(
535
+ "--max-results",
536
+ type=int,
537
+ help="Maximum number of prompts to export. Limits the total number of prompts downloaded.",
538
+ )
539
+ @click.option(
540
+ "--path",
541
+ "-p",
542
+ type=click.Path(file_okay=False, dir_okay=True, writable=True),
543
+ default="opik_exports",
544
+ help="Directory to save exported data. Defaults to opik_exports.",
545
+ )
546
+ @click.option(
547
+ "--force",
548
+ is_flag=True,
549
+ help="Re-download items even if they already exist locally.",
550
+ )
551
+ @click.option(
552
+ "--debug",
553
+ is_flag=True,
554
+ help="Enable debug output to show detailed information about the export process.",
555
+ )
556
+ @click.option(
557
+ "--format",
558
+ type=click.Choice(["json", "csv"], case_sensitive=False),
559
+ default="json",
560
+ help="Format for exporting data. Defaults to json.",
561
+ )
562
+ @click.pass_context
563
+ def export_prompt_command(
564
+ ctx: click.Context,
565
+ name: str,
566
+ max_results: Optional[int],
567
+ path: str,
568
+ force: bool,
569
+ debug: bool,
570
+ format: str,
571
+ ) -> None:
572
+ """Export a prompt by exact name to workspace/prompts."""
573
+ # Get workspace and API key from context
574
+ workspace = ctx.obj["workspace"]
575
+ api_key = ctx.obj.get("api_key") if ctx.obj else None
576
+ export_prompt_by_name(
577
+ name, workspace, path, max_results, force, debug, format, api_key
578
+ )