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,578 @@
|
|
|
1
|
+
"""Prompt export functionality."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
import opik
|
|
13
|
+
from opik.api_objects.prompt import Prompt, ChatPrompt
|
|
14
|
+
from .utils import (
|
|
15
|
+
debug_print,
|
|
16
|
+
prompt_to_csv_rows,
|
|
17
|
+
should_skip_file,
|
|
18
|
+
write_csv_data,
|
|
19
|
+
write_json_data,
|
|
20
|
+
print_export_summary,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_prompt_content(prompt: Any) -> Any:
|
|
27
|
+
"""Extract prompt content based on prompt type.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
prompt: A Prompt or ChatPrompt instance
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
For Prompt: the prompt string
|
|
34
|
+
For ChatPrompt: the template (list of message dicts)
|
|
35
|
+
Otherwise: None
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(prompt, Prompt):
|
|
38
|
+
return prompt.prompt
|
|
39
|
+
elif isinstance(prompt, ChatPrompt):
|
|
40
|
+
return prompt.template
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_template_structure(prompt: Any) -> str:
|
|
45
|
+
"""Get template_structure based on prompt type.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
prompt: A Prompt or ChatPrompt instance
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
"text" for Prompt, "chat" for ChatPrompt, "text" as default
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(prompt, ChatPrompt):
|
|
54
|
+
return "chat"
|
|
55
|
+
elif isinstance(prompt, Prompt):
|
|
56
|
+
return "text"
|
|
57
|
+
return "text"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_prompt_type_string(prompt: Any) -> Optional[str]:
|
|
61
|
+
"""Get prompt type as uppercase string.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
prompt: A Prompt or ChatPrompt instance
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Uppercase type string (e.g., "JINJA2", "MUSTACHE") or None
|
|
68
|
+
"""
|
|
69
|
+
prompt_type = getattr(prompt, "type", None)
|
|
70
|
+
if prompt_type is None:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# If it's an enum, get the value and convert to uppercase
|
|
74
|
+
if hasattr(prompt_type, "value"):
|
|
75
|
+
return prompt_type.value.upper()
|
|
76
|
+
# If it's already a string, convert to uppercase
|
|
77
|
+
if isinstance(prompt_type, str):
|
|
78
|
+
return prompt_type.upper()
|
|
79
|
+
# Otherwise, convert to string and uppercase
|
|
80
|
+
return str(prompt_type).upper()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def export_single_prompt(
|
|
84
|
+
client: opik.Opik,
|
|
85
|
+
prompt: Union[Prompt, ChatPrompt],
|
|
86
|
+
output_dir: Path,
|
|
87
|
+
max_results: Optional[int],
|
|
88
|
+
force: bool,
|
|
89
|
+
debug: bool,
|
|
90
|
+
format: str,
|
|
91
|
+
) -> int:
|
|
92
|
+
"""Export a single prompt."""
|
|
93
|
+
try:
|
|
94
|
+
# Check if already exists and force is not set
|
|
95
|
+
if format.lower() == "csv":
|
|
96
|
+
prompt_file = output_dir / f"prompts_{prompt.name.replace('/', '_')}.csv"
|
|
97
|
+
else:
|
|
98
|
+
prompt_file = output_dir / f"prompt_{prompt.name.replace('/', '_')}.json"
|
|
99
|
+
|
|
100
|
+
if should_skip_file(prompt_file, force):
|
|
101
|
+
if debug:
|
|
102
|
+
debug_print(f"Skipping {prompt.name} (already exists)", debug)
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
# Get prompt history - use appropriate method based on prompt type
|
|
106
|
+
prompt_history: List[Union[Prompt, ChatPrompt]]
|
|
107
|
+
if isinstance(prompt, ChatPrompt):
|
|
108
|
+
prompt_history = list(client.get_chat_prompt_history(prompt.name))
|
|
109
|
+
else:
|
|
110
|
+
prompt_history = list(client.get_prompt_history(prompt.name))
|
|
111
|
+
|
|
112
|
+
# Create prompt data structure
|
|
113
|
+
prompt_data = {
|
|
114
|
+
"name": prompt.name,
|
|
115
|
+
"current_version": {
|
|
116
|
+
"prompt": _get_prompt_content(prompt),
|
|
117
|
+
"metadata": getattr(prompt, "metadata", None),
|
|
118
|
+
"type": _get_prompt_type_string(prompt),
|
|
119
|
+
"commit": getattr(prompt, "commit", None),
|
|
120
|
+
"template_structure": _get_template_structure(prompt),
|
|
121
|
+
},
|
|
122
|
+
"history": [
|
|
123
|
+
{
|
|
124
|
+
"prompt": _get_prompt_content(version),
|
|
125
|
+
"metadata": getattr(version, "metadata", None),
|
|
126
|
+
"type": _get_prompt_type_string(version),
|
|
127
|
+
"commit": getattr(version, "commit", None),
|
|
128
|
+
"template_structure": _get_template_structure(version),
|
|
129
|
+
}
|
|
130
|
+
for version in prompt_history
|
|
131
|
+
],
|
|
132
|
+
"downloaded_at": datetime.now().isoformat(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Save to file using the appropriate format
|
|
136
|
+
if format.lower() == "csv":
|
|
137
|
+
write_csv_data(prompt_data, prompt_file, prompt_to_csv_rows)
|
|
138
|
+
else:
|
|
139
|
+
write_json_data(prompt_data, prompt_file)
|
|
140
|
+
|
|
141
|
+
if debug:
|
|
142
|
+
debug_print(f"Exported prompt: {prompt.name}", debug)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
console.print(f"[red]Error exporting prompt {prompt.name}: {e}[/red]")
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def export_prompt_by_name(
|
|
151
|
+
name: str,
|
|
152
|
+
workspace: str,
|
|
153
|
+
output_path: str,
|
|
154
|
+
max_results: Optional[int],
|
|
155
|
+
force: bool,
|
|
156
|
+
debug: bool,
|
|
157
|
+
format: str,
|
|
158
|
+
api_key: Optional[str] = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Export a prompt by exact name."""
|
|
161
|
+
try:
|
|
162
|
+
if debug:
|
|
163
|
+
debug_print(f"Exporting prompt: {name}", debug)
|
|
164
|
+
|
|
165
|
+
# Initialize client
|
|
166
|
+
if api_key:
|
|
167
|
+
client = opik.Opik(api_key=api_key, workspace=workspace)
|
|
168
|
+
else:
|
|
169
|
+
client = opik.Opik(workspace=workspace)
|
|
170
|
+
|
|
171
|
+
# Create output directory
|
|
172
|
+
output_dir = Path(output_path) / workspace / "prompts"
|
|
173
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
|
|
175
|
+
if debug:
|
|
176
|
+
debug_print(f"Target directory: {output_dir}", debug)
|
|
177
|
+
|
|
178
|
+
# Try to get prompt by exact name
|
|
179
|
+
# Try ChatPrompt first, then regular Prompt
|
|
180
|
+
prompt: Optional[Union[Prompt, ChatPrompt]] = None
|
|
181
|
+
try:
|
|
182
|
+
prompt = client.get_chat_prompt(name)
|
|
183
|
+
if debug and prompt:
|
|
184
|
+
debug_print(f"Found ChatPrompt by direct lookup: {prompt.name}", debug)
|
|
185
|
+
except Exception:
|
|
186
|
+
# Not a ChatPrompt, try regular Prompt
|
|
187
|
+
try:
|
|
188
|
+
prompt = client.get_prompt(name)
|
|
189
|
+
if not prompt:
|
|
190
|
+
console.print(f"[red]Prompt '{name}' not found[/red]")
|
|
191
|
+
return
|
|
192
|
+
if debug:
|
|
193
|
+
debug_print(f"Found Prompt by direct lookup: {prompt.name}", debug)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
console.print(f"[red]Prompt '{name}' not found: {e}[/red]")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
if prompt is None:
|
|
199
|
+
console.print(f"[red]Prompt '{name}' not found[/red]")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
# Export the prompt
|
|
203
|
+
exported_count = export_single_prompt(
|
|
204
|
+
client, prompt, output_dir, max_results, force, debug, format
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Collect statistics for summary
|
|
208
|
+
stats = {
|
|
209
|
+
"prompts": 1 if exported_count > 0 else 0,
|
|
210
|
+
"prompts_skipped": 0 if exported_count > 0 else 1,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Show export summary
|
|
214
|
+
print_export_summary(stats, format)
|
|
215
|
+
|
|
216
|
+
if exported_count > 0:
|
|
217
|
+
console.print(
|
|
218
|
+
f"[green]Successfully exported prompt '{name}' to {output_dir}[/green]"
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
console.print(
|
|
222
|
+
f"[yellow]Prompt '{name}' already exists (use --force to re-download)[/yellow]"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
console.print(f"[red]Error exporting prompt: {e}[/red]")
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def export_prompts_by_ids(
|
|
231
|
+
client: opik.Opik,
|
|
232
|
+
prompt_ids: set[str],
|
|
233
|
+
prompts_dir: Path,
|
|
234
|
+
format: str,
|
|
235
|
+
debug: bool,
|
|
236
|
+
force: bool,
|
|
237
|
+
) -> tuple[int, int]:
|
|
238
|
+
"""Export prompts by their IDs.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
client: Opik client instance
|
|
242
|
+
prompt_ids: Set of prompt IDs to export
|
|
243
|
+
prompts_dir: Directory to save prompts
|
|
244
|
+
format: Export format ('json' or 'csv')
|
|
245
|
+
debug: Enable debug output
|
|
246
|
+
force: Re-download prompts even if they already exist locally
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (exported_count, skipped_count)
|
|
250
|
+
"""
|
|
251
|
+
exported_count = 0
|
|
252
|
+
skipped_count = 0
|
|
253
|
+
|
|
254
|
+
for prompt_id in prompt_ids:
|
|
255
|
+
try:
|
|
256
|
+
# Get the prompt - try ChatPrompt first, then regular Prompt
|
|
257
|
+
prompt: Optional[Union[Prompt, ChatPrompt]] = None
|
|
258
|
+
try:
|
|
259
|
+
prompt = client.get_chat_prompt(prompt_id)
|
|
260
|
+
except Exception:
|
|
261
|
+
# Not a ChatPrompt, try regular Prompt
|
|
262
|
+
prompt = client.get_prompt(prompt_id)
|
|
263
|
+
|
|
264
|
+
if not prompt:
|
|
265
|
+
if debug:
|
|
266
|
+
console.print(
|
|
267
|
+
f"[yellow]Warning: Prompt {prompt_id} not found[/yellow]"
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
# Determine file path
|
|
272
|
+
if format.lower() == "csv":
|
|
273
|
+
prompt_file = (
|
|
274
|
+
prompts_dir
|
|
275
|
+
/ f"prompts_{prompt.name or getattr(prompt, 'id', 'unknown')}.csv"
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
prompt_file = (
|
|
279
|
+
prompts_dir
|
|
280
|
+
/ f"prompt_{prompt.name or getattr(prompt, 'id', 'unknown')}.json"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Check if file already exists and should be skipped
|
|
284
|
+
if should_skip_file(prompt_file, force):
|
|
285
|
+
if debug:
|
|
286
|
+
debug_print(
|
|
287
|
+
f"Skipping prompt {prompt.name or prompt_id} (already exists)",
|
|
288
|
+
debug,
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
console.print(
|
|
292
|
+
f"[yellow]Skipping prompt: {prompt.name or prompt_id} (already exists)[/yellow]"
|
|
293
|
+
)
|
|
294
|
+
skipped_count += 1
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
# Get prompt history - use appropriate method based on prompt type
|
|
298
|
+
prompt_history: List[Union[Prompt, ChatPrompt]]
|
|
299
|
+
if isinstance(prompt, ChatPrompt):
|
|
300
|
+
prompt_history = list(client.get_chat_prompt_history(prompt.name))
|
|
301
|
+
else:
|
|
302
|
+
prompt_history = list(client.get_prompt_history(prompt_id))
|
|
303
|
+
|
|
304
|
+
# Create prompt data structure
|
|
305
|
+
prompt_data = {
|
|
306
|
+
"prompt": {
|
|
307
|
+
"id": getattr(prompt, "id", None),
|
|
308
|
+
"name": prompt.name,
|
|
309
|
+
"description": getattr(prompt, "description", None),
|
|
310
|
+
"created_at": (
|
|
311
|
+
created_at.isoformat()
|
|
312
|
+
if (created_at := getattr(prompt, "created_at", None))
|
|
313
|
+
else None
|
|
314
|
+
),
|
|
315
|
+
"last_updated_at": (
|
|
316
|
+
last_updated_at.isoformat()
|
|
317
|
+
if (last_updated_at := getattr(prompt, "last_updated_at", None))
|
|
318
|
+
else None
|
|
319
|
+
),
|
|
320
|
+
},
|
|
321
|
+
"current_version": {
|
|
322
|
+
"prompt": _get_prompt_content(prompt),
|
|
323
|
+
"metadata": getattr(prompt, "metadata", None),
|
|
324
|
+
"type": _get_prompt_type_string(prompt),
|
|
325
|
+
"commit": getattr(prompt, "commit", None),
|
|
326
|
+
"template_structure": _get_template_structure(prompt),
|
|
327
|
+
},
|
|
328
|
+
"history": [
|
|
329
|
+
{
|
|
330
|
+
"prompt": _get_prompt_content(version),
|
|
331
|
+
"metadata": getattr(version, "metadata", None),
|
|
332
|
+
"type": _get_prompt_type_string(version),
|
|
333
|
+
"commit": getattr(version, "commit", None),
|
|
334
|
+
"template_structure": _get_template_structure(version),
|
|
335
|
+
}
|
|
336
|
+
for version in prompt_history
|
|
337
|
+
],
|
|
338
|
+
"downloaded_at": datetime.now().isoformat(),
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Save prompt data using the appropriate format
|
|
342
|
+
if format.lower() == "csv":
|
|
343
|
+
write_csv_data(prompt_data, prompt_file, prompt_to_csv_rows)
|
|
344
|
+
else:
|
|
345
|
+
write_json_data(prompt_data, prompt_file)
|
|
346
|
+
|
|
347
|
+
console.print(f"[green]Exported prompt: {prompt.name or prompt_id}[/green]")
|
|
348
|
+
exported_count += 1
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
if debug:
|
|
352
|
+
console.print(
|
|
353
|
+
f"[yellow]Warning: Could not export prompt {prompt_id}: {e}[/yellow]"
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
console.print(f"[red]Error exporting prompt {prompt_id}: {e}[/red]")
|
|
357
|
+
continue
|
|
358
|
+
|
|
359
|
+
return exported_count, skipped_count
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def export_related_prompts_by_name(
|
|
363
|
+
client: opik.Opik,
|
|
364
|
+
experiment: Any,
|
|
365
|
+
output_dir: Path,
|
|
366
|
+
force: bool,
|
|
367
|
+
debug: bool,
|
|
368
|
+
format: str = "json",
|
|
369
|
+
) -> int:
|
|
370
|
+
"""Export prompts explicitly related to the experiment from experiment metadata."""
|
|
371
|
+
try:
|
|
372
|
+
prompts_dir = output_dir.parent / "prompts"
|
|
373
|
+
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
|
|
375
|
+
# Get experiment data to access metadata
|
|
376
|
+
experiment_data = experiment.get_experiment_data()
|
|
377
|
+
if not experiment_data:
|
|
378
|
+
debug_print("Could not get experiment data", debug)
|
|
379
|
+
return 0
|
|
380
|
+
|
|
381
|
+
# Extract prompt names from experiment metadata
|
|
382
|
+
prompt_names = []
|
|
383
|
+
metadata = experiment_data.metadata
|
|
384
|
+
|
|
385
|
+
if metadata:
|
|
386
|
+
# Metadata can be a dict, list, or string (JsonListStringPublic)
|
|
387
|
+
# Parse if it's a string, otherwise use directly
|
|
388
|
+
if isinstance(metadata, str):
|
|
389
|
+
try:
|
|
390
|
+
metadata = json.loads(metadata)
|
|
391
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
392
|
+
if debug:
|
|
393
|
+
debug_print(f"Could not parse metadata as JSON: {e}", debug)
|
|
394
|
+
metadata = None
|
|
395
|
+
|
|
396
|
+
# Check if metadata is a dict and has "prompts" key
|
|
397
|
+
if isinstance(metadata, dict) and "prompts" in metadata:
|
|
398
|
+
prompts_dict = metadata["prompts"]
|
|
399
|
+
if isinstance(prompts_dict, dict):
|
|
400
|
+
# Prompts are stored as a dict with prompt names as keys
|
|
401
|
+
prompt_names = list(prompts_dict.keys())
|
|
402
|
+
if debug:
|
|
403
|
+
debug_print(
|
|
404
|
+
f"Found {len(prompt_names)} prompt(s) in experiment metadata: {prompt_names}",
|
|
405
|
+
debug,
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
if debug:
|
|
409
|
+
debug_print(
|
|
410
|
+
f"Metadata 'prompts' is not a dict, got: {type(prompts_dict)}",
|
|
411
|
+
debug,
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
if debug:
|
|
415
|
+
debug_print("No 'prompts' key found in experiment metadata", debug)
|
|
416
|
+
|
|
417
|
+
if not prompt_names:
|
|
418
|
+
debug_print("No prompts found in experiment metadata", debug)
|
|
419
|
+
return 0
|
|
420
|
+
|
|
421
|
+
console.print(
|
|
422
|
+
f"[blue]Exporting {len(prompt_names)} prompt(s) from experiment metadata...[/blue]"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
exported_count = 0
|
|
426
|
+
# Export each prompt by name from metadata
|
|
427
|
+
for prompt_name in prompt_names:
|
|
428
|
+
try:
|
|
429
|
+
debug_print(f"Exporting prompt: {prompt_name}", debug)
|
|
430
|
+
|
|
431
|
+
# Try to get the prompt - try ChatPrompt first, then regular Prompt
|
|
432
|
+
prompt: Optional[Union[Prompt, ChatPrompt]] = None
|
|
433
|
+
try:
|
|
434
|
+
prompt = client.get_chat_prompt(prompt_name)
|
|
435
|
+
except Exception:
|
|
436
|
+
# Not a ChatPrompt, try regular Prompt
|
|
437
|
+
try:
|
|
438
|
+
prompt = client.get_prompt(prompt_name)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
if debug:
|
|
441
|
+
console.print(
|
|
442
|
+
f"[yellow]Warning: Could not get prompt '{prompt_name}': {e}[/yellow]"
|
|
443
|
+
)
|
|
444
|
+
continue
|
|
445
|
+
|
|
446
|
+
if not prompt:
|
|
447
|
+
if debug:
|
|
448
|
+
console.print(
|
|
449
|
+
f"[yellow]Warning: Prompt '{prompt_name}' not found[/yellow]"
|
|
450
|
+
)
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
# Get prompt history - use appropriate method based on prompt type
|
|
454
|
+
prompt_history: List[Union[Prompt, ChatPrompt]]
|
|
455
|
+
if isinstance(prompt, ChatPrompt):
|
|
456
|
+
prompt_history = list(client.get_chat_prompt_history(prompt.name))
|
|
457
|
+
else:
|
|
458
|
+
prompt_history = list(client.get_prompt_history(prompt.name))
|
|
459
|
+
|
|
460
|
+
# Create prompt data structure
|
|
461
|
+
prompt_data = {
|
|
462
|
+
"prompt": {
|
|
463
|
+
"id": getattr(prompt, "__internal_api__prompt_id__", None),
|
|
464
|
+
"name": prompt.name,
|
|
465
|
+
"description": getattr(prompt, "description", None),
|
|
466
|
+
"created_at": getattr(prompt, "created_at", None),
|
|
467
|
+
"last_updated_at": getattr(prompt, "last_updated_at", None),
|
|
468
|
+
},
|
|
469
|
+
"current_version": {
|
|
470
|
+
"prompt": _get_prompt_content(prompt),
|
|
471
|
+
"metadata": getattr(prompt, "metadata", None),
|
|
472
|
+
"type": _get_prompt_type_string(prompt),
|
|
473
|
+
"commit": getattr(prompt, "commit", None),
|
|
474
|
+
"template_structure": _get_template_structure(prompt),
|
|
475
|
+
},
|
|
476
|
+
"history": [
|
|
477
|
+
{
|
|
478
|
+
"prompt": _get_prompt_content(version),
|
|
479
|
+
"metadata": getattr(version, "metadata", None),
|
|
480
|
+
"type": _get_prompt_type_string(version),
|
|
481
|
+
"commit": getattr(version, "commit", None),
|
|
482
|
+
"template_structure": _get_template_structure(version),
|
|
483
|
+
}
|
|
484
|
+
for version in prompt_history
|
|
485
|
+
],
|
|
486
|
+
"downloaded_at": datetime.now().isoformat(),
|
|
487
|
+
"related_to_experiment": experiment.name or experiment.id,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
# Save prompt data using the appropriate format
|
|
491
|
+
# Sanitize prompt name for filename (replace / with _)
|
|
492
|
+
sanitized_name = prompt.name.replace("/", "_")
|
|
493
|
+
if format.lower() == "csv":
|
|
494
|
+
prompt_file = prompts_dir / f"prompts_{sanitized_name}.csv"
|
|
495
|
+
else:
|
|
496
|
+
prompt_file = prompts_dir / f"prompt_{sanitized_name}.json"
|
|
497
|
+
|
|
498
|
+
# Check if file should be skipped using the standard utility
|
|
499
|
+
if should_skip_file(prompt_file, force):
|
|
500
|
+
debug_print(
|
|
501
|
+
f"Skipping prompt {prompt.name} (already exists)", debug
|
|
502
|
+
)
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
# File doesn't exist or force is set, so export it
|
|
506
|
+
if format.lower() == "csv":
|
|
507
|
+
write_csv_data(prompt_data, prompt_file, prompt_to_csv_rows)
|
|
508
|
+
else:
|
|
509
|
+
write_json_data(prompt_data, prompt_file)
|
|
510
|
+
|
|
511
|
+
console.print(f"[green]Exported related prompt: {prompt.name}[/green]")
|
|
512
|
+
exported_count += 1
|
|
513
|
+
|
|
514
|
+
except Exception as e:
|
|
515
|
+
if debug:
|
|
516
|
+
prompt_display_name = prompt.name if prompt else prompt_name
|
|
517
|
+
console.print(
|
|
518
|
+
f"[yellow]Warning: Could not export related prompt {prompt_display_name}: {e}[/yellow]"
|
|
519
|
+
)
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
return exported_count
|
|
523
|
+
|
|
524
|
+
except Exception as e:
|
|
525
|
+
if debug:
|
|
526
|
+
console.print(
|
|
527
|
+
f"[yellow]Warning: Could not export related prompts: {e}[/yellow]"
|
|
528
|
+
)
|
|
529
|
+
return 0
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@click.command(name="prompt")
|
|
533
|
+
@click.argument("name", type=str)
|
|
534
|
+
@click.option(
|
|
535
|
+
"--max-results",
|
|
536
|
+
type=int,
|
|
537
|
+
help="Maximum number of prompts to export. Limits the total number of prompts downloaded.",
|
|
538
|
+
)
|
|
539
|
+
@click.option(
|
|
540
|
+
"--path",
|
|
541
|
+
"-p",
|
|
542
|
+
type=click.Path(file_okay=False, dir_okay=True, writable=True),
|
|
543
|
+
default="opik_exports",
|
|
544
|
+
help="Directory to save exported data. Defaults to opik_exports.",
|
|
545
|
+
)
|
|
546
|
+
@click.option(
|
|
547
|
+
"--force",
|
|
548
|
+
is_flag=True,
|
|
549
|
+
help="Re-download items even if they already exist locally.",
|
|
550
|
+
)
|
|
551
|
+
@click.option(
|
|
552
|
+
"--debug",
|
|
553
|
+
is_flag=True,
|
|
554
|
+
help="Enable debug output to show detailed information about the export process.",
|
|
555
|
+
)
|
|
556
|
+
@click.option(
|
|
557
|
+
"--format",
|
|
558
|
+
type=click.Choice(["json", "csv"], case_sensitive=False),
|
|
559
|
+
default="json",
|
|
560
|
+
help="Format for exporting data. Defaults to json.",
|
|
561
|
+
)
|
|
562
|
+
@click.pass_context
|
|
563
|
+
def export_prompt_command(
|
|
564
|
+
ctx: click.Context,
|
|
565
|
+
name: str,
|
|
566
|
+
max_results: Optional[int],
|
|
567
|
+
path: str,
|
|
568
|
+
force: bool,
|
|
569
|
+
debug: bool,
|
|
570
|
+
format: str,
|
|
571
|
+
) -> None:
|
|
572
|
+
"""Export a prompt by exact name to workspace/prompts."""
|
|
573
|
+
# Get workspace and API key from context
|
|
574
|
+
workspace = ctx.obj["workspace"]
|
|
575
|
+
api_key = ctx.obj.get("api_key") if ctx.obj else None
|
|
576
|
+
export_prompt_by_name(
|
|
577
|
+
name, workspace, path, max_results, force, debug, format, api_key
|
|
578
|
+
)
|