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,280 @@
1
+ """Common utilities for import functionality."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import opik
6
+ from opik.types import FeedbackScoreDict
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ console = Console()
11
+
12
+
13
+ def debug_print(message: str, debug: bool) -> None:
14
+ """Print debug message only if debug is enabled."""
15
+ if debug:
16
+ console.print(f"[blue]{message}[/blue]")
17
+
18
+
19
+ def matches_name_pattern(name: str, pattern: Optional[str]) -> bool:
20
+ """Check if a name matches the given pattern using case-insensitive substring matching."""
21
+ if pattern is None:
22
+ return True
23
+ # Simple string matching - check if pattern is contained in name (case-insensitive)
24
+ return pattern.lower() in name.lower()
25
+
26
+
27
+ def translate_trace_id(
28
+ original_trace_id: str, trace_id_map: Dict[str, str]
29
+ ) -> Optional[str]:
30
+ """Translate an original trace id from export to the newly created id.
31
+
32
+ Args:
33
+ original_trace_id: The original trace ID from the export
34
+ trace_id_map: Mapping from original trace IDs to new trace IDs (required, can be empty)
35
+
36
+ Returns:
37
+ The new trace ID if found in the map, None otherwise.
38
+ """
39
+ return trace_id_map.get(original_trace_id)
40
+
41
+
42
+ def find_dataset_item_by_content(
43
+ dataset: opik.Dataset, expected_content: Dict[str, Any]
44
+ ) -> Optional[str]:
45
+ """Find a dataset item by matching its content.
46
+
47
+ Compares all fields in expected_content (excluding 'id') with dataset items.
48
+ """
49
+ try:
50
+ items = dataset.get_items()
51
+ # Remove 'id' from expected_content for comparison
52
+ expected_without_id = {k: v for k, v in expected_content.items() if k != "id"}
53
+
54
+ for item in items:
55
+ # Compare all fields (excluding 'id')
56
+ item_without_id = {k: v for k, v in item.items() if k != "id"}
57
+ if item_without_id == expected_without_id:
58
+ return item.get("id")
59
+ except Exception:
60
+ pass
61
+ return None
62
+
63
+
64
+ def create_dataset_item(dataset: opik.Dataset, item_data: Dict[str, Any]) -> str:
65
+ """Create a dataset item and return its ID.
66
+
67
+ Uses all fields from item_data (except 'id') to support flexible dataset schemas.
68
+ """
69
+ # Use all fields from item_data except 'id' (which is handled separately)
70
+ new_item = {k: v for k, v in item_data.items() if k != "id" and v is not None}
71
+
72
+ # Ensure item is not empty (backend requires non-empty data)
73
+ if not new_item:
74
+ raise ValueError(
75
+ "Dataset item data is empty - at least one field must be provided"
76
+ )
77
+
78
+ dataset.insert([new_item])
79
+
80
+ # Find the newly created item by matching all fields
81
+ items = dataset.get_items()
82
+ for item in items:
83
+ # Compare all fields (excluding 'id')
84
+ item_without_id = {k: v for k, v in item.items() if k != "id"}
85
+ if item_without_id == new_item:
86
+ item_id = item.get("id")
87
+ if item_id is not None:
88
+ return item_id
89
+
90
+ dataset_name = getattr(dataset, "name", None)
91
+ dataset_info = f", Dataset: {dataset_name!r}" if dataset_name else ""
92
+ raise Exception(
93
+ f"Failed to create dataset item. " f"Content: {new_item!r}{dataset_info}"
94
+ )
95
+
96
+
97
+ def handle_trace_reference(item_data: Dict[str, Any]) -> Optional[str]:
98
+ """Handle trace references from deduplicated exports."""
99
+ trace_reference = item_data.get("trace_reference")
100
+ if trace_reference:
101
+ trace_id = trace_reference.get("trace_id")
102
+ return trace_id
103
+
104
+ # Fall back to direct trace_id
105
+ return item_data.get("trace_id")
106
+
107
+
108
+ def clean_feedback_scores(
109
+ feedback_scores: Optional[List[Dict[str, Any]]],
110
+ ) -> Optional[List[FeedbackScoreDict]]:
111
+ """Clean feedback scores by removing fields that are not allowed when creating them.
112
+
113
+ Exported feedback scores include read-only fields like 'source', 'created_at', etc.
114
+ that must be removed before importing.
115
+
116
+ Allowed fields: name, value, category_name, reason
117
+ """
118
+ if not feedback_scores:
119
+ return None
120
+
121
+ cleaned_scores: List[FeedbackScoreDict] = []
122
+ for score in feedback_scores:
123
+ if not isinstance(score, dict):
124
+ continue
125
+
126
+ # Only keep allowed fields
127
+ name = score.get("name")
128
+ value = score.get("value")
129
+
130
+ # Only add if name and value are present
131
+ if not name or value is None:
132
+ continue
133
+
134
+ # Construct FeedbackScoreDict with required fields
135
+ cleaned_score: FeedbackScoreDict = {
136
+ "name": name,
137
+ "value": value,
138
+ }
139
+
140
+ # Add optional fields if they exist
141
+ if "category_name" in score:
142
+ cleaned_score["category_name"] = score.get("category_name")
143
+ if "reason" in score:
144
+ cleaned_score["reason"] = score.get("reason")
145
+
146
+ cleaned_scores.append(cleaned_score)
147
+
148
+ return cleaned_scores if cleaned_scores else None
149
+
150
+
151
+ def print_import_summary(stats: Dict[str, int]) -> None:
152
+ """Print a nice summary table of import statistics."""
153
+ table = Table(
154
+ title="📥 Import Summary", show_header=True, header_style="bold magenta"
155
+ )
156
+ table.add_column("Type", style="cyan", no_wrap=True)
157
+ table.add_column("Imported", justify="right", style="green")
158
+ table.add_column("Skipped", justify="right", style="yellow")
159
+ table.add_column("Errors", justify="right", style="red")
160
+
161
+ # Add rows for each type
162
+ if (
163
+ stats.get("experiments", 0) > 0
164
+ or stats.get("experiments_skipped", 0) > 0
165
+ or stats.get("experiments_errors", 0) > 0
166
+ ):
167
+ imported = stats.get("experiments", 0)
168
+ skipped = stats.get("experiments_skipped", 0)
169
+ errors = stats.get("experiments_errors", 0)
170
+ table.add_row(
171
+ "🧪 Experiments",
172
+ str(imported),
173
+ str(skipped) if skipped > 0 else "",
174
+ str(errors) if errors > 0 else "",
175
+ )
176
+
177
+ if (
178
+ stats.get("datasets", 0) > 0
179
+ or stats.get("datasets_skipped", 0) > 0
180
+ or stats.get("datasets_errors", 0) > 0
181
+ ):
182
+ imported = stats.get("datasets", 0)
183
+ skipped = stats.get("datasets_skipped", 0)
184
+ errors = stats.get("datasets_errors", 0)
185
+ table.add_row(
186
+ "📊 Datasets",
187
+ str(imported),
188
+ str(skipped) if skipped > 0 else "",
189
+ str(errors) if errors > 0 else "",
190
+ )
191
+
192
+ if (
193
+ stats.get("traces", 0) > 0
194
+ or stats.get("traces_skipped", 0) > 0
195
+ or stats.get("traces_errors", 0) > 0
196
+ ):
197
+ imported = stats.get("traces", 0)
198
+ skipped = stats.get("traces_skipped", 0)
199
+ errors = stats.get("traces_errors", 0)
200
+ table.add_row(
201
+ "🔍 Traces",
202
+ str(imported),
203
+ str(skipped) if skipped > 0 else "",
204
+ str(errors) if errors > 0 else "",
205
+ )
206
+
207
+ if (
208
+ stats.get("prompts", 0) > 0
209
+ or stats.get("prompts_skipped", 0) > 0
210
+ or stats.get("prompts_errors", 0) > 0
211
+ ):
212
+ imported = stats.get("prompts", 0)
213
+ skipped = stats.get("prompts_skipped", 0)
214
+ errors = stats.get("prompts_errors", 0)
215
+ table.add_row(
216
+ "💬 Prompts",
217
+ str(imported),
218
+ str(skipped) if skipped > 0 else "",
219
+ str(errors) if errors > 0 else "",
220
+ )
221
+
222
+ if (
223
+ stats.get("projects", 0) > 0
224
+ or stats.get("projects_skipped", 0) > 0
225
+ or stats.get("projects_errors", 0) > 0
226
+ ):
227
+ imported = stats.get("projects", 0)
228
+ skipped = stats.get("projects_skipped", 0)
229
+ errors = stats.get("projects_errors", 0)
230
+ table.add_row(
231
+ "📁 Projects",
232
+ str(imported),
233
+ str(skipped) if skipped > 0 else "",
234
+ str(errors) if errors > 0 else "",
235
+ )
236
+
237
+ # Calculate totals
238
+ total_imported = sum(
239
+ [
240
+ stats.get(key, 0)
241
+ for key in ["experiments", "datasets", "traces", "prompts", "projects"]
242
+ ]
243
+ )
244
+ total_skipped = sum(
245
+ [
246
+ stats.get(key, 0)
247
+ for key in [
248
+ "experiments_skipped",
249
+ "datasets_skipped",
250
+ "traces_skipped",
251
+ "prompts_skipped",
252
+ "projects_skipped",
253
+ ]
254
+ ]
255
+ )
256
+ total_errors = sum(
257
+ [
258
+ stats.get(key, 0)
259
+ for key in [
260
+ "experiments_errors",
261
+ "datasets_errors",
262
+ "traces_errors",
263
+ "prompts_errors",
264
+ "projects_errors",
265
+ ]
266
+ ]
267
+ )
268
+
269
+ table.add_row("", "", "", "", style="bold")
270
+ table.add_row(
271
+ "📦 Total",
272
+ str(total_imported),
273
+ str(total_skipped) if total_skipped > 0 else "",
274
+ str(total_errors) if total_errors > 0 else "",
275
+ style="bold green",
276
+ )
277
+
278
+ console.print()
279
+ console.print(table)
280
+ console.print()
opik/cli/main.py CHANGED
@@ -5,12 +5,13 @@ from typing import Optional
5
5
 
6
6
  import click
7
7
 
8
- from .configure import configure
9
- from .export import export
10
- from .healthcheck import healthcheck
11
- from .import_command import import_data
12
- from .proxy import proxy
13
- from .usage_report import usage_report # Import from usage_report package
8
+ from . import configure
9
+ from . import exports
10
+ from . import harbor
11
+ from . import healthcheck
12
+ from . import imports
13
+ from . import proxy
14
+ from . import usage_report
14
15
 
15
16
  __version__: str = "0.0.0+dev"
16
17
  try:
@@ -39,9 +40,10 @@ def cli(ctx: click.Context, api_key: Optional[str]) -> None:
39
40
 
40
41
 
41
42
  # Register all commands
42
- cli.add_command(configure)
43
- cli.add_command(proxy)
44
- cli.add_command(healthcheck)
45
- cli.add_command(export)
46
- cli.add_command(import_data)
47
- cli.add_command(usage_report)
43
+ cli.add_command(configure.configure)
44
+ cli.add_command(proxy.proxy)
45
+ cli.add_command(healthcheck.healthcheck)
46
+ cli.add_command(exports.export_group)
47
+ cli.add_command(imports.import_group)
48
+ cli.add_command(usage_report.usage_report)
49
+ cli.add_command(harbor.harbor)
opik/config.py CHANGED
@@ -227,6 +227,17 @@ class OpikConfig(pydantic_settings.BaseSettings):
227
227
  For shorter traces/spans, it is recommended to keep this setting disabled to minimize data logging overhead.
228
228
  """
229
229
 
230
+ min_base64_embedded_attachment_size: int = 256_000
231
+ """
232
+ Minimum size of the attachment string in bytes that will be kept embedded in the base64 string. (250KB)
233
+ Attachments larger than this size will be extracted from inputs/outputs of spans/traces and uploaded to the Opik backend.
234
+ """
235
+
236
+ is_attachment_extraction_active: bool = False
237
+ """
238
+ If set to True, attachments larger than `min_base64_embedded_attachment_size` will be extracted from spans/traces and uploaded to the Opik backend.
239
+ """
240
+
230
241
  @property
231
242
  def config_file_fullpath(self) -> pathlib.Path:
232
243
  config_file_path = os.getenv("OPIK_CONFIG_PATH", CONFIG_FILE_PATH_DEFAULT)
@@ -257,7 +268,7 @@ class OpikConfig(pydantic_settings.BaseSettings):
257
268
  return url_helpers.get_base_url(self.url_override) + "guardrails/"
258
269
 
259
270
  @pydantic.model_validator(mode="after")
260
- def _set_url_override_from_api_key(self) -> "OpikConfig":
271
+ def _set_url_override_from_api_key(self) -> OpikConfig:
261
272
  url_was_not_provided = (
262
273
  "url_override" not in self.model_fields_set or self.url_override is None
263
274
  )
opik/datetime_helpers.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ from typing import Optional
2
3
 
3
4
 
4
5
  def local_timestamp() -> datetime.datetime:
@@ -8,3 +9,14 @@ def local_timestamp() -> datetime.datetime:
8
9
 
9
10
  def datetime_to_iso8601(value: datetime.datetime) -> str:
10
11
  return value.isoformat()
12
+
13
+
14
+ def parse_iso_timestamp(timestamp_str: Optional[str]) -> Optional[datetime.datetime]:
15
+ """Parse an ISO 8601 timestamp string to datetime."""
16
+ if timestamp_str is None:
17
+ return None
18
+ try:
19
+ timestamp_str = timestamp_str.replace("Z", "+00:00")
20
+ return datetime.datetime.fromisoformat(timestamp_str)
21
+ except (ValueError, TypeError):
22
+ return None
@@ -2,7 +2,7 @@ import dataclasses
2
2
  from typing import Any, Callable, Dict, List, Optional, Union
3
3
 
4
4
  from .. import datetime_helpers, llm_usage
5
- from ..api_objects import helpers, span
5
+ from ..api_objects import helpers, span, attachment
6
6
  from ..types import ErrorInfoDict, SpanType, DistributedTraceHeadersDict
7
7
 
8
8
 
@@ -35,6 +35,7 @@ class EndSpanParameters(BaseArguments):
35
35
  provider: Optional[str] = None
36
36
  error_info: Optional[ErrorInfoDict] = None
37
37
  total_cost: Optional[float] = None
38
+ attachments: Optional[List[attachment.Attachment]] = None
38
39
 
39
40
 
40
41
  @dataclasses.dataclass
@@ -52,6 +53,7 @@ class StartSpanParameters(BaseArguments):
52
53
  project_name: Optional[str] = None
53
54
  model: Optional[str] = None
54
55
  provider: Optional[str] = None
56
+ thread_id: Optional[str] = None # used for traces only
55
57
 
56
58
 
57
59
  @dataclasses.dataclass
@@ -70,6 +72,7 @@ class TrackOptions(BaseArguments):
70
72
  generations_aggregator: Optional[Callable[[List[Any]], Any]]
71
73
  flush: bool
72
74
  project_name: Optional[str]
75
+ create_duplicate_root_span: bool
73
76
 
74
77
 
75
78
  def create_span_data(