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,685 @@
|
|
|
1
|
+
"""Project export functionality."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
11
|
+
|
|
12
|
+
import opik
|
|
13
|
+
from opik.rest_api.core.api_error import ApiError
|
|
14
|
+
from opik.rest_api.types.project_public import ProjectPublic
|
|
15
|
+
from .utils import (
|
|
16
|
+
debug_print,
|
|
17
|
+
matches_name_pattern,
|
|
18
|
+
should_skip_file,
|
|
19
|
+
print_export_summary,
|
|
20
|
+
trace_to_csv_rows,
|
|
21
|
+
write_csv_data,
|
|
22
|
+
write_json_data,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def export_traces(
|
|
29
|
+
client: opik.Opik,
|
|
30
|
+
project_name: str,
|
|
31
|
+
project_dir: Path,
|
|
32
|
+
max_results: int,
|
|
33
|
+
filter_string: Optional[str],
|
|
34
|
+
project_name_filter: Optional[str] = None,
|
|
35
|
+
format: str = "json",
|
|
36
|
+
debug: bool = False,
|
|
37
|
+
force: bool = False,
|
|
38
|
+
) -> tuple[int, int]:
|
|
39
|
+
"""Download traces and their spans with pagination support for large projects."""
|
|
40
|
+
if debug:
|
|
41
|
+
debug_print(
|
|
42
|
+
f"DEBUG: _export_traces called with project_name: {project_name}, project_dir: {project_dir}",
|
|
43
|
+
debug,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Validate filter syntax before using it
|
|
47
|
+
if filter_string:
|
|
48
|
+
try:
|
|
49
|
+
from opik.api_objects import opik_query_language
|
|
50
|
+
|
|
51
|
+
# Try to parse the filter to validate syntax
|
|
52
|
+
oql = opik_query_language.OpikQueryLanguage(filter_string)
|
|
53
|
+
if oql.get_filter_expressions() is None and filter_string.strip():
|
|
54
|
+
console.print(
|
|
55
|
+
f"[red]Error: Invalid filter syntax: Filter '{filter_string}' could not be parsed.[/red]"
|
|
56
|
+
)
|
|
57
|
+
console.print("[yellow]OQL filter syntax examples:[/yellow]")
|
|
58
|
+
console.print(' status = "running"')
|
|
59
|
+
console.print(' name contains "test"')
|
|
60
|
+
console.print(' start_time >= "2024-01-01T00:00:00Z"')
|
|
61
|
+
console.print(" usage.total_tokens > 1000")
|
|
62
|
+
console.print(
|
|
63
|
+
"[yellow]Note: String values must be in double quotes, use = not :[/yellow]"
|
|
64
|
+
)
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Invalid filter syntax: '{filter_string}' could not be parsed"
|
|
67
|
+
)
|
|
68
|
+
except ValueError as e:
|
|
69
|
+
# If it's already our custom error message, re-raise it
|
|
70
|
+
if "Invalid filter syntax" in str(e) and "could not be parsed" in str(e):
|
|
71
|
+
raise
|
|
72
|
+
# Otherwise, format the error nicely
|
|
73
|
+
error_msg = str(e)
|
|
74
|
+
console.print(f"[red]Error: Invalid filter syntax: {error_msg}[/red]")
|
|
75
|
+
console.print("[yellow]OQL filter syntax examples:[/yellow]")
|
|
76
|
+
console.print(' status = "running"')
|
|
77
|
+
console.print(' name contains "test"')
|
|
78
|
+
console.print(' start_time >= "2024-01-01T00:00:00Z"')
|
|
79
|
+
console.print(" usage.total_tokens > 1000")
|
|
80
|
+
console.print(
|
|
81
|
+
"[yellow]Note: String values must be in double quotes, use = not :[/yellow]"
|
|
82
|
+
)
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
exported_count = 0
|
|
86
|
+
skipped_count = 0
|
|
87
|
+
page_size = min(100, max_results) # Process in smaller batches
|
|
88
|
+
current_page = 1
|
|
89
|
+
total_processed = 0
|
|
90
|
+
|
|
91
|
+
with Progress(
|
|
92
|
+
SpinnerColumn(),
|
|
93
|
+
TextColumn("[progress.description]{task.description}"),
|
|
94
|
+
console=console,
|
|
95
|
+
) as progress:
|
|
96
|
+
task = progress.add_task("Searching for traces...", total=None)
|
|
97
|
+
|
|
98
|
+
while total_processed < max_results:
|
|
99
|
+
# Calculate how many traces to fetch in this batch
|
|
100
|
+
remaining = max_results - total_processed
|
|
101
|
+
current_page_size = min(page_size, remaining)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
if debug:
|
|
105
|
+
debug_print(
|
|
106
|
+
f"DEBUG: Getting traces by project with project_name: {project_name}, filter: {filter_string}, page: {current_page}, size: {current_page_size}",
|
|
107
|
+
debug,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Parse filter to JSON format - backend expects JSON, not OQL string
|
|
111
|
+
parsed_filter = None
|
|
112
|
+
if filter_string:
|
|
113
|
+
from opik.api_objects import opik_query_language
|
|
114
|
+
|
|
115
|
+
oql = opik_query_language.OpikQueryLanguage(filter_string)
|
|
116
|
+
parsed_filter = oql.parsed_filters # This is the JSON string
|
|
117
|
+
if debug:
|
|
118
|
+
debug_print(
|
|
119
|
+
f"DEBUG: Parsed filter to JSON: {parsed_filter}", debug
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Use get_traces_by_project for better performance when we know the project name
|
|
123
|
+
# Backend expects JSON string format for filters
|
|
124
|
+
trace_page = client.rest_client.traces.get_traces_by_project(
|
|
125
|
+
project_name=project_name,
|
|
126
|
+
filters=parsed_filter, # Pass parsed JSON string - backend expects JSON
|
|
127
|
+
page=current_page,
|
|
128
|
+
size=current_page_size,
|
|
129
|
+
truncate=False, # Don't truncate data for download
|
|
130
|
+
)
|
|
131
|
+
traces = trace_page.content or []
|
|
132
|
+
if debug:
|
|
133
|
+
debug_print(
|
|
134
|
+
f"DEBUG: Found {len(traces)} traces in project (page {current_page})",
|
|
135
|
+
debug,
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
# Check if it's an OQL parsing error and provide user-friendly message
|
|
139
|
+
error_msg = str(e)
|
|
140
|
+
if "Invalid value" in error_msg and (
|
|
141
|
+
"expected an string in double quotes" in error_msg
|
|
142
|
+
or "expected a string in double quotes" in error_msg
|
|
143
|
+
):
|
|
144
|
+
console.print(
|
|
145
|
+
"[red]Error: Invalid filter format in export query.[/red]"
|
|
146
|
+
)
|
|
147
|
+
console.print(
|
|
148
|
+
'[yellow]String values in filters must be in double quotes, e.g., status = "running"[/yellow]'
|
|
149
|
+
)
|
|
150
|
+
if debug:
|
|
151
|
+
debug_print(f"Technical details: {e}", debug)
|
|
152
|
+
else:
|
|
153
|
+
console.print(f"[red]Error searching traces: {e}[/red]")
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
if not traces:
|
|
157
|
+
# No more traces to process
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
# Update progress description to show current page
|
|
161
|
+
progress.update(
|
|
162
|
+
task,
|
|
163
|
+
description=f"Downloading traces... (page {current_page}, {len(traces)} traces)",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Filter traces by project name if specified
|
|
167
|
+
if project_name_filter:
|
|
168
|
+
original_count = len(traces)
|
|
169
|
+
traces = [
|
|
170
|
+
trace
|
|
171
|
+
for trace in traces
|
|
172
|
+
if matches_name_pattern(trace.name or "", project_name_filter)
|
|
173
|
+
]
|
|
174
|
+
if len(traces) < original_count:
|
|
175
|
+
console.print(
|
|
176
|
+
f"[blue]Filtered to {len(traces)} traces matching project '{project_name_filter}' in current batch[/blue]"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if not traces:
|
|
180
|
+
# No traces match the name pattern, but we might have more to process
|
|
181
|
+
# Use original_traces for pagination, not the filtered empty list
|
|
182
|
+
total_processed += current_page_size
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Update progress for downloading
|
|
186
|
+
progress.update(
|
|
187
|
+
task,
|
|
188
|
+
description=f"Downloading traces... (batch {total_processed // page_size + 1})",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Download each trace with its spans
|
|
192
|
+
for trace in traces:
|
|
193
|
+
try:
|
|
194
|
+
# Get spans for this trace
|
|
195
|
+
spans = client.search_spans(
|
|
196
|
+
project_name=project_name,
|
|
197
|
+
trace_id=trace.id,
|
|
198
|
+
max_results=1000, # Get all spans for the trace
|
|
199
|
+
truncate=False,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Create trace data structure
|
|
203
|
+
trace_data = {
|
|
204
|
+
"trace": trace.model_dump(),
|
|
205
|
+
"spans": [span.model_dump() for span in spans],
|
|
206
|
+
"downloaded_at": datetime.now().isoformat(),
|
|
207
|
+
"project_name": project_name,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Determine file path based on format
|
|
211
|
+
if format.lower() == "csv":
|
|
212
|
+
file_path = project_dir / f"trace_{trace.id}.csv"
|
|
213
|
+
else:
|
|
214
|
+
file_path = project_dir / f"trace_{trace.id}.json"
|
|
215
|
+
|
|
216
|
+
# Check if file already exists and should be skipped
|
|
217
|
+
if should_skip_file(file_path, force):
|
|
218
|
+
if debug:
|
|
219
|
+
debug_print(
|
|
220
|
+
f"Skipping trace {trace.id} (already exists)",
|
|
221
|
+
debug,
|
|
222
|
+
)
|
|
223
|
+
skipped_count += 1
|
|
224
|
+
total_processed += 1
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
# Save to file using the appropriate format
|
|
228
|
+
try:
|
|
229
|
+
if format.lower() == "csv":
|
|
230
|
+
write_csv_data(trace_data, file_path, trace_to_csv_rows)
|
|
231
|
+
if debug:
|
|
232
|
+
debug_print(f"Wrote CSV file: {file_path}", debug)
|
|
233
|
+
else:
|
|
234
|
+
write_json_data(trace_data, file_path)
|
|
235
|
+
if debug:
|
|
236
|
+
debug_print(f"Wrote JSON file: {file_path}", debug)
|
|
237
|
+
|
|
238
|
+
exported_count += 1
|
|
239
|
+
total_processed += 1
|
|
240
|
+
except Exception as write_error:
|
|
241
|
+
console.print(
|
|
242
|
+
f"[red]Error writing trace {trace.id} to file: {write_error}[/red]"
|
|
243
|
+
)
|
|
244
|
+
if debug:
|
|
245
|
+
import traceback
|
|
246
|
+
|
|
247
|
+
debug_print(f"Traceback: {traceback.format_exc()}", debug)
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
except Exception as e:
|
|
251
|
+
console.print(f"[red]Error exporting trace {trace.id}: {e}[/red]")
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Update pagination for next iteration
|
|
255
|
+
if traces:
|
|
256
|
+
current_page += 1
|
|
257
|
+
else:
|
|
258
|
+
# No more traces to process
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
# If we got fewer traces than requested, we've reached the end
|
|
262
|
+
if len(traces) < current_page_size:
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
# Final progress update
|
|
266
|
+
if exported_count == 0:
|
|
267
|
+
console.print("[yellow]No traces found in the project.[/yellow]")
|
|
268
|
+
else:
|
|
269
|
+
progress.update(task, description=f"Exported {exported_count} traces total")
|
|
270
|
+
|
|
271
|
+
return exported_count, skipped_count
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def export_project_by_name(
|
|
275
|
+
name: str,
|
|
276
|
+
workspace: str,
|
|
277
|
+
output_path: str,
|
|
278
|
+
filter_string: Optional[str],
|
|
279
|
+
max_results: Optional[int],
|
|
280
|
+
force: bool,
|
|
281
|
+
debug: bool,
|
|
282
|
+
format: str,
|
|
283
|
+
api_key: Optional[str] = None,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Export a project by exact name."""
|
|
286
|
+
try:
|
|
287
|
+
if debug:
|
|
288
|
+
debug_print(f"Exporting project: {name}", debug)
|
|
289
|
+
|
|
290
|
+
# Validate filter syntax before doing anything else
|
|
291
|
+
if filter_string:
|
|
292
|
+
try:
|
|
293
|
+
from opik.api_objects import opik_query_language
|
|
294
|
+
|
|
295
|
+
# Try to parse the filter to validate syntax
|
|
296
|
+
oql = opik_query_language.OpikQueryLanguage(filter_string)
|
|
297
|
+
if oql.get_filter_expressions() is None and filter_string.strip():
|
|
298
|
+
console.print(
|
|
299
|
+
f"[red]Error: Invalid filter syntax: Filter '{filter_string}' could not be parsed.[/red]"
|
|
300
|
+
)
|
|
301
|
+
console.print("[yellow]OQL filter syntax examples:[/yellow]")
|
|
302
|
+
console.print(' status = "running"')
|
|
303
|
+
console.print(' name contains "test"')
|
|
304
|
+
console.print(' start_time >= "2024-01-01T00:00:00Z"')
|
|
305
|
+
console.print(" usage.total_tokens > 1000")
|
|
306
|
+
console.print(
|
|
307
|
+
"[yellow]Note: String values must be in double quotes, use = not :[/yellow]"
|
|
308
|
+
)
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"Invalid filter syntax: '{filter_string}' could not be parsed"
|
|
311
|
+
)
|
|
312
|
+
except ValueError as e:
|
|
313
|
+
# If it's already our custom error message, re-raise it
|
|
314
|
+
if "Invalid filter syntax" in str(e) and "could not be parsed" in str(
|
|
315
|
+
e
|
|
316
|
+
):
|
|
317
|
+
raise
|
|
318
|
+
# Otherwise, format the error nicely
|
|
319
|
+
error_msg = str(e)
|
|
320
|
+
console.print(f"[red]Error: Invalid filter syntax: {error_msg}[/red]")
|
|
321
|
+
console.print("[yellow]OQL filter syntax examples:[/yellow]")
|
|
322
|
+
console.print(' status = "running"')
|
|
323
|
+
console.print(' name contains "test"')
|
|
324
|
+
console.print(' start_time >= "2024-01-01T00:00:00Z"')
|
|
325
|
+
console.print(" usage.total_tokens > 1000")
|
|
326
|
+
console.print(
|
|
327
|
+
"[yellow]Note: String values must be in double quotes, use = not :[/yellow]"
|
|
328
|
+
)
|
|
329
|
+
raise
|
|
330
|
+
|
|
331
|
+
# Initialize client
|
|
332
|
+
if api_key:
|
|
333
|
+
client = opik.Opik(api_key=api_key, workspace=workspace)
|
|
334
|
+
else:
|
|
335
|
+
client = opik.Opik(workspace=workspace)
|
|
336
|
+
|
|
337
|
+
# Create output directory
|
|
338
|
+
output_dir = Path(output_path) / workspace / "projects"
|
|
339
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
|
|
341
|
+
if debug:
|
|
342
|
+
debug_print(f"Target directory: {output_dir}", debug)
|
|
343
|
+
|
|
344
|
+
# Get projects and find exact match
|
|
345
|
+
# Use name filter to narrow down results (backend does partial match, case-insensitive)
|
|
346
|
+
# Then verify exact match on client side
|
|
347
|
+
projects_response = client.rest_client.projects.find_projects(name=name)
|
|
348
|
+
projects = projects_response.content or []
|
|
349
|
+
matching_project = None
|
|
350
|
+
|
|
351
|
+
for project in projects:
|
|
352
|
+
if project.name == name:
|
|
353
|
+
matching_project = project
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
# If not found with name filter, try paginating through all projects as fallback
|
|
357
|
+
# This handles edge cases where the name filter might not work as expected
|
|
358
|
+
if not matching_project:
|
|
359
|
+
if debug:
|
|
360
|
+
debug_print(
|
|
361
|
+
f"Project '{name}' not found in filtered results, searching all projects...",
|
|
362
|
+
debug,
|
|
363
|
+
)
|
|
364
|
+
# Paginate through all projects
|
|
365
|
+
page = 1
|
|
366
|
+
page_size = 100
|
|
367
|
+
while True:
|
|
368
|
+
projects_response = client.rest_client.projects.find_projects(
|
|
369
|
+
page=page, size=page_size
|
|
370
|
+
)
|
|
371
|
+
projects = projects_response.content or []
|
|
372
|
+
if not projects:
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
for project in projects:
|
|
376
|
+
if project.name == name:
|
|
377
|
+
matching_project = project
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
if matching_project:
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
# Check if there are more pages
|
|
384
|
+
if projects_response.total is not None:
|
|
385
|
+
total_pages = (projects_response.total + page_size - 1) // page_size
|
|
386
|
+
if page >= total_pages:
|
|
387
|
+
break
|
|
388
|
+
elif len(projects) < page_size:
|
|
389
|
+
# No more pages if we got fewer results than page size
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
page += 1
|
|
393
|
+
|
|
394
|
+
if not matching_project:
|
|
395
|
+
console.print(f"[red]Project '{name}' not found[/red]")
|
|
396
|
+
sys.exit(1)
|
|
397
|
+
|
|
398
|
+
if debug:
|
|
399
|
+
debug_print(f"Found project by exact match: {matching_project.name}", debug)
|
|
400
|
+
|
|
401
|
+
# Export the project
|
|
402
|
+
exported_count, traces_exported, traces_skipped = export_single_project(
|
|
403
|
+
client,
|
|
404
|
+
matching_project,
|
|
405
|
+
output_dir,
|
|
406
|
+
filter_string,
|
|
407
|
+
max_results,
|
|
408
|
+
force,
|
|
409
|
+
debug,
|
|
410
|
+
format,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Collect statistics for summary using actual export counts
|
|
414
|
+
stats = {
|
|
415
|
+
"projects": 1 if exported_count > 0 else 0,
|
|
416
|
+
"projects_skipped": 0 if exported_count > 0 else 1,
|
|
417
|
+
"traces": traces_exported,
|
|
418
|
+
"traces_skipped": traces_skipped,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# Show export summary
|
|
422
|
+
print_export_summary(stats, format)
|
|
423
|
+
|
|
424
|
+
if exported_count > 0:
|
|
425
|
+
console.print(
|
|
426
|
+
f"[green]Successfully exported project '{name}' to {output_dir}[/green]"
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
console.print(
|
|
430
|
+
f"[yellow]Project '{name}' already exists (use --force to re-download)[/yellow]"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
except ValueError as e:
|
|
434
|
+
# Filter validation errors are already formatted, just exit
|
|
435
|
+
if "Invalid filter syntax" in str(e):
|
|
436
|
+
sys.exit(1)
|
|
437
|
+
# Other ValueErrors should be shown
|
|
438
|
+
console.print(f"[red]Error exporting project: {e}[/red]")
|
|
439
|
+
sys.exit(1)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
console.print(f"[red]Error exporting project: {e}[/red]")
|
|
442
|
+
sys.exit(1)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def export_single_project(
|
|
446
|
+
client: opik.Opik,
|
|
447
|
+
project: ProjectPublic,
|
|
448
|
+
output_dir: Path,
|
|
449
|
+
filter_string: Optional[str],
|
|
450
|
+
max_results: Optional[int],
|
|
451
|
+
force: bool,
|
|
452
|
+
debug: bool,
|
|
453
|
+
format: str,
|
|
454
|
+
) -> tuple[int, int, int]:
|
|
455
|
+
"""Export a single project."""
|
|
456
|
+
try:
|
|
457
|
+
# Create project-specific directory for traces
|
|
458
|
+
project_traces_dir = output_dir / project.name
|
|
459
|
+
project_traces_dir.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
|
|
461
|
+
# Check if traces directory already has files in the requested format and force is not set
|
|
462
|
+
# Skip early return if a filter is provided, as we need to check if existing traces match the filter
|
|
463
|
+
if not force and not filter_string:
|
|
464
|
+
# Only check for files in the requested format
|
|
465
|
+
if format.lower() == "csv":
|
|
466
|
+
existing_traces = list(project_traces_dir.glob("trace_*.csv"))
|
|
467
|
+
else:
|
|
468
|
+
existing_traces = list(project_traces_dir.glob("trace_*.json"))
|
|
469
|
+
|
|
470
|
+
if existing_traces:
|
|
471
|
+
if debug:
|
|
472
|
+
debug_print(
|
|
473
|
+
f"Skipping {project.name} (already has {len(existing_traces)} trace files in {format} format, use --force to re-download)",
|
|
474
|
+
debug,
|
|
475
|
+
)
|
|
476
|
+
# Return project status, and trace counts (all skipped)
|
|
477
|
+
return (1, 0, len(existing_traces))
|
|
478
|
+
|
|
479
|
+
# Export related traces for this project
|
|
480
|
+
traces_exported, traces_skipped = export_traces(
|
|
481
|
+
client,
|
|
482
|
+
project.name,
|
|
483
|
+
project_traces_dir,
|
|
484
|
+
max_results or 1000, # Use provided max_results or default to 1000
|
|
485
|
+
filter_string,
|
|
486
|
+
None, # project_name_filter
|
|
487
|
+
format,
|
|
488
|
+
debug,
|
|
489
|
+
force,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Project export only exports traces - datasets and prompts must be exported separately
|
|
493
|
+
if traces_exported > 0:
|
|
494
|
+
if debug:
|
|
495
|
+
debug_print(
|
|
496
|
+
f"Exported project: {project.name} with {traces_exported} traces",
|
|
497
|
+
debug,
|
|
498
|
+
)
|
|
499
|
+
return (1, traces_exported, traces_skipped)
|
|
500
|
+
elif traces_skipped > 0:
|
|
501
|
+
# Traces were skipped (already exist)
|
|
502
|
+
if debug:
|
|
503
|
+
debug_print(
|
|
504
|
+
f"Project {project.name} already has {traces_skipped} trace files",
|
|
505
|
+
debug,
|
|
506
|
+
)
|
|
507
|
+
return (1, traces_exported, traces_skipped)
|
|
508
|
+
else:
|
|
509
|
+
# No traces found or exported
|
|
510
|
+
if debug:
|
|
511
|
+
debug_print(
|
|
512
|
+
f"No traces found in project: {project.name}",
|
|
513
|
+
debug,
|
|
514
|
+
)
|
|
515
|
+
# Still return 1 to indicate the project was processed
|
|
516
|
+
# (the empty directory will remain, but that's expected if there are no traces)
|
|
517
|
+
return (1, traces_exported, traces_skipped)
|
|
518
|
+
|
|
519
|
+
except Exception as e:
|
|
520
|
+
console.print(f"[red]Error exporting project {project.name}: {e}[/red]")
|
|
521
|
+
return (0, 0, 0)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def export_project_by_name_or_id(
|
|
525
|
+
name_or_id: str,
|
|
526
|
+
workspace: str,
|
|
527
|
+
output_path: str,
|
|
528
|
+
filter_string: Optional[str],
|
|
529
|
+
max_results: Optional[int],
|
|
530
|
+
force: bool,
|
|
531
|
+
debug: bool,
|
|
532
|
+
format: str,
|
|
533
|
+
api_key: Optional[str] = None,
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Export a project by name or ID.
|
|
536
|
+
|
|
537
|
+
First tries to get the project by ID. If not found, tries by name.
|
|
538
|
+
"""
|
|
539
|
+
try:
|
|
540
|
+
if debug:
|
|
541
|
+
debug_print(f"Attempting to export project: {name_or_id}", debug)
|
|
542
|
+
|
|
543
|
+
# Initialize client
|
|
544
|
+
if api_key:
|
|
545
|
+
client = opik.Opik(api_key=api_key, workspace=workspace)
|
|
546
|
+
else:
|
|
547
|
+
client = opik.Opik(workspace=workspace)
|
|
548
|
+
|
|
549
|
+
# Create output directory
|
|
550
|
+
output_dir = Path(output_path) / workspace / "projects"
|
|
551
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
552
|
+
|
|
553
|
+
if debug:
|
|
554
|
+
debug_print(f"Target directory: {output_dir}", debug)
|
|
555
|
+
|
|
556
|
+
# Try to get project by ID first
|
|
557
|
+
try:
|
|
558
|
+
if debug:
|
|
559
|
+
debug_print(f"Trying to get project by ID: {name_or_id}", debug)
|
|
560
|
+
project = client.get_project(name_or_id)
|
|
561
|
+
|
|
562
|
+
# Successfully found by ID, export it
|
|
563
|
+
if debug:
|
|
564
|
+
debug_print(
|
|
565
|
+
f"Found project by ID: {project.name} (ID: {project.id})", debug
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Export the project
|
|
569
|
+
exported_count, traces_exported, traces_skipped = export_single_project(
|
|
570
|
+
client,
|
|
571
|
+
project,
|
|
572
|
+
output_dir,
|
|
573
|
+
filter_string,
|
|
574
|
+
max_results,
|
|
575
|
+
force,
|
|
576
|
+
debug,
|
|
577
|
+
format,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# Show export summary
|
|
581
|
+
stats = {
|
|
582
|
+
"projects": exported_count,
|
|
583
|
+
"traces": traces_exported,
|
|
584
|
+
"traces_skipped": traces_skipped,
|
|
585
|
+
}
|
|
586
|
+
print_export_summary(stats, format)
|
|
587
|
+
|
|
588
|
+
if exported_count > 0:
|
|
589
|
+
console.print(
|
|
590
|
+
f"[green]Successfully exported project '{project.name}' (ID: {project.id}) to {output_dir}[/green]"
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
console.print(
|
|
594
|
+
f"[yellow]Project '{project.name}' (ID: {project.id}) already exists (use --force to re-download)[/yellow]"
|
|
595
|
+
)
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
except ApiError as e:
|
|
599
|
+
# Check if it's a 404 (not found) error
|
|
600
|
+
if e.status_code == 404:
|
|
601
|
+
# Not found by ID, try by name
|
|
602
|
+
if debug:
|
|
603
|
+
debug_print(
|
|
604
|
+
f"Project not found by ID, trying by name: {name_or_id}", debug
|
|
605
|
+
)
|
|
606
|
+
# Fall through to name-based export
|
|
607
|
+
pass
|
|
608
|
+
else:
|
|
609
|
+
# Some other API error, re-raise it
|
|
610
|
+
raise
|
|
611
|
+
|
|
612
|
+
# Try by name (either because ID lookup failed or we're explicitly trying name)
|
|
613
|
+
export_project_by_name(
|
|
614
|
+
name_or_id,
|
|
615
|
+
workspace,
|
|
616
|
+
output_path,
|
|
617
|
+
filter_string,
|
|
618
|
+
max_results,
|
|
619
|
+
force,
|
|
620
|
+
debug,
|
|
621
|
+
format,
|
|
622
|
+
api_key,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
except Exception as e:
|
|
626
|
+
console.print(f"[red]Error exporting project: {e}[/red]")
|
|
627
|
+
sys.exit(1)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@click.command(name="project")
|
|
631
|
+
@click.argument("name_or_id", type=str)
|
|
632
|
+
@click.option(
|
|
633
|
+
"--filter",
|
|
634
|
+
type=str,
|
|
635
|
+
help="Filter string to narrow down traces using Opik Query Language (OQL).",
|
|
636
|
+
)
|
|
637
|
+
@click.option(
|
|
638
|
+
"--max-results",
|
|
639
|
+
type=int,
|
|
640
|
+
help="Maximum number of traces to export. Limits the total number of traces downloaded.",
|
|
641
|
+
)
|
|
642
|
+
@click.option(
|
|
643
|
+
"--path",
|
|
644
|
+
"-p",
|
|
645
|
+
type=click.Path(file_okay=False, dir_okay=True, writable=True),
|
|
646
|
+
default="opik_exports",
|
|
647
|
+
help="Directory to save exported data. Defaults to opik_exports.",
|
|
648
|
+
)
|
|
649
|
+
@click.option(
|
|
650
|
+
"--force",
|
|
651
|
+
is_flag=True,
|
|
652
|
+
help="Re-download items even if they already exist locally.",
|
|
653
|
+
)
|
|
654
|
+
@click.option(
|
|
655
|
+
"--debug",
|
|
656
|
+
is_flag=True,
|
|
657
|
+
help="Enable debug output to show detailed information about the export process.",
|
|
658
|
+
)
|
|
659
|
+
@click.option(
|
|
660
|
+
"--format",
|
|
661
|
+
type=click.Choice(["json", "csv"], case_sensitive=False),
|
|
662
|
+
default="json",
|
|
663
|
+
help="Format for exporting data. Defaults to json.",
|
|
664
|
+
)
|
|
665
|
+
@click.pass_context
|
|
666
|
+
def export_project_command(
|
|
667
|
+
ctx: click.Context,
|
|
668
|
+
name_or_id: str,
|
|
669
|
+
filter: Optional[str],
|
|
670
|
+
max_results: Optional[int],
|
|
671
|
+
path: str,
|
|
672
|
+
force: bool,
|
|
673
|
+
debug: bool,
|
|
674
|
+
format: str,
|
|
675
|
+
) -> None:
|
|
676
|
+
"""Export a project by name or ID to workspace/projects.
|
|
677
|
+
|
|
678
|
+
The command will first try to find the project by ID. If not found, it will try by name.
|
|
679
|
+
"""
|
|
680
|
+
# Get workspace and API key from context
|
|
681
|
+
workspace = ctx.obj["workspace"]
|
|
682
|
+
api_key = ctx.obj.get("api_key") if ctx.obj else None
|
|
683
|
+
export_project_by_name_or_id(
|
|
684
|
+
name_or_id, workspace, path, filter, max_results, force, debug, format, api_key
|
|
685
|
+
)
|