opik 1.9.41__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 (192) 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 +38 -0
  114. opik/rest_api/datasets/client.py +249 -148
  115. opik/rest_api/datasets/raw_client.py +356 -217
  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 +46 -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_item.py +1 -1
  146. opik/rest_api/types/dataset_item_batch.py +4 -0
  147. opik/rest_api/types/dataset_item_changes_public.py +5 -0
  148. opik/rest_api/types/dataset_item_compare.py +1 -1
  149. opik/rest_api/types/dataset_item_filter.py +4 -0
  150. opik/rest_api/types/dataset_item_page_compare.py +0 -1
  151. opik/rest_api/types/dataset_item_page_public.py +0 -1
  152. opik/rest_api/types/dataset_item_public.py +1 -1
  153. opik/rest_api/types/dataset_version_public.py +5 -0
  154. opik/rest_api/types/dataset_version_summary.py +5 -0
  155. opik/rest_api/types/dataset_version_summary_public.py +5 -0
  156. opik/rest_api/types/experiment.py +9 -0
  157. opik/rest_api/types/experiment_public.py +9 -0
  158. opik/rest_api/types/llm_as_judge_message_content.py +2 -0
  159. opik/rest_api/types/llm_as_judge_message_content_public.py +2 -0
  160. opik/rest_api/types/llm_as_judge_message_content_write.py +2 -0
  161. opik/rest_api/types/manual_evaluation_request_entity_type.py +1 -1
  162. opik/rest_api/types/project.py +1 -0
  163. opik/rest_api/types/project_detailed.py +1 -0
  164. opik/rest_api/types/project_metric_response_public_metric_type.py +4 -0
  165. opik/rest_api/types/project_reference.py +31 -0
  166. opik/rest_api/types/project_reference_public.py +31 -0
  167. opik/rest_api/types/project_stats_summary_item.py +1 -0
  168. opik/rest_api/types/prompt_version.py +1 -0
  169. opik/rest_api/types/prompt_version_detail.py +1 -0
  170. opik/rest_api/types/prompt_version_page_public.py +5 -0
  171. opik/rest_api/types/prompt_version_public.py +1 -0
  172. opik/rest_api/types/prompt_version_update.py +33 -0
  173. opik/rest_api/types/provider_api_key.py +5 -1
  174. opik/rest_api/types/provider_api_key_provider.py +2 -1
  175. opik/rest_api/types/provider_api_key_public.py +5 -1
  176. opik/rest_api/types/provider_api_key_public_provider.py +2 -1
  177. opik/rest_api/types/service_toggles_config.py +11 -1
  178. opik/rest_api/types/span_user_defined_metric_python_code.py +20 -0
  179. opik/rest_api/types/span_user_defined_metric_python_code_public.py +20 -0
  180. opik/rest_api/types/span_user_defined_metric_python_code_write.py +20 -0
  181. opik/types.py +36 -0
  182. opik/validation/chat_prompt_messages.py +241 -0
  183. opik/validation/feedback_score.py +3 -3
  184. opik/validation/validator.py +28 -0
  185. {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/METADATA +5 -5
  186. {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/RECORD +190 -141
  187. opik/cli/export.py +0 -791
  188. opik/cli/import_command.py +0 -575
  189. {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/WHEEL +0 -0
  190. {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/entry_points.txt +0 -0
  191. {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/licenses/LICENSE +0 -0
  192. {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/top_level.txt +0 -0
opik/cli/export.py DELETED
@@ -1,791 +0,0 @@
1
- """Download command for Opik CLI."""
2
-
3
- import csv
4
- import json
5
- import re
6
- import sys
7
- from datetime import datetime
8
- from pathlib import Path
9
- from typing import Dict, List, Optional
10
-
11
- import click
12
- from rich.console import Console
13
- from rich.progress import Progress, SpinnerColumn, TextColumn
14
-
15
- import opik
16
-
17
- console = Console()
18
-
19
-
20
- def _matches_name_pattern(name: str, pattern: Optional[str]) -> bool:
21
- """Check if a name matches the given regex pattern."""
22
- if pattern is None:
23
- return True
24
- try:
25
- return bool(re.search(pattern, name))
26
- except re.error as e:
27
- console.print(f"[red]Invalid regex pattern '{pattern}': {e}[/red]")
28
- return False
29
-
30
-
31
- def _flatten_dict_with_prefix(data: Dict, prefix: str = "") -> Dict:
32
- """Flatten a nested dictionary with a prefix for CSV export."""
33
- if not data:
34
- return {}
35
-
36
- flattened = {}
37
- for key, value in data.items():
38
- prefixed_key = f"{prefix}_{key}" if prefix else key
39
- if isinstance(value, (dict, list)):
40
- flattened[prefixed_key] = str(value)
41
- else:
42
- flattened[prefixed_key] = value if value is not None else ""
43
-
44
- return flattened
45
-
46
-
47
- def _dump_to_file(
48
- data: dict,
49
- file_path: Path,
50
- file_format: str,
51
- csv_writer: Optional[csv.DictWriter] = None,
52
- csv_fieldnames: Optional[List[str]] = None,
53
- ) -> tuple:
54
- """
55
- Helper function to dump data to file in the specified format.
56
-
57
- Args:
58
- data: The data to dump
59
- file_path: Path where to save the file
60
- file_format: Format to use ("json" or "csv")
61
- csv_writer: Existing CSV writer (for CSV format)
62
- csv_fieldnames: Existing CSV fieldnames (for CSV format)
63
-
64
- Returns:
65
- Tuple of (csv_writer, csv_fieldnames) for CSV format, or (None, None) for JSON
66
- """
67
- if file_format.lower() == "csv":
68
- # Convert to CSV rows
69
- csv_rows = _trace_to_csv_rows(data)
70
-
71
- # Initialize CSV writer if not already done
72
- if csv_writer is None and csv_rows:
73
- csv_file_handle = open(file_path, "w", newline="", encoding="utf-8")
74
- csv_fieldnames = list(csv_rows[0].keys())
75
- csv_writer = csv.DictWriter(csv_file_handle, fieldnames=csv_fieldnames)
76
- csv_writer.writeheader()
77
-
78
- # Write rows
79
- if csv_writer and csv_rows:
80
- csv_writer.writerows(csv_rows)
81
-
82
- return csv_writer, csv_fieldnames
83
- else:
84
- # Save to JSON file
85
- with open(file_path, "w", encoding="utf-8") as f:
86
- json.dump(data, f, indent=2, default=str)
87
-
88
- return None, None
89
-
90
-
91
- def _trace_to_csv_rows(trace_data: dict) -> List[Dict]:
92
- """Convert trace data to CSV rows format."""
93
- trace = trace_data["trace"]
94
- spans = trace_data.get("spans", [])
95
-
96
- # Flatten trace data with "trace" prefix
97
- trace_flat = _flatten_dict_with_prefix(trace, "trace")
98
-
99
- # If no spans, create a single row for the trace
100
- if not spans:
101
- # Create empty span fields to maintain consistent structure
102
- span_flat = {f"span_{key}": "" for key in trace.keys()}
103
- span_flat["span_parent_span_id"] = "" # Special case for parent_span_id
104
-
105
- # Combine trace and empty span data
106
- row = {**trace_flat, **span_flat}
107
- return [row]
108
-
109
- # Create rows for each span
110
- rows = []
111
- for span in spans:
112
- # Flatten span data with "span" prefix
113
- span_flat = _flatten_dict_with_prefix(span, "span")
114
-
115
- # Combine trace and span data
116
- row = {**trace_flat, **span_flat}
117
- rows.append(row)
118
-
119
- return rows
120
-
121
-
122
- def _export_data_type(
123
- data_type: str,
124
- client: opik.Opik,
125
- output_dir: Path,
126
- max_results: int,
127
- name_pattern: Optional[str] = None,
128
- debug: bool = False,
129
- ) -> int:
130
- """
131
- Helper function to export a specific data type.
132
-
133
- Args:
134
- data_type: Type of data to export ("traces", "datasets", "prompts")
135
- client: Opik client instance
136
- output_dir: Directory to save exported data
137
- max_results: Maximum number of items to export
138
- name_pattern: Optional name pattern filter
139
- debug: Whether to enable debug output
140
-
141
- Returns:
142
- Number of items exported
143
- """
144
- if data_type == "traces":
145
- # This requires additional parameters, so we'll handle it separately
146
- raise ValueError(
147
- "Traces export requires additional parameters - use _export_traces directly"
148
- )
149
- elif data_type == "datasets":
150
- if debug:
151
- console.print("[blue]Downloading datasets...[/blue]")
152
- return _export_datasets(client, output_dir, max_results, name_pattern, debug)
153
- elif data_type == "prompts":
154
- if debug:
155
- console.print("[blue]Downloading prompts...[/blue]")
156
- return _export_prompts(client, output_dir, max_results, name_pattern)
157
- else:
158
- console.print(f"[red]Unknown data type: {data_type}[/red]")
159
- return 0
160
-
161
-
162
- def _export_traces(
163
- client: opik.Opik,
164
- project_name: str,
165
- project_dir: Path,
166
- max_results: int,
167
- filter: Optional[str],
168
- name_pattern: Optional[str] = None,
169
- trace_format: str = "json",
170
- ) -> int:
171
- """Download traces and their spans with pagination support for large projects."""
172
- console.print(
173
- f"[blue]DEBUG: _export_traces called with project_name: {project_name}, project_dir: {project_dir}[/blue]"
174
- )
175
- exported_count = 0
176
- page_size = min(100, max_results) # Process in smaller batches
177
- last_trace_time = None
178
- total_processed = 0
179
-
180
- # For CSV format, set up streaming writer
181
- csv_file = None
182
- csv_file_handle = None
183
- csv_writer = None
184
- csv_fieldnames = None
185
-
186
- try:
187
- with Progress(
188
- SpinnerColumn(),
189
- TextColumn("[progress.description]{task.description}"),
190
- console=console,
191
- ) as progress:
192
- task = progress.add_task("Searching for traces...", total=None)
193
-
194
- while total_processed < max_results:
195
- # Calculate how many traces to fetch in this batch
196
- remaining = max_results - total_processed
197
- current_page_size = min(page_size, remaining)
198
-
199
- # Build filter string with pagination
200
- pagination_filter = filter or ""
201
- if last_trace_time:
202
- # Add timestamp filter to continue from where we left off
203
- time_filter = f"start_time < '{last_trace_time.isoformat()}'"
204
- if pagination_filter:
205
- pagination_filter = f"({pagination_filter}) AND {time_filter}"
206
- else:
207
- pagination_filter = time_filter
208
-
209
- try:
210
- console.print(
211
- f"[blue]DEBUG: Searching traces with project_name: {project_name}, filter: {pagination_filter}, max_results: {current_page_size}[/blue]"
212
- )
213
- traces = client.search_traces(
214
- project_name=project_name,
215
- filter_string=pagination_filter if pagination_filter else None,
216
- max_results=current_page_size,
217
- truncate=False, # Don't truncate data for download
218
- )
219
- console.print(
220
- f"[blue]DEBUG: Found {len(traces) if traces else 0} traces[/blue]"
221
- )
222
- except Exception as e:
223
- console.print(f"[red]Error searching traces: {e}[/red]")
224
- break
225
-
226
- if not traces:
227
- # No more traces to process
228
- break
229
-
230
- # Update progress description
231
- progress.update(
232
- task, description=f"Found {len(traces)} traces in current batch"
233
- )
234
-
235
- # Store original traces for pagination before filtering
236
- original_traces = traces
237
-
238
- # Filter traces by name pattern if specified
239
- if name_pattern:
240
- original_count = len(traces)
241
- traces = [
242
- trace
243
- for trace in traces
244
- if _matches_name_pattern(trace.name or "", name_pattern)
245
- ]
246
- if len(traces) < original_count:
247
- console.print(
248
- f"[blue]Filtered to {len(traces)} traces matching pattern '{name_pattern}' in current batch[/blue]"
249
- )
250
-
251
- if not traces:
252
- # No traces match the name pattern, but we might have more to process
253
- # Use original_traces for pagination, not the filtered empty list
254
- last_trace_time = (
255
- original_traces[0].start_time if original_traces else None
256
- )
257
- total_processed += current_page_size
258
- continue
259
-
260
- # Update progress for downloading
261
- progress.update(
262
- task,
263
- description=f"Downloading traces... (batch {total_processed // page_size + 1})",
264
- )
265
-
266
- # Download each trace with its spans
267
- for trace in traces:
268
- try:
269
- # Get spans for this trace
270
- spans = client.search_spans(
271
- project_name=project_name,
272
- trace_id=trace.id,
273
- max_results=1000, # Get all spans for the trace
274
- truncate=False,
275
- )
276
-
277
- # Create trace data structure
278
- trace_data = {
279
- "trace": trace.model_dump(),
280
- "spans": [span.model_dump() for span in spans],
281
- "downloaded_at": datetime.now().isoformat(),
282
- "project_name": project_name,
283
- }
284
-
285
- # Determine file path based on format
286
- if trace_format.lower() == "csv":
287
- file_path = project_dir / f"traces_{project_name}.csv"
288
- else:
289
- file_path = project_dir / f"trace_{trace.id}.json"
290
-
291
- # Use helper function to dump data
292
- csv_writer, csv_fieldnames = _dump_to_file(
293
- trace_data,
294
- file_path,
295
- trace_format,
296
- csv_writer,
297
- csv_fieldnames,
298
- )
299
-
300
- exported_count += 1
301
- total_processed += 1
302
-
303
- except Exception as e:
304
- console.print(
305
- f"[red]Error exporting trace {trace.id}: {e}[/red]"
306
- )
307
- continue
308
-
309
- # Update last trace time for pagination
310
- if traces:
311
- last_trace_time = traces[-1].start_time
312
-
313
- # If we got fewer traces than requested, we've reached the end
314
- if len(traces) < current_page_size:
315
- break
316
-
317
- # Final progress update
318
- if exported_count == 0:
319
- console.print("[yellow]No traces found in the project.[/yellow]")
320
- else:
321
- progress.update(
322
- task, description=f"Exported {exported_count} traces total"
323
- )
324
-
325
- finally:
326
- # Close CSV file if it was opened
327
- if csv_file_handle:
328
- csv_file_handle.close()
329
- if csv_file and csv_file.exists():
330
- console.print(f"[green]CSV file saved to {csv_file}[/green]")
331
-
332
- return exported_count
333
-
334
-
335
- def _export_datasets(
336
- client: opik.Opik,
337
- project_dir: Path,
338
- max_results: int,
339
- name_pattern: Optional[str] = None,
340
- debug: bool = False,
341
- ) -> int:
342
- """Export datasets."""
343
- try:
344
- datasets = client.get_datasets(max_results=max_results, sync_items=True)
345
-
346
- if not datasets:
347
- console.print("[yellow]No datasets found in the project.[/yellow]")
348
- return 0
349
-
350
- # Filter datasets by name pattern if specified
351
- if name_pattern:
352
- original_count = len(datasets)
353
- datasets = [
354
- dataset
355
- for dataset in datasets
356
- if _matches_name_pattern(dataset.name, name_pattern)
357
- ]
358
- if len(datasets) < original_count:
359
- console.print(
360
- f"[blue]Filtered to {len(datasets)} datasets matching pattern '{name_pattern}'[/blue]"
361
- )
362
-
363
- if not datasets:
364
- console.print(
365
- "[yellow]No datasets found matching the name pattern.[/yellow]"
366
- )
367
- return 0
368
-
369
- exported_count = 0
370
- for dataset in datasets:
371
- try:
372
- # Get dataset items using the get_items method
373
- if debug:
374
- console.print(
375
- f"[blue]Getting items for dataset: {dataset.name}[/blue]"
376
- )
377
- dataset_items = dataset.get_items()
378
-
379
- # Convert dataset items to the expected format for import
380
- formatted_items = []
381
- for item in dataset_items:
382
- formatted_item = {
383
- "input": item.get("input"),
384
- "expected_output": item.get("expected_output"),
385
- "metadata": item.get("metadata"),
386
- }
387
- formatted_items.append(formatted_item)
388
-
389
- # Create dataset data structure
390
- dataset_data = {
391
- "name": dataset.name,
392
- "description": dataset.description,
393
- "items": formatted_items,
394
- "downloaded_at": datetime.now().isoformat(),
395
- }
396
-
397
- # Save to file
398
- dataset_file = project_dir / f"dataset_{dataset.name}.json"
399
- with open(dataset_file, "w", encoding="utf-8") as f:
400
- json.dump(dataset_data, f, indent=2, default=str)
401
-
402
- exported_count += 1
403
-
404
- except Exception as e:
405
- console.print(
406
- f"[red]Error downloading dataset {dataset.name}: {e}[/red]"
407
- )
408
- continue
409
-
410
- return exported_count
411
-
412
- except Exception as e:
413
- console.print(f"[red]Error exporting datasets: {e}[/red]")
414
- return 0
415
-
416
-
417
- def _export_prompts(
418
- client: opik.Opik,
419
- project_dir: Path,
420
- max_results: int,
421
- name_pattern: Optional[str] = None,
422
- ) -> int:
423
- """Export prompts."""
424
- try:
425
- prompts = client.search_prompts()
426
-
427
- if not prompts:
428
- console.print("[yellow]No prompts found in the project.[/yellow]")
429
- return 0
430
-
431
- # Filter prompts by name pattern if specified
432
- if name_pattern:
433
- original_count = len(prompts)
434
- prompts = [
435
- prompt
436
- for prompt in prompts
437
- if _matches_name_pattern(prompt.name, name_pattern)
438
- ]
439
- if len(prompts) < original_count:
440
- console.print(
441
- f"[blue]Filtered to {len(prompts)} prompts matching pattern '{name_pattern}'[/blue]"
442
- )
443
-
444
- if not prompts:
445
- console.print(
446
- "[yellow]No prompts found matching the name pattern.[/yellow]"
447
- )
448
- return 0
449
-
450
- exported_count = 0
451
- for prompt in prompts:
452
- try:
453
- # Get prompt history
454
- prompt_history = client.get_prompt_history(prompt.name)
455
-
456
- # Create prompt data structure
457
- prompt_data = {
458
- "name": prompt.name,
459
- "current_version": {
460
- "prompt": prompt.prompt
461
- if isinstance(prompt, opik.Prompt)
462
- else None, # TODO: add support for chat prompts
463
- "metadata": prompt.metadata,
464
- "type": prompt.type if prompt.type else None,
465
- "commit": prompt.commit,
466
- },
467
- "history": [
468
- {
469
- "prompt": version.prompt
470
- if isinstance(version, opik.Prompt)
471
- else None, # TODO: add support for chat prompts
472
- "metadata": version.metadata,
473
- "type": version.type if version.type else None,
474
- "commit": version.commit,
475
- }
476
- for version in prompt_history
477
- ],
478
- "downloaded_at": datetime.now().isoformat(),
479
- }
480
-
481
- # Save to file
482
- prompt_file = (
483
- project_dir / f"prompt_{prompt.name.replace('/', '_')}.json"
484
- )
485
- with open(prompt_file, "w", encoding="utf-8") as f:
486
- json.dump(prompt_data, f, indent=2, default=str)
487
-
488
- exported_count += 1
489
-
490
- except Exception as e:
491
- console.print(f"[red]Error downloading prompt {prompt.name}: {e}[/red]")
492
- continue
493
-
494
- return exported_count
495
-
496
- except Exception as e:
497
- console.print(f"[red]Error exporting prompts: {e}[/red]")
498
- return 0
499
-
500
-
501
- @click.command(name="export")
502
- @click.argument("workspace_or_project", type=str)
503
- @click.option(
504
- "--path",
505
- "-p",
506
- type=click.Path(file_okay=False, dir_okay=True, writable=True),
507
- default="./",
508
- help="Directory to save exported data. Defaults to current directory.",
509
- )
510
- @click.option(
511
- "--max-results",
512
- type=int,
513
- default=1000,
514
- help="Maximum number of items to download per data type. Defaults to 1000.",
515
- )
516
- @click.option(
517
- "--filter",
518
- type=str,
519
- help="Filter string to narrow down the search using Opik Query Language (OQL).",
520
- )
521
- @click.option(
522
- "--all",
523
- is_flag=True,
524
- help="Include all data types (traces, datasets, prompts).",
525
- )
526
- @click.option(
527
- "--include",
528
- type=click.Choice(["traces", "datasets", "prompts"], case_sensitive=False),
529
- multiple=True,
530
- default=["traces"],
531
- help="Data types to include in download. Can be specified multiple times. Defaults to traces only.",
532
- )
533
- @click.option(
534
- "--exclude",
535
- type=click.Choice(["traces", "datasets", "prompts"], case_sensitive=False),
536
- multiple=True,
537
- help="Data types to exclude from download. Can be specified multiple times.",
538
- )
539
- @click.option(
540
- "--name",
541
- type=str,
542
- help="Filter items by name using Python regex patterns. Matches against trace names, dataset names, or prompt names.",
543
- )
544
- @click.option(
545
- "--debug",
546
- is_flag=True,
547
- help="Enable debug output to show detailed information about the export process.",
548
- )
549
- @click.option(
550
- "--trace-format",
551
- type=click.Choice(["json", "csv"], case_sensitive=False),
552
- default="json",
553
- help="Format for exporting traces. Defaults to json.",
554
- )
555
- def export(
556
- workspace_or_project: str,
557
- path: str,
558
- max_results: int,
559
- filter: Optional[str],
560
- all: bool,
561
- include: tuple,
562
- exclude: tuple,
563
- name: Optional[str],
564
- debug: bool,
565
- trace_format: str,
566
- ) -> None:
567
- """
568
- Download data from a workspace or workspace/project to local files.
569
-
570
- This command fetches traces, datasets, and prompts from the specified workspace or project
571
- and saves them to local JSON files in the output directory.
572
-
573
- Note: Thread metadata is automatically derived from traces with the same thread_id,
574
- so threads don't need to be exported separately.
575
-
576
- WORKSPACE_OR_PROJECT: Either a workspace name (e.g., "my-workspace") to export all projects,
577
- or workspace/project (e.g., "my-workspace/my-project") to export a specific project.
578
- """
579
- try:
580
- if debug:
581
- console.print("[blue]DEBUG: Starting export with parameters:[/blue]")
582
- console.print(
583
- f"[blue] workspace_or_project: {workspace_or_project}[/blue]"
584
- )
585
- console.print(f"[blue] path: {path}[/blue]")
586
- console.print(f"[blue] max_results: {max_results}[/blue]")
587
- console.print(f"[blue] include: {include}[/blue]")
588
- console.print(f"[blue] debug: {debug}[/blue]")
589
-
590
- # Parse workspace/project from the argument
591
- if "/" in workspace_or_project:
592
- workspace, project_name = workspace_or_project.split("/", 1)
593
- export_specific_project = True
594
- if debug:
595
- console.print(
596
- f"[blue]DEBUG: Parsed workspace: {workspace}, project: {project_name}[/blue]"
597
- )
598
- else:
599
- # Only workspace specified - download all projects
600
- workspace = workspace_or_project
601
- project_name = None
602
- export_specific_project = False
603
- if debug:
604
- console.print(f"[blue]DEBUG: Workspace only: {workspace}[/blue]")
605
-
606
- # Initialize Opik client with workspace
607
- if debug:
608
- console.print(
609
- f"[blue]DEBUG: Initializing Opik client with workspace: {workspace}[/blue]"
610
- )
611
- client = opik.Opik(workspace=workspace)
612
-
613
- # Create output directory
614
- output_path = Path(path)
615
- output_path.mkdir(parents=True, exist_ok=True)
616
-
617
- # Determine which data types to download
618
- if all:
619
- # If --all is specified, include all data types
620
- include_set = {"traces", "datasets", "prompts"}
621
- else:
622
- include_set = set(item.lower() for item in include)
623
-
624
- exclude_set = set(item.lower() for item in exclude)
625
-
626
- # Apply exclusions
627
- data_types = include_set - exclude_set
628
-
629
- if export_specific_project:
630
- # Download from specific project
631
- console.print(
632
- f"[green]Downloading data from workspace: {workspace}, project: {project_name}[/green]"
633
- )
634
-
635
- # Create workspace/project directory structure
636
- workspace_dir = output_path / workspace
637
- assert project_name is not None # Type narrowing for mypy
638
- project_dir = workspace_dir / project_name
639
- project_dir.mkdir(parents=True, exist_ok=True)
640
-
641
- if debug:
642
- console.print(
643
- f"[blue]Output directory: {workspace}/{project_name}[/blue]"
644
- )
645
- console.print(
646
- f"[blue]Data types: {', '.join(sorted(data_types))}[/blue]"
647
- )
648
-
649
- # Note about workspace vs project-specific data
650
- project_specific = [dt for dt in data_types if dt in ["traces"]]
651
- workspace_data = [dt for dt in data_types if dt in ["datasets", "prompts"]]
652
-
653
- if project_specific and workspace_data:
654
- console.print(
655
- f"[yellow]Note: {', '.join(project_specific)} are project-specific, {', '.join(workspace_data)} belong to workspace '{workspace}'[/yellow]"
656
- )
657
- elif workspace_data:
658
- console.print(
659
- f"[yellow]Note: {', '.join(workspace_data)} belong to workspace '{workspace}'[/yellow]"
660
- )
661
-
662
- # Download each data type
663
- total_exported = 0
664
-
665
- # Download traces
666
- if "traces" in data_types:
667
- if debug:
668
- console.print("[blue]Downloading traces...[/blue]")
669
- if debug:
670
- console.print(
671
- f"[blue]DEBUG: Calling _export_traces with project_name: {project_name}, project_dir: {project_dir}[/blue]"
672
- )
673
- traces_exported = _export_traces(
674
- client,
675
- project_name,
676
- project_dir,
677
- max_results,
678
- filter,
679
- name,
680
- trace_format,
681
- )
682
- if debug:
683
- console.print(
684
- f"[blue]DEBUG: _export_traces returned: {traces_exported}[/blue]"
685
- )
686
- total_exported += traces_exported
687
-
688
- # Download datasets and prompts (workspace-level data, but export to project directory for consistency)
689
- for data_type in ["datasets", "prompts"]:
690
- if data_type in data_types:
691
- exported_count = _export_data_type(
692
- data_type, client, project_dir, max_results, name, debug
693
- )
694
- total_exported += exported_count
695
-
696
- console.print(
697
- f"[green]Successfully exported {total_exported} items to {project_dir}[/green]"
698
- )
699
- else:
700
- # Export from all projects in workspace
701
- console.print(
702
- f"[green]Exporting data from workspace: {workspace} (all projects)[/green]"
703
- )
704
-
705
- # Get all projects in the workspace
706
- try:
707
- projects_response = client.rest_client.projects.find_projects()
708
- projects = projects_response.content or []
709
-
710
- if not projects:
711
- console.print(
712
- f"[yellow]No projects found in workspace '{workspace}'[/yellow]"
713
- )
714
- return
715
-
716
- console.print(
717
- f"[blue]Found {len(projects)} projects in workspace[/blue]"
718
- )
719
- console.print(
720
- f"[blue]Data types: {', '.join(sorted(data_types))}[/blue]"
721
- )
722
-
723
- # Note about workspace vs project-specific data
724
- project_specific = [dt for dt in data_types if dt in ["traces"]]
725
- workspace_data = [
726
- dt for dt in data_types if dt in ["datasets", "prompts"]
727
- ]
728
-
729
- if project_specific and workspace_data:
730
- console.print(
731
- f"[yellow]Note: {', '.join(project_specific)} are project-specific, {', '.join(workspace_data)} belong to workspace '{workspace}'[/yellow]"
732
- )
733
- elif workspace_data:
734
- console.print(
735
- f"[yellow]Note: {', '.join(workspace_data)} belong to workspace '{workspace}'[/yellow]"
736
- )
737
-
738
- total_exported = 0
739
-
740
- # Download workspace-level data once (datasets, experiments, prompts)
741
- workspace_dir = output_path / workspace
742
- workspace_dir.mkdir(parents=True, exist_ok=True)
743
-
744
- # Download datasets and prompts (workspace-level data)
745
- for data_type in ["datasets", "prompts"]:
746
- if data_type in data_types:
747
- exported_count = _export_data_type(
748
- data_type, client, workspace_dir, max_results, name, debug
749
- )
750
- total_exported += exported_count
751
-
752
- # Download traces from each project
753
- if "traces" in data_types:
754
- for project in projects:
755
- project_name = project.name
756
- console.print(
757
- f"[blue]Downloading traces from project: {project_name}...[/blue]"
758
- )
759
-
760
- # Create project directory
761
- assert project_name is not None # Type narrowing for mypy
762
- project_dir = workspace_dir / project_name
763
- project_dir.mkdir(parents=True, exist_ok=True)
764
-
765
- traces_exported = _export_traces(
766
- client,
767
- project_name,
768
- project_dir,
769
- max_results,
770
- filter,
771
- name,
772
- trace_format,
773
- )
774
- total_exported += traces_exported
775
-
776
- if traces_exported > 0:
777
- console.print(
778
- f"[green]Exported {traces_exported} traces from {project_name}[/green]"
779
- )
780
-
781
- console.print(
782
- f"[green]Successfully exported {total_exported} items from workspace '{workspace}'[/green]"
783
- )
784
-
785
- except Exception as e:
786
- console.print(f"[red]Error getting projects from workspace: {e}[/red]")
787
- sys.exit(1)
788
-
789
- except Exception as e:
790
- console.print(f"[red]Error: {e}[/red]")
791
- sys.exit(1)