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.
- opik/api_objects/attachment/attachment_context.py +36 -0
- opik/api_objects/attachment/attachments_extractor.py +153 -0
- opik/api_objects/attachment/client.py +1 -0
- opik/api_objects/attachment/converters.py +2 -0
- opik/api_objects/attachment/decoder.py +18 -0
- opik/api_objects/attachment/decoder_base64.py +83 -0
- opik/api_objects/attachment/decoder_helpers.py +137 -0
- opik/api_objects/constants.py +2 -0
- opik/api_objects/dataset/dataset.py +133 -40
- opik/api_objects/dataset/rest_operations.py +2 -0
- opik/api_objects/experiment/experiment.py +6 -0
- opik/api_objects/helpers.py +8 -4
- opik/api_objects/local_recording.py +6 -5
- opik/api_objects/observation_data.py +101 -0
- opik/api_objects/opik_client.py +78 -45
- opik/api_objects/opik_query_language.py +9 -3
- opik/api_objects/prompt/chat/chat_prompt.py +18 -1
- opik/api_objects/prompt/client.py +8 -1
- opik/api_objects/span/span_data.py +3 -88
- opik/api_objects/threads/threads_client.py +7 -4
- opik/api_objects/trace/trace_data.py +3 -74
- opik/api_objects/validation_helpers.py +3 -3
- opik/cli/exports/__init__.py +131 -0
- opik/cli/exports/dataset.py +278 -0
- opik/cli/exports/experiment.py +784 -0
- opik/cli/exports/project.py +685 -0
- opik/cli/exports/prompt.py +578 -0
- opik/cli/exports/utils.py +406 -0
- opik/cli/harbor.py +39 -0
- opik/cli/imports/__init__.py +439 -0
- opik/cli/imports/dataset.py +143 -0
- opik/cli/imports/experiment.py +1192 -0
- opik/cli/imports/project.py +262 -0
- opik/cli/imports/prompt.py +177 -0
- opik/cli/imports/utils.py +280 -0
- opik/cli/main.py +14 -12
- opik/config.py +12 -1
- opik/datetime_helpers.py +12 -0
- opik/decorator/arguments_helpers.py +4 -1
- opik/decorator/base_track_decorator.py +111 -37
- opik/decorator/context_manager/span_context_manager.py +5 -1
- opik/decorator/generator_wrappers.py +5 -4
- opik/decorator/span_creation_handler.py +13 -4
- opik/evaluation/engine/engine.py +111 -28
- opik/evaluation/engine/evaluation_tasks_executor.py +71 -19
- opik/evaluation/evaluator.py +12 -0
- opik/evaluation/metrics/conversation/llm_judges/conversational_coherence/metric.py +3 -1
- opik/evaluation/metrics/conversation/llm_judges/session_completeness/metric.py +3 -1
- opik/evaluation/metrics/conversation/llm_judges/user_frustration/metric.py +3 -1
- opik/evaluation/metrics/heuristics/equals.py +11 -7
- opik/evaluation/metrics/llm_judges/answer_relevance/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/context_precision/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/context_recall/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/factuality/metric.py +1 -1
- opik/evaluation/metrics/llm_judges/g_eval/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/hallucination/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/moderation/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/structure_output_compliance/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/syc_eval/metric.py +4 -2
- opik/evaluation/metrics/llm_judges/trajectory_accuracy/metric.py +3 -1
- opik/evaluation/metrics/llm_judges/usefulness/metric.py +3 -1
- opik/evaluation/metrics/ragas_metric.py +43 -23
- opik/evaluation/models/litellm/litellm_chat_model.py +7 -2
- opik/evaluation/models/litellm/util.py +4 -20
- opik/evaluation/models/models_factory.py +19 -5
- opik/evaluation/rest_operations.py +3 -3
- opik/evaluation/threads/helpers.py +3 -2
- opik/file_upload/file_uploader.py +13 -0
- opik/file_upload/upload_options.py +2 -0
- opik/integrations/adk/legacy_opik_tracer.py +9 -11
- opik/integrations/adk/opik_tracer.py +2 -2
- opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +2 -2
- opik/integrations/dspy/callback.py +100 -14
- opik/integrations/dspy/parsers.py +168 -0
- opik/integrations/harbor/__init__.py +17 -0
- opik/integrations/harbor/experiment_service.py +269 -0
- opik/integrations/harbor/opik_tracker.py +528 -0
- opik/integrations/haystack/opik_tracer.py +2 -2
- opik/integrations/langchain/__init__.py +15 -2
- opik/integrations/langchain/langgraph_tracer_injector.py +88 -0
- opik/integrations/langchain/opik_tracer.py +258 -160
- opik/integrations/langchain/provider_usage_extractors/langchain_run_helpers/helpers.py +7 -4
- opik/integrations/llama_index/callback.py +43 -6
- opik/integrations/openai/agents/opik_tracing_processor.py +8 -10
- opik/integrations/openai/opik_tracker.py +99 -4
- opik/integrations/openai/videos/__init__.py +9 -0
- opik/integrations/openai/videos/binary_response_write_to_file_decorator.py +88 -0
- opik/integrations/openai/videos/videos_create_decorator.py +159 -0
- opik/integrations/openai/videos/videos_download_decorator.py +110 -0
- opik/message_processing/batching/base_batcher.py +14 -21
- opik/message_processing/batching/batch_manager.py +22 -10
- opik/message_processing/batching/batchers.py +32 -40
- opik/message_processing/batching/flushing_thread.py +0 -3
- opik/message_processing/emulation/emulator_message_processor.py +36 -1
- opik/message_processing/emulation/models.py +21 -0
- opik/message_processing/messages.py +9 -0
- opik/message_processing/preprocessing/__init__.py +0 -0
- opik/message_processing/preprocessing/attachments_preprocessor.py +70 -0
- opik/message_processing/preprocessing/batching_preprocessor.py +53 -0
- opik/message_processing/preprocessing/constants.py +1 -0
- opik/message_processing/preprocessing/file_upload_preprocessor.py +38 -0
- opik/message_processing/preprocessing/preprocessor.py +36 -0
- opik/message_processing/processors/__init__.py +0 -0
- opik/message_processing/processors/attachments_extraction_processor.py +146 -0
- opik/message_processing/{message_processors.py → processors/message_processors.py} +15 -1
- opik/message_processing/{message_processors_chain.py → processors/message_processors_chain.py} +3 -2
- opik/message_processing/{online_message_processor.py → processors/online_message_processor.py} +11 -9
- opik/message_processing/queue_consumer.py +4 -2
- opik/message_processing/streamer.py +71 -33
- opik/message_processing/streamer_constructors.py +36 -8
- opik/plugins/pytest/experiment_runner.py +1 -1
- opik/plugins/pytest/hooks.py +5 -3
- opik/rest_api/__init__.py +38 -0
- opik/rest_api/datasets/client.py +249 -148
- opik/rest_api/datasets/raw_client.py +356 -217
- opik/rest_api/experiments/client.py +26 -0
- opik/rest_api/experiments/raw_client.py +26 -0
- opik/rest_api/llm_provider_key/client.py +4 -4
- opik/rest_api/llm_provider_key/raw_client.py +4 -4
- opik/rest_api/llm_provider_key/types/provider_api_key_write_provider.py +2 -1
- opik/rest_api/manual_evaluation/client.py +101 -0
- opik/rest_api/manual_evaluation/raw_client.py +172 -0
- opik/rest_api/optimizations/client.py +0 -166
- opik/rest_api/optimizations/raw_client.py +0 -248
- opik/rest_api/projects/client.py +9 -0
- opik/rest_api/projects/raw_client.py +13 -0
- opik/rest_api/projects/types/project_metric_request_public_metric_type.py +4 -0
- opik/rest_api/prompts/client.py +130 -2
- opik/rest_api/prompts/raw_client.py +175 -0
- opik/rest_api/traces/client.py +101 -0
- opik/rest_api/traces/raw_client.py +120 -0
- opik/rest_api/types/__init__.py +46 -0
- opik/rest_api/types/audio_url.py +19 -0
- opik/rest_api/types/audio_url_public.py +19 -0
- opik/rest_api/types/audio_url_write.py +19 -0
- opik/rest_api/types/automation_rule_evaluator.py +38 -2
- opik/rest_api/types/automation_rule_evaluator_object_object_public.py +33 -2
- opik/rest_api/types/automation_rule_evaluator_public.py +33 -2
- opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_public.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_write.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_update.py +27 -1
- opik/rest_api/types/automation_rule_evaluator_update_span_user_defined_metric_python.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_write.py +27 -1
- opik/rest_api/types/dataset_item.py +1 -1
- opik/rest_api/types/dataset_item_batch.py +4 -0
- opik/rest_api/types/dataset_item_changes_public.py +5 -0
- opik/rest_api/types/dataset_item_compare.py +1 -1
- opik/rest_api/types/dataset_item_filter.py +4 -0
- opik/rest_api/types/dataset_item_page_compare.py +0 -1
- opik/rest_api/types/dataset_item_page_public.py +0 -1
- opik/rest_api/types/dataset_item_public.py +1 -1
- opik/rest_api/types/dataset_version_public.py +5 -0
- opik/rest_api/types/dataset_version_summary.py +5 -0
- opik/rest_api/types/dataset_version_summary_public.py +5 -0
- opik/rest_api/types/experiment.py +9 -0
- opik/rest_api/types/experiment_public.py +9 -0
- opik/rest_api/types/llm_as_judge_message_content.py +2 -0
- opik/rest_api/types/llm_as_judge_message_content_public.py +2 -0
- opik/rest_api/types/llm_as_judge_message_content_write.py +2 -0
- opik/rest_api/types/manual_evaluation_request_entity_type.py +1 -1
- opik/rest_api/types/project.py +1 -0
- opik/rest_api/types/project_detailed.py +1 -0
- opik/rest_api/types/project_metric_response_public_metric_type.py +4 -0
- opik/rest_api/types/project_reference.py +31 -0
- opik/rest_api/types/project_reference_public.py +31 -0
- opik/rest_api/types/project_stats_summary_item.py +1 -0
- opik/rest_api/types/prompt_version.py +1 -0
- opik/rest_api/types/prompt_version_detail.py +1 -0
- opik/rest_api/types/prompt_version_page_public.py +5 -0
- opik/rest_api/types/prompt_version_public.py +1 -0
- opik/rest_api/types/prompt_version_update.py +33 -0
- opik/rest_api/types/provider_api_key.py +5 -1
- opik/rest_api/types/provider_api_key_provider.py +2 -1
- opik/rest_api/types/provider_api_key_public.py +5 -1
- opik/rest_api/types/provider_api_key_public_provider.py +2 -1
- opik/rest_api/types/service_toggles_config.py +11 -1
- opik/rest_api/types/span_user_defined_metric_python_code.py +20 -0
- opik/rest_api/types/span_user_defined_metric_python_code_public.py +20 -0
- opik/rest_api/types/span_user_defined_metric_python_code_write.py +20 -0
- opik/types.py +36 -0
- opik/validation/chat_prompt_messages.py +241 -0
- opik/validation/feedback_score.py +3 -3
- opik/validation/validator.py +28 -0
- {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/METADATA +5 -5
- {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/RECORD +190 -141
- opik/cli/export.py +0 -791
- opik/cli/import_command.py +0 -575
- {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/WHEEL +0 -0
- {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/entry_points.txt +0 -0
- {opik-1.9.41.dist-info → opik-1.9.86.dist-info}/licenses/LICENSE +0 -0
- {opik-1.9.41.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 .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
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(
|
|
46
|
-
cli.add_command(
|
|
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) ->
|
|
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(
|