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
@@ -0,0 +1,439 @@
1
+ """Import command for Opik CLI."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Dict, Optional
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ import opik
11
+
12
+ from .dataset import import_datasets_from_directory
13
+ from .experiment import import_experiments_from_directory
14
+ from .project import import_projects_from_directory
15
+ from .prompt import import_prompts_from_directory
16
+ from .utils import print_import_summary, debug_print
17
+
18
+ console = Console()
19
+
20
+ IMPORT_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
21
+
22
+
23
+ def _import_by_type(
24
+ import_type: str,
25
+ path: str,
26
+ workspace: str,
27
+ dry_run: bool,
28
+ name_pattern: Optional[str],
29
+ debug: bool,
30
+ recreate_experiments: bool = False,
31
+ api_key: Optional[str] = None,
32
+ ) -> None:
33
+ """
34
+ Import data by type (dataset, project, experiment) with pattern matching.
35
+
36
+ Args:
37
+ import_type: Type of data to import ("dataset", "project", "experiment")
38
+ path: Base directory containing the exported data
39
+ workspace: Target workspace name
40
+ dry_run: Whether to show what would be imported without importing
41
+ name_pattern: Optional string pattern to filter items by name (case-insensitive substring matching)
42
+ debug: Enable debug output
43
+ recreate_experiments: Whether to recreate experiments after importing
44
+ """
45
+ try:
46
+ debug_print(f"DEBUG: Starting {import_type} import from {path}", debug)
47
+
48
+ # Initialize Opik client
49
+ if api_key:
50
+ client = opik.Opik(api_key=api_key, workspace=workspace)
51
+ else:
52
+ client = opik.Opik(workspace=workspace)
53
+
54
+ # Determine source directory based on import type
55
+ base_path = Path(path)
56
+
57
+ if import_type == "dataset":
58
+ source_dir = base_path / "datasets"
59
+ elif import_type == "project":
60
+ source_dir = base_path / "projects"
61
+ elif import_type == "experiment":
62
+ source_dir = base_path / "experiments"
63
+ elif import_type == "prompt":
64
+ source_dir = base_path / "prompts"
65
+ else:
66
+ console.print(f"[red]Unknown import type: {import_type}[/red]")
67
+ return
68
+
69
+ if not source_dir.exists():
70
+ console.print(f"[red]Source directory {source_dir} does not exist[/red]")
71
+ sys.exit(1)
72
+
73
+ debug_print(f"Source directory: {source_dir}", debug)
74
+
75
+ stats: Dict[str, int] = {}
76
+
77
+ if import_type == "dataset":
78
+ stats = import_datasets_from_directory(
79
+ client, source_dir, dry_run, name_pattern, debug
80
+ )
81
+ elif import_type == "project":
82
+ stats = import_projects_from_directory(
83
+ client, source_dir, dry_run, name_pattern, debug, recreate_experiments
84
+ )
85
+ elif import_type == "experiment":
86
+ stats = import_experiments_from_directory(
87
+ client, source_dir, dry_run, name_pattern, debug
88
+ )
89
+ elif import_type == "prompt":
90
+ stats = import_prompts_from_directory(
91
+ client, source_dir, dry_run, name_pattern, debug
92
+ )
93
+
94
+ # Display summary
95
+ print_import_summary(stats)
96
+
97
+ # Also show a simple message for backward compatibility
98
+ # Map import_type to the key used in stats dictionary
99
+ type_key_map = {
100
+ "dataset": "datasets",
101
+ "prompt": "prompts",
102
+ "project": "projects",
103
+ "experiment": "experiments",
104
+ }
105
+ stats_key = type_key_map.get(import_type, import_type + "s")
106
+ imported_count = stats.get(stats_key, 0)
107
+ errors = stats.get(stats_key + "_errors", 0)
108
+
109
+ if dry_run:
110
+ console.print(
111
+ f"[blue]Dry run complete: Would import {imported_count} {import_type}s[/blue]"
112
+ )
113
+ else:
114
+ if errors > 0:
115
+ console.print(
116
+ f"[yellow]Import completed with {errors} error(s) while importing {import_type}s[/yellow]"
117
+ )
118
+ elif imported_count == 0:
119
+ console.print(f"[yellow]No {import_type}s were imported[/yellow]")
120
+ else:
121
+ console.print(
122
+ f"[green]Successfully imported {imported_count} {import_type}s[/green]"
123
+ )
124
+
125
+ except Exception as e:
126
+ console.print(f"[red]Error importing {import_type}s: {e}[/red]")
127
+ sys.exit(1)
128
+
129
+
130
+ @click.group(name="import", context_settings=IMPORT_CONTEXT_SETTINGS)
131
+ @click.argument("workspace", type=str)
132
+ @click.option(
133
+ "--api-key",
134
+ type=str,
135
+ help="Opik API key. If not provided, will use OPIK_API_KEY environment variable or configuration.",
136
+ )
137
+ @click.pass_context
138
+ def import_group(ctx: click.Context, workspace: str, api_key: Optional[str]) -> None:
139
+ """Import data to Opik workspace.
140
+
141
+ This command allows you to import previously exported data back into an Opik workspace.
142
+ Supported data types include projects, datasets, experiments, and prompts.
143
+
144
+ \b
145
+ General Usage:
146
+ opik import WORKSPACE TYPE NAME [OPTIONS]
147
+
148
+ \b
149
+ Data Types:
150
+ project Import projects from path/projects/ (default: opik_exports)
151
+ dataset Import datasets from path/datasets/ (default: opik_exports)
152
+ experiment Import experiments from path/experiments/ (default: opik_exports)
153
+ prompt Import prompts from path/prompts/ (default: opik_exports)
154
+
155
+ \b
156
+ Common Options:
157
+ --path, -p Directory containing exported data (default: opik_exports)
158
+ --dry-run Preview what would be imported without actually importing
159
+ --debug Show detailed information about the import process
160
+
161
+ \b
162
+ Examples:
163
+ # Preview an experiment that would be imported
164
+ opik import my-workspace experiment "my-experiment" --dry-run
165
+
166
+ # Import a specific project
167
+ opik import my-workspace project "my-project"
168
+
169
+ # Import a specific dataset
170
+ opik import my-workspace dataset "my-dataset"
171
+
172
+ # Import from a custom path
173
+ opik import my-workspace project "my-project" --path ./custom-exports/
174
+ """
175
+ ctx.ensure_object(dict)
176
+ ctx.obj["workspace"] = workspace
177
+ # Use API key from this command or from parent context
178
+ ctx.obj["api_key"] = api_key or (
179
+ ctx.parent.obj.get("api_key") if ctx.parent and ctx.parent.obj else None
180
+ )
181
+
182
+
183
+ # Set subcommand metavar to ITEM instead of COMMAND
184
+ import_group.subcommand_metavar = "ITEM [ARGS]..."
185
+
186
+
187
+ def format_commands(
188
+ self: click.Group, ctx: click.Context, formatter: click.HelpFormatter
189
+ ) -> None:
190
+ """Override to change 'Commands' heading to 'Items'."""
191
+ commands = []
192
+ for subcommand in self.list_commands(ctx):
193
+ cmd = self.get_command(ctx, subcommand)
194
+ if cmd is None or cmd.hidden:
195
+ continue
196
+ commands.append((subcommand, cmd))
197
+
198
+ if len(commands):
199
+ limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
200
+ rows = []
201
+ for subcommand, cmd in commands:
202
+ help = cmd.get_short_help_str(limit)
203
+ rows.append((subcommand, help))
204
+
205
+ if rows:
206
+ with formatter.section("Items"):
207
+ formatter.write_dl(rows)
208
+
209
+
210
+ # Override format_commands method
211
+ setattr(
212
+ import_group,
213
+ "format_commands",
214
+ format_commands.__get__(import_group, type(import_group)),
215
+ )
216
+
217
+
218
+ @import_group.command(name="dataset")
219
+ @click.argument("name", type=str)
220
+ @click.option(
221
+ "--path",
222
+ "-p",
223
+ type=click.Path(file_okay=False, dir_okay=True, readable=True),
224
+ default="opik_exports",
225
+ help="Directory containing exported data. Defaults to opik_exports.",
226
+ )
227
+ @click.option(
228
+ "--dry-run",
229
+ is_flag=True,
230
+ help="Show what would be imported without actually importing. Use this to preview datasets before importing.",
231
+ )
232
+ @click.option(
233
+ "--debug",
234
+ is_flag=True,
235
+ help="Enable debug output to show detailed information about the import process.",
236
+ )
237
+ @click.pass_context
238
+ def import_dataset(
239
+ ctx: click.Context,
240
+ name: str,
241
+ path: str,
242
+ dry_run: bool,
243
+ debug: bool,
244
+ ) -> None:
245
+ """Import datasets from workspace/datasets directory.
246
+
247
+ This command imports datasets matching the specified name from the path/datasets/ directory.
248
+ The name is matched using case-insensitive substring matching.
249
+
250
+ \b
251
+ Examples:
252
+ \b
253
+ # Preview a dataset that would be imported
254
+ opik import my-workspace dataset "my-dataset" --dry-run
255
+ \b
256
+ # Import a specific dataset
257
+ opik import my-workspace dataset "my-dataset"
258
+ \b
259
+ # Import datasets containing "training" in the name
260
+ opik import my-workspace dataset "training"
261
+ \b
262
+ # Import from a custom path
263
+ opik import my-workspace dataset "my-dataset" --path ./custom-exports/
264
+ """
265
+ workspace = ctx.obj["workspace"]
266
+ api_key = ctx.obj.get("api_key") if ctx.obj else None
267
+ _import_by_type("dataset", path, workspace, dry_run, name, debug, api_key=api_key)
268
+
269
+
270
+ @import_group.command(name="project")
271
+ @click.argument("name", type=str)
272
+ @click.option(
273
+ "--path",
274
+ "-p",
275
+ type=click.Path(file_okay=False, dir_okay=True, readable=True),
276
+ default="opik_exports",
277
+ help="Directory containing exported data. Defaults to opik_exports.",
278
+ )
279
+ @click.option(
280
+ "--dry-run",
281
+ is_flag=True,
282
+ help="Show what would be imported without actually importing. Use this to preview projects before importing.",
283
+ )
284
+ @click.option(
285
+ "--debug",
286
+ is_flag=True,
287
+ help="Enable debug output to show detailed information about the import process.",
288
+ )
289
+ @click.pass_context
290
+ def import_project(
291
+ ctx: click.Context,
292
+ name: str,
293
+ path: str,
294
+ dry_run: bool,
295
+ debug: bool,
296
+ ) -> None:
297
+ """Import projects from workspace/projects directory.
298
+
299
+ This command imports projects matching the specified name from the path/projects/ directory.
300
+ The name is matched using case-insensitive substring matching.
301
+
302
+ \b
303
+ Examples:
304
+ \b
305
+ # Preview a project that would be imported
306
+ opik import my-workspace project "my-project" --dry-run
307
+ \b
308
+ # Import a specific project
309
+ opik import my-workspace project "my-project"
310
+ \b
311
+ # Import projects containing "test" in the name
312
+ opik import my-workspace project "test"
313
+ \b
314
+ # Import projects with debug output
315
+ opik import my-workspace project "my-project" --debug
316
+ \b
317
+ # Import from a custom path
318
+ opik import my-workspace project "my-project" --path ./custom-exports/
319
+ """
320
+ workspace = ctx.obj["workspace"]
321
+ api_key = ctx.obj.get("api_key") if ctx.obj else None
322
+ _import_by_type(
323
+ "project",
324
+ path,
325
+ workspace,
326
+ dry_run,
327
+ name,
328
+ debug,
329
+ True, # Always recreate experiments when importing projects
330
+ api_key=api_key,
331
+ )
332
+
333
+
334
+ @import_group.command(name="experiment")
335
+ @click.argument("name", type=str)
336
+ @click.option(
337
+ "--path",
338
+ "-p",
339
+ type=click.Path(file_okay=False, dir_okay=True, readable=True),
340
+ default="opik_exports",
341
+ help="Directory containing exported data. Defaults to opik_exports.",
342
+ )
343
+ @click.option(
344
+ "--dry-run",
345
+ is_flag=True,
346
+ help="Show what would be imported without actually importing.",
347
+ )
348
+ @click.option(
349
+ "--debug",
350
+ is_flag=True,
351
+ help="Enable debug output to show detailed information about the import process.",
352
+ )
353
+ @click.pass_context
354
+ def import_experiment(
355
+ ctx: click.Context,
356
+ name: str,
357
+ path: str,
358
+ dry_run: bool,
359
+ debug: bool,
360
+ ) -> None:
361
+ """Import experiments from workspace/experiments directory.
362
+
363
+ This command imports experiments matching the specified name from the path/experiments/ directory.
364
+ The name is matched using case-insensitive substring matching.
365
+
366
+ \b
367
+ Examples:
368
+ \b
369
+ # Preview an experiment that would be imported
370
+ opik import my-workspace experiment "my-experiment" --dry-run
371
+ \b
372
+ # Import a specific experiment
373
+ opik import my-workspace experiment "my-experiment"
374
+ \b
375
+ # Import from a custom path
376
+ opik import my-workspace experiment "my-experiment" --path ./custom-exports/
377
+ """
378
+ workspace = ctx.obj["workspace"]
379
+ api_key = ctx.obj.get("api_key") if ctx.obj else None
380
+ # Always recreate experiments when importing
381
+ _import_by_type(
382
+ "experiment",
383
+ path,
384
+ workspace,
385
+ dry_run,
386
+ name,
387
+ debug,
388
+ True,
389
+ api_key=api_key,
390
+ )
391
+
392
+
393
+ @import_group.command(name="prompt")
394
+ @click.argument("name", type=str)
395
+ @click.option(
396
+ "--path",
397
+ "-p",
398
+ type=click.Path(file_okay=False, dir_okay=True, readable=True),
399
+ default="opik_exports",
400
+ help="Directory containing exported data. Defaults to opik_exports.",
401
+ )
402
+ @click.option(
403
+ "--dry-run",
404
+ is_flag=True,
405
+ help="Show what would be imported without actually importing.",
406
+ )
407
+ @click.option(
408
+ "--debug",
409
+ is_flag=True,
410
+ help="Enable debug output to show detailed information about the import process.",
411
+ )
412
+ @click.pass_context
413
+ def import_prompt(
414
+ ctx: click.Context,
415
+ name: str,
416
+ path: str,
417
+ dry_run: bool,
418
+ debug: bool,
419
+ ) -> None:
420
+ """Import prompts from workspace/prompts directory.
421
+
422
+ This command imports prompts matching the specified name from the path/prompts/ directory.
423
+ The name is matched using case-insensitive substring matching.
424
+
425
+ \b
426
+ Examples:
427
+ \b
428
+ # Preview a prompt that would be imported
429
+ opik import my-workspace prompt "my-prompt" --dry-run
430
+ \b
431
+ # Import a specific prompt
432
+ opik import my-workspace prompt "my-prompt"
433
+ \b
434
+ # Import from a custom path
435
+ opik import my-workspace prompt "my-prompt" --path ./custom-exports/
436
+ """
437
+ workspace = ctx.obj["workspace"]
438
+ api_key = ctx.obj.get("api_key") if ctx.obj else None
439
+ _import_by_type("prompt", path, workspace, dry_run, name, debug, api_key=api_key)
@@ -0,0 +1,143 @@
1
+ """Dataset import functionality."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, Optional
6
+
7
+ import opik
8
+ from rich.console import Console
9
+
10
+ from .utils import matches_name_pattern
11
+
12
+ console = Console()
13
+
14
+
15
+ def import_datasets_from_directory(
16
+ client: opik.Opik,
17
+ source_dir: Path,
18
+ dry_run: bool,
19
+ name_pattern: Optional[str],
20
+ debug: bool,
21
+ ) -> Dict[str, int]:
22
+ """Import datasets from a directory.
23
+
24
+ Returns:
25
+ Dictionary with keys: 'datasets', 'datasets_skipped', 'datasets_errors'
26
+ """
27
+ try:
28
+ dataset_files = list(source_dir.glob("dataset_*.json"))
29
+
30
+ if not dataset_files:
31
+ console.print("[yellow]No dataset files found in the directory[/yellow]")
32
+ return {"datasets": 0, "datasets_skipped": 0, "datasets_errors": 0}
33
+
34
+ imported_count = 0
35
+ skipped_count = 0
36
+ error_count = 0
37
+ for dataset_file in dataset_files:
38
+ try:
39
+ with open(dataset_file, "r", encoding="utf-8") as f:
40
+ dataset_data = json.load(f)
41
+
42
+ # Handle two export formats:
43
+ # 1. {"name": "...", "items": [...]} - from export_single_dataset
44
+ # 2. {"dataset": {"name": "...", "id": "..."}, "items": [...]} - from export_experiment_datasets
45
+ dataset_name = dataset_data.get("name") or (
46
+ dataset_data.get("dataset", {}).get("name")
47
+ if dataset_data.get("dataset")
48
+ else None
49
+ )
50
+
51
+ # Check if name is missing or empty
52
+ if not dataset_name or (
53
+ isinstance(dataset_name, str) and not dataset_name.strip()
54
+ ):
55
+ console.print(
56
+ f"[red]Error: Dataset file {dataset_file.name} is missing or has an empty name field[/red]"
57
+ )
58
+ error_count += 1
59
+ continue
60
+
61
+ # Strip whitespace from name
62
+ dataset_name = dataset_name.strip()
63
+
64
+ # Filter by name pattern if specified
65
+ if name_pattern and not matches_name_pattern(
66
+ dataset_name, name_pattern
67
+ ):
68
+ if debug:
69
+ console.print(
70
+ f"[blue]Skipping dataset {dataset_name} (doesn't match pattern)[/blue]"
71
+ )
72
+ skipped_count += 1
73
+ continue
74
+
75
+ if dry_run:
76
+ console.print(f"[blue]Would import dataset: {dataset_name}[/blue]")
77
+ imported_count += 1
78
+ continue
79
+
80
+ if debug:
81
+ console.print(f"[blue]Importing dataset: {dataset_name}[/blue]")
82
+
83
+ # Get or create dataset (handles case where dataset already exists)
84
+ try:
85
+ dataset = client.get_dataset(dataset_name)
86
+ if debug:
87
+ console.print(
88
+ f"[blue]Dataset '{dataset_name}' already exists, using existing dataset[/blue]"
89
+ )
90
+ except Exception:
91
+ # Dataset doesn't exist, create it
92
+ dataset = client.create_dataset(name=dataset_name)
93
+ if debug:
94
+ console.print(
95
+ f"[blue]Created new dataset: {dataset_name}[/blue]"
96
+ )
97
+
98
+ # Import dataset items
99
+ items = dataset_data.get("items", [])
100
+ if items:
101
+ # Remove 'id' field from items before inserting (IDs are auto-generated)
102
+ items_to_insert = []
103
+ for item in items:
104
+ if isinstance(item, dict):
105
+ # Create a copy without the 'id' field
106
+ item_copy = {k: v for k, v in item.items() if k != "id"}
107
+ items_to_insert.append(item_copy)
108
+ else:
109
+ items_to_insert.append(item)
110
+
111
+ if items_to_insert:
112
+ dataset.insert(items_to_insert)
113
+ if debug:
114
+ console.print(
115
+ f"[blue]Inserted {len(items_to_insert)} items into dataset '{dataset_name}'[/blue]"
116
+ )
117
+ else:
118
+ console.print(
119
+ f"[yellow]Warning: No items to insert for dataset '{dataset_name}' (all items were empty after removing 'id' field)[/yellow]"
120
+ )
121
+
122
+ imported_count += 1
123
+ if debug:
124
+ console.print(
125
+ f"[green]Imported dataset: {dataset_name} with {len(items)} items[/green]"
126
+ )
127
+
128
+ except Exception as e:
129
+ console.print(
130
+ f"[red]Error importing dataset from {dataset_file.name}: {e}[/red]"
131
+ )
132
+ error_count += 1
133
+ continue
134
+
135
+ return {
136
+ "datasets": imported_count,
137
+ "datasets_skipped": skipped_count,
138
+ "datasets_errors": error_count,
139
+ }
140
+
141
+ except Exception as e:
142
+ console.print(f"[red]Error importing datasets: {e}[/red]")
143
+ return {"datasets": 0, "datasets_skipped": 0, "datasets_errors": 1}