opik 1.9.5__py3-none-any.whl → 1.9.39__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/__init__.py +10 -3
- opik/anonymizer/__init__.py +5 -0
- opik/anonymizer/anonymizer.py +12 -0
- opik/anonymizer/factory.py +80 -0
- opik/anonymizer/recursive_anonymizer.py +64 -0
- opik/anonymizer/rules.py +56 -0
- opik/anonymizer/rules_anonymizer.py +35 -0
- opik/api_objects/dataset/rest_operations.py +5 -0
- opik/api_objects/experiment/experiment.py +46 -49
- opik/api_objects/experiment/helpers.py +34 -10
- opik/api_objects/local_recording.py +8 -3
- opik/api_objects/opik_client.py +230 -48
- opik/api_objects/opik_query_language.py +9 -0
- opik/api_objects/prompt/__init__.py +11 -3
- opik/api_objects/prompt/base_prompt.py +69 -0
- opik/api_objects/prompt/base_prompt_template.py +29 -0
- opik/api_objects/prompt/chat/__init__.py +1 -0
- opik/api_objects/prompt/chat/chat_prompt.py +193 -0
- opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
- opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +37 -35
- opik/api_objects/prompt/client.py +101 -30
- opik/api_objects/prompt/text/__init__.py +1 -0
- opik/api_objects/prompt/text/prompt.py +174 -0
- opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
- opik/api_objects/prompt/types.py +1 -1
- opik/cli/export.py +6 -2
- opik/cli/usage_report/charts.py +39 -10
- opik/cli/usage_report/cli.py +164 -45
- opik/cli/usage_report/pdf.py +14 -1
- opik/config.py +0 -5
- opik/decorator/base_track_decorator.py +37 -40
- opik/decorator/context_manager/span_context_manager.py +9 -0
- opik/decorator/context_manager/trace_context_manager.py +5 -0
- opik/dict_utils.py +3 -3
- opik/evaluation/__init__.py +13 -2
- opik/evaluation/engine/engine.py +195 -223
- opik/evaluation/engine/helpers.py +8 -7
- opik/evaluation/engine/metrics_evaluator.py +237 -0
- opik/evaluation/evaluation_result.py +35 -1
- opik/evaluation/evaluator.py +318 -30
- opik/evaluation/models/litellm/util.py +78 -6
- opik/evaluation/models/model_capabilities.py +33 -0
- opik/evaluation/report.py +14 -2
- opik/evaluation/rest_operations.py +36 -33
- opik/evaluation/test_case.py +2 -2
- opik/evaluation/types.py +9 -1
- opik/exceptions.py +17 -0
- opik/hooks/__init__.py +17 -1
- opik/hooks/anonymizer_hook.py +36 -0
- opik/id_helpers.py +18 -0
- opik/integrations/adk/helpers.py +16 -7
- opik/integrations/adk/legacy_opik_tracer.py +7 -4
- opik/integrations/adk/opik_tracer.py +3 -1
- opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
- opik/integrations/adk/recursive_callback_injector.py +1 -6
- opik/integrations/dspy/callback.py +1 -4
- opik/integrations/haystack/opik_connector.py +2 -2
- opik/integrations/haystack/opik_tracer.py +2 -4
- opik/integrations/langchain/opik_tracer.py +273 -82
- opik/integrations/llama_index/callback.py +110 -108
- opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
- opik/integrations/openai/opik_tracker.py +1 -1
- opik/message_processing/batching/batchers.py +11 -7
- opik/message_processing/encoder_helpers.py +79 -0
- opik/message_processing/messages.py +25 -1
- opik/message_processing/online_message_processor.py +23 -8
- opik/opik_context.py +7 -7
- opik/rest_api/__init__.py +188 -12
- opik/rest_api/client.py +3 -0
- opik/rest_api/dashboards/__init__.py +4 -0
- opik/rest_api/dashboards/client.py +462 -0
- opik/rest_api/dashboards/raw_client.py +648 -0
- opik/rest_api/datasets/client.py +893 -89
- opik/rest_api/datasets/raw_client.py +1328 -87
- opik/rest_api/experiments/client.py +30 -2
- opik/rest_api/experiments/raw_client.py +26 -0
- opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
- opik/rest_api/optimizations/client.py +302 -0
- opik/rest_api/optimizations/raw_client.py +463 -0
- opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
- opik/rest_api/prompts/__init__.py +2 -2
- opik/rest_api/prompts/client.py +34 -4
- opik/rest_api/prompts/raw_client.py +32 -2
- opik/rest_api/prompts/types/__init__.py +3 -1
- opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
- opik/rest_api/spans/__init__.py +0 -2
- opik/rest_api/spans/client.py +148 -64
- opik/rest_api/spans/raw_client.py +210 -83
- opik/rest_api/spans/types/__init__.py +0 -2
- opik/rest_api/traces/client.py +241 -73
- opik/rest_api/traces/raw_client.py +344 -90
- opik/rest_api/types/__init__.py +200 -15
- opik/rest_api/types/aggregation_data.py +1 -0
- opik/rest_api/types/alert_trigger_config_public_type.py +6 -1
- opik/rest_api/types/alert_trigger_config_type.py +6 -1
- opik/rest_api/types/alert_trigger_config_write_type.py +6 -1
- opik/rest_api/types/automation_rule_evaluator.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
- opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
- opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
- opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
- opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
- opik/rest_api/types/boolean_feedback_definition.py +25 -0
- opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
- opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
- opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
- opik/rest_api/types/boolean_feedback_detail.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
- opik/rest_api/types/dashboard_page_public.py +24 -0
- opik/rest_api/types/dashboard_public.py +30 -0
- opik/rest_api/types/dataset.py +2 -0
- opik/rest_api/types/dataset_item.py +2 -0
- opik/rest_api/types/dataset_item_compare.py +2 -0
- opik/rest_api/types/dataset_item_filter.py +23 -0
- opik/rest_api/types/dataset_item_filter_operator.py +21 -0
- opik/rest_api/types/dataset_item_page_compare.py +1 -0
- opik/rest_api/types/dataset_item_page_public.py +1 -0
- opik/rest_api/types/dataset_item_public.py +2 -0
- opik/rest_api/types/dataset_item_update.py +39 -0
- opik/rest_api/types/dataset_item_write.py +1 -0
- opik/rest_api/types/dataset_public.py +2 -0
- opik/rest_api/types/dataset_public_status.py +5 -0
- opik/rest_api/types/dataset_status.py +5 -0
- opik/rest_api/types/dataset_version_diff.py +22 -0
- opik/rest_api/types/dataset_version_diff_stats.py +24 -0
- opik/rest_api/types/dataset_version_page_public.py +23 -0
- opik/rest_api/types/dataset_version_public.py +49 -0
- opik/rest_api/types/experiment.py +2 -0
- opik/rest_api/types/experiment_public.py +2 -0
- opik/rest_api/types/experiment_score.py +20 -0
- opik/rest_api/types/experiment_score_public.py +20 -0
- opik/rest_api/types/experiment_score_write.py +20 -0
- opik/rest_api/types/feedback.py +20 -1
- opik/rest_api/types/feedback_create.py +16 -1
- opik/rest_api/types/feedback_object_public.py +22 -1
- opik/rest_api/types/feedback_public.py +20 -1
- opik/rest_api/types/feedback_score_public.py +4 -0
- opik/rest_api/types/feedback_update.py +16 -1
- opik/rest_api/types/image_url.py +20 -0
- opik/rest_api/types/image_url_public.py +20 -0
- opik/rest_api/types/image_url_write.py +20 -0
- opik/rest_api/types/llm_as_judge_message.py +5 -1
- opik/rest_api/types/llm_as_judge_message_content.py +24 -0
- opik/rest_api/types/llm_as_judge_message_content_public.py +24 -0
- opik/rest_api/types/llm_as_judge_message_content_write.py +24 -0
- opik/rest_api/types/llm_as_judge_message_public.py +5 -1
- opik/rest_api/types/llm_as_judge_message_write.py +5 -1
- opik/rest_api/types/llm_as_judge_model_parameters.py +2 -0
- opik/rest_api/types/llm_as_judge_model_parameters_public.py +2 -0
- opik/rest_api/types/llm_as_judge_model_parameters_write.py +2 -0
- opik/rest_api/types/optimization.py +2 -0
- opik/rest_api/types/optimization_public.py +2 -0
- opik/rest_api/types/optimization_public_status.py +3 -1
- opik/rest_api/types/optimization_status.py +3 -1
- opik/rest_api/types/optimization_studio_config.py +27 -0
- opik/rest_api/types/optimization_studio_config_public.py +27 -0
- opik/rest_api/types/optimization_studio_config_write.py +27 -0
- opik/rest_api/types/optimization_studio_log.py +22 -0
- opik/rest_api/types/optimization_write.py +2 -0
- opik/rest_api/types/optimization_write_status.py +3 -1
- opik/rest_api/types/prompt.py +6 -0
- opik/rest_api/types/prompt_detail.py +6 -0
- opik/rest_api/types/prompt_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_public.py +6 -0
- opik/rest_api/types/prompt_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_template_structure.py +5 -0
- opik/rest_api/types/prompt_version.py +2 -0
- opik/rest_api/types/prompt_version_detail.py +2 -0
- opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_public.py +2 -0
- opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_template_structure.py +5 -0
- opik/rest_api/types/score_name.py +1 -0
- opik/rest_api/types/service_toggles_config.py +6 -0
- opik/rest_api/types/span_enrichment_options.py +31 -0
- opik/rest_api/types/span_filter.py +23 -0
- opik/rest_api/types/span_filter_operator.py +21 -0
- opik/rest_api/types/span_filter_write.py +23 -0
- opik/rest_api/types/span_filter_write_operator.py +21 -0
- opik/rest_api/types/span_llm_as_judge_code.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
- opik/rest_api/types/span_update.py +46 -0
- opik/rest_api/types/studio_evaluation.py +20 -0
- opik/rest_api/types/studio_evaluation_public.py +20 -0
- opik/rest_api/types/studio_evaluation_write.py +20 -0
- opik/rest_api/types/studio_llm_model.py +21 -0
- opik/rest_api/types/studio_llm_model_public.py +21 -0
- opik/rest_api/types/studio_llm_model_write.py +21 -0
- opik/rest_api/types/studio_message.py +20 -0
- opik/rest_api/types/studio_message_public.py +20 -0
- opik/rest_api/types/studio_message_write.py +20 -0
- opik/rest_api/types/studio_metric.py +21 -0
- opik/rest_api/types/studio_metric_public.py +21 -0
- opik/rest_api/types/studio_metric_write.py +21 -0
- opik/rest_api/types/studio_optimizer.py +21 -0
- opik/rest_api/types/studio_optimizer_public.py +21 -0
- opik/rest_api/types/studio_optimizer_write.py +21 -0
- opik/rest_api/types/studio_prompt.py +20 -0
- opik/rest_api/types/studio_prompt_public.py +20 -0
- opik/rest_api/types/studio_prompt_write.py +20 -0
- opik/rest_api/types/trace.py +6 -0
- opik/rest_api/types/trace_public.py +6 -0
- opik/rest_api/types/trace_thread_filter_write.py +23 -0
- opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
- opik/rest_api/types/trace_thread_update.py +19 -0
- opik/rest_api/types/trace_update.py +39 -0
- opik/rest_api/types/value_entry.py +2 -0
- opik/rest_api/types/value_entry_compare.py +2 -0
- opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
- opik/rest_api/types/value_entry_public.py +2 -0
- opik/rest_api/types/video_url.py +19 -0
- opik/rest_api/types/video_url_public.py +19 -0
- opik/rest_api/types/video_url_write.py +19 -0
- opik/synchronization.py +5 -6
- opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/METADATA +5 -4
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/RECORD +246 -151
- opik/api_objects/prompt/chat_prompt_template.py +0 -164
- opik/api_objects/prompt/prompt.py +0 -131
- /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
- {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional, Tuple
|
|
2
2
|
import json
|
|
3
|
+
import dataclasses
|
|
3
4
|
|
|
5
|
+
import opik.exceptions
|
|
4
6
|
from opik.rest_api import client as rest_client
|
|
5
7
|
from opik.rest_api import core as rest_api_core
|
|
6
|
-
from opik.rest_api.types import prompt_version_detail
|
|
8
|
+
from opik.rest_api.types import prompt_version_detail
|
|
9
|
+
from . import types as prompt_types
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
|
|
12
|
+
@dataclasses.dataclass
|
|
13
|
+
class PromptSearchResult:
|
|
14
|
+
"""Result from searching prompts, containing name, template structure, and latest version details."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
template_structure: str
|
|
18
|
+
prompt_version_detail: prompt_version_detail.PromptVersionDetail
|
|
9
19
|
|
|
10
20
|
|
|
11
21
|
class PromptClient:
|
|
@@ -17,7 +27,8 @@ class PromptClient:
|
|
|
17
27
|
name: str,
|
|
18
28
|
prompt: str,
|
|
19
29
|
metadata: Optional[Dict[str, Any]],
|
|
20
|
-
type: PromptType = PromptType.MUSTACHE,
|
|
30
|
+
type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
|
|
31
|
+
template_structure: str = "text",
|
|
21
32
|
) -> prompt_version_detail.PromptVersionDetail:
|
|
22
33
|
"""
|
|
23
34
|
Creates the prompt detail for the given prompt name and template.
|
|
@@ -25,20 +36,59 @@ class PromptClient:
|
|
|
25
36
|
Parameters:
|
|
26
37
|
- name: The name of the prompt.
|
|
27
38
|
- prompt: The template content for the prompt.
|
|
39
|
+
- metadata: Optional metadata for the prompt.
|
|
40
|
+
- type: The template type (MUSTACHE or JINJA2).
|
|
41
|
+
- template_structure: Either "text" (default) or "chat".
|
|
28
42
|
|
|
29
43
|
Returns:
|
|
30
44
|
- A Prompt object for the provided prompt name and template.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
- PromptTemplateStructureMismatch: If a prompt with the same name already exists but has a different
|
|
48
|
+
template_structure (e.g., trying to create a text prompt when a chat prompt exists, or vice versa).
|
|
49
|
+
Template structure is immutable after prompt creation.
|
|
31
50
|
"""
|
|
32
51
|
prompt_version = self._get_latest_version(name)
|
|
33
52
|
|
|
53
|
+
# For chat prompts, compare parsed JSON to avoid formatting differences
|
|
54
|
+
templates_equal = False
|
|
55
|
+
|
|
56
|
+
if prompt_version is not None:
|
|
57
|
+
if prompt_version.template_structure != template_structure:
|
|
58
|
+
raise opik.exceptions.PromptTemplateStructureMismatch(
|
|
59
|
+
prompt_name=name,
|
|
60
|
+
existing_structure=prompt_version.template_structure,
|
|
61
|
+
attempted_structure=template_structure,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if template_structure == "chat":
|
|
65
|
+
try:
|
|
66
|
+
existing_messages = json.loads(prompt_version.template)
|
|
67
|
+
new_messages = json.loads(prompt)
|
|
68
|
+
templates_equal = existing_messages == new_messages
|
|
69
|
+
except (json.JSONDecodeError, TypeError):
|
|
70
|
+
templates_equal = prompt_version.template == prompt
|
|
71
|
+
else:
|
|
72
|
+
templates_equal = prompt_version.template == prompt
|
|
73
|
+
|
|
74
|
+
# Create a new version if:
|
|
75
|
+
# - No version exists yet (new prompt)
|
|
76
|
+
# - Template content has changed
|
|
77
|
+
# - Metadata has changed
|
|
78
|
+
# - Type has changed
|
|
79
|
+
# Note: template_structure is immutable and used by the backend only if it is the first prompt version.
|
|
34
80
|
if (
|
|
35
81
|
prompt_version is None
|
|
36
|
-
or
|
|
82
|
+
or not templates_equal
|
|
37
83
|
or prompt_version.metadata != metadata
|
|
38
84
|
or prompt_version.type != type.value
|
|
39
85
|
):
|
|
40
86
|
prompt_version = self._create_new_version(
|
|
41
|
-
name=name,
|
|
87
|
+
name=name,
|
|
88
|
+
prompt=prompt,
|
|
89
|
+
type=type,
|
|
90
|
+
metadata=metadata,
|
|
91
|
+
template_structure=template_structure,
|
|
42
92
|
)
|
|
43
93
|
|
|
44
94
|
return prompt_version
|
|
@@ -47,8 +97,9 @@ class PromptClient:
|
|
|
47
97
|
self,
|
|
48
98
|
name: str,
|
|
49
99
|
prompt: str,
|
|
50
|
-
type: PromptVersionDetailType,
|
|
100
|
+
type: prompt_version_detail.PromptVersionDetailType,
|
|
51
101
|
metadata: Optional[Dict[str, Any]],
|
|
102
|
+
template_structure: str = "text",
|
|
52
103
|
) -> prompt_version_detail.PromptVersionDetail:
|
|
53
104
|
new_prompt_version_detail_data = prompt_version_detail.PromptVersionDetail(
|
|
54
105
|
template=prompt,
|
|
@@ -59,6 +110,7 @@ class PromptClient:
|
|
|
59
110
|
self._rest_client.prompts.create_prompt_version(
|
|
60
111
|
name=name,
|
|
61
112
|
version=new_prompt_version_detail_data,
|
|
113
|
+
template_structure=template_structure,
|
|
62
114
|
)
|
|
63
115
|
)
|
|
64
116
|
return new_prompt_version_detail
|
|
@@ -66,20 +118,13 @@ class PromptClient:
|
|
|
66
118
|
def _get_latest_version(
|
|
67
119
|
self, name: str
|
|
68
120
|
) -> Optional[prompt_version_detail.PromptVersionDetail]:
|
|
69
|
-
|
|
70
|
-
prompt_latest_version = self._rest_client.prompts.retrieve_prompt_version(
|
|
71
|
-
name=name
|
|
72
|
-
)
|
|
73
|
-
return prompt_latest_version
|
|
74
|
-
except rest_api_core.ApiError as e:
|
|
75
|
-
if e.status_code != 404:
|
|
76
|
-
raise e
|
|
77
|
-
return None
|
|
121
|
+
return self.get_prompt(name=name, commit=None)
|
|
78
122
|
|
|
79
123
|
def get_prompt(
|
|
80
124
|
self,
|
|
81
125
|
name: str,
|
|
82
126
|
commit: Optional[str] = None,
|
|
127
|
+
raise_if_not_template_structure: Optional[str] = None,
|
|
83
128
|
) -> Optional[prompt_version_detail.PromptVersionDetail]:
|
|
84
129
|
"""
|
|
85
130
|
Retrieve the prompt detail for a given prompt name and commit version.
|
|
@@ -87,6 +132,7 @@ class PromptClient:
|
|
|
87
132
|
Parameters:
|
|
88
133
|
name: The name of the prompt.
|
|
89
134
|
commit: An optional commit version of the prompt. If not provided, the latest version is retrieved.
|
|
135
|
+
raise_if_not_template_structure: Optional template structure validation. If provided and doesn't match, raises PromptTemplateStructureMismatch.
|
|
90
136
|
|
|
91
137
|
Returns:
|
|
92
138
|
Prompt: The details of the specified prompt.
|
|
@@ -96,12 +142,23 @@ class PromptClient:
|
|
|
96
142
|
name=name,
|
|
97
143
|
commit=commit,
|
|
98
144
|
)
|
|
99
|
-
return prompt_version
|
|
100
145
|
|
|
146
|
+
# Client-side validation for template_structure if requested
|
|
147
|
+
if (
|
|
148
|
+
raise_if_not_template_structure is not None
|
|
149
|
+
and prompt_version.template_structure != raise_if_not_template_structure
|
|
150
|
+
):
|
|
151
|
+
raise opik.exceptions.PromptTemplateStructureMismatch(
|
|
152
|
+
prompt_name=name,
|
|
153
|
+
existing_structure=prompt_version.template_structure,
|
|
154
|
+
attempted_structure=raise_if_not_template_structure,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return prompt_version
|
|
101
158
|
except rest_api_core.ApiError as e:
|
|
102
159
|
if e.status_code != 404:
|
|
103
160
|
raise e
|
|
104
|
-
|
|
161
|
+
# 400, 404 - not found
|
|
105
162
|
return None
|
|
106
163
|
|
|
107
164
|
# TODO: Need to add support for prompt name in the BE so we don't
|
|
@@ -184,7 +241,7 @@ class PromptClient:
|
|
|
184
241
|
*,
|
|
185
242
|
name: Optional[str] = None,
|
|
186
243
|
parsed_filters: Optional[List[Dict[str, Any]]] = None,
|
|
187
|
-
) -> List[
|
|
244
|
+
) -> List[PromptSearchResult]:
|
|
188
245
|
"""
|
|
189
246
|
Search prompt containers by optional name substring and filters, then
|
|
190
247
|
return the latest version detail for each matched prompt container.
|
|
@@ -194,17 +251,17 @@ class PromptClient:
|
|
|
194
251
|
parsed_filters: List of parsed filters (OQL) that will be stringified for the backend.
|
|
195
252
|
|
|
196
253
|
Returns:
|
|
197
|
-
List[
|
|
254
|
+
List[PromptSearchResult]: Each result contains name, template_structure, and prompt_version_detail.
|
|
198
255
|
"""
|
|
199
256
|
try:
|
|
200
257
|
filters_str = (
|
|
201
258
|
json.dumps(parsed_filters) if parsed_filters is not None else None
|
|
202
259
|
)
|
|
203
260
|
|
|
204
|
-
# Page through all prompt containers
|
|
261
|
+
# Page through all prompt containers and collect name + template_structure
|
|
205
262
|
page = 1
|
|
206
|
-
size =
|
|
207
|
-
|
|
263
|
+
size = 1000
|
|
264
|
+
prompt_info: List[Tuple[str, str]] = [] # (name, template_structure)
|
|
208
265
|
while True:
|
|
209
266
|
prompts_page = self._rest_client.prompts.get_prompts(
|
|
210
267
|
page=page,
|
|
@@ -215,21 +272,35 @@ class PromptClient:
|
|
|
215
272
|
content = prompts_page.content or []
|
|
216
273
|
if len(content) == 0:
|
|
217
274
|
break
|
|
218
|
-
|
|
275
|
+
prompt_info.extend(
|
|
276
|
+
[(p.name, p.template_structure or "text") for p in content]
|
|
277
|
+
)
|
|
219
278
|
if len(content) < size:
|
|
220
279
|
break
|
|
221
280
|
page += 1
|
|
222
281
|
|
|
223
|
-
if len(
|
|
282
|
+
if len(prompt_info) == 0:
|
|
224
283
|
return []
|
|
225
284
|
|
|
226
285
|
# Retrieve latest version for each container name
|
|
227
|
-
results: List[
|
|
228
|
-
for prompt_name in
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
286
|
+
results: List[PromptSearchResult] = []
|
|
287
|
+
for prompt_name, template_structure in prompt_info:
|
|
288
|
+
try:
|
|
289
|
+
latest_version = self._rest_client.prompts.retrieve_prompt_version(
|
|
290
|
+
name=prompt_name,
|
|
291
|
+
)
|
|
292
|
+
results.append(
|
|
293
|
+
PromptSearchResult(
|
|
294
|
+
name=prompt_name,
|
|
295
|
+
template_structure=template_structure,
|
|
296
|
+
prompt_version_detail=latest_version,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
except rest_api_core.ApiError as e:
|
|
300
|
+
# Skip prompts that can't be retrieved (e.g., deleted between search and retrieval)
|
|
301
|
+
if e.status_code == 404:
|
|
302
|
+
continue
|
|
303
|
+
raise e
|
|
233
304
|
|
|
234
305
|
return results
|
|
235
306
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Empty - all exports handled by parent __init__.py
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional, Union, List
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
from opik.rest_api import types as rest_api_types
|
|
7
|
+
from . import prompt_template
|
|
8
|
+
from .. import types as prompt_types
|
|
9
|
+
from .. import client as prompt_client
|
|
10
|
+
from .. import base_prompt
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Prompt(base_prompt.BasePrompt):
|
|
16
|
+
"""
|
|
17
|
+
Prompt class represents a prompt with a name, prompt text/template and commit hash.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
prompt: str,
|
|
24
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
25
|
+
type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
|
|
26
|
+
validate_placeholders: bool = True,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initializes a new instance of the class with the given parameters.
|
|
30
|
+
Creates a new text prompt using the opik client and sets the initial state of the instance attributes based on the created prompt.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
name: The name for the prompt.
|
|
34
|
+
prompt: The template for the prompt.
|
|
35
|
+
metadata: Optional metadata for the prompt.
|
|
36
|
+
type: The template type (MUSTACHE or JINJA2).
|
|
37
|
+
validate_placeholders: Whether to validate template placeholders.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
PromptTemplateStructureMismatch: If a chat prompt with the same name already exists (template structure is immutable).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
self._template = prompt_template.PromptTemplate(
|
|
44
|
+
template=prompt, type=type, validate_placeholders=validate_placeholders
|
|
45
|
+
)
|
|
46
|
+
self._name = name
|
|
47
|
+
self._metadata = metadata
|
|
48
|
+
self._type = type
|
|
49
|
+
|
|
50
|
+
self._sync_with_backend()
|
|
51
|
+
|
|
52
|
+
def _sync_with_backend(self) -> None:
|
|
53
|
+
from opik.api_objects import opik_client
|
|
54
|
+
|
|
55
|
+
opik_client_ = opik_client.get_client_cached()
|
|
56
|
+
prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)
|
|
57
|
+
prompt_version = prompt_client_.create_prompt(
|
|
58
|
+
name=self._name,
|
|
59
|
+
prompt=self._template.text,
|
|
60
|
+
metadata=self._metadata,
|
|
61
|
+
type=self._type,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self._commit = prompt_version.commit
|
|
65
|
+
self.__internal_api__prompt_id__ = prompt_version.prompt_id
|
|
66
|
+
self.__internal_api__version_id__ = prompt_version.id
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
@override
|
|
70
|
+
def name(self) -> str:
|
|
71
|
+
"""The name of the prompt."""
|
|
72
|
+
return self._name
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def prompt(self) -> str:
|
|
76
|
+
"""The latest template of the prompt."""
|
|
77
|
+
return str(self._template)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
@override
|
|
81
|
+
def commit(self) -> Optional[str]:
|
|
82
|
+
"""The commit hash of the prompt."""
|
|
83
|
+
return self._commit
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@override
|
|
87
|
+
def metadata(self) -> Optional[Dict[str, Any]]:
|
|
88
|
+
"""The metadata dictionary associated with the prompt"""
|
|
89
|
+
return copy.deepcopy(self._metadata)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
@override
|
|
93
|
+
def type(self) -> prompt_types.PromptType:
|
|
94
|
+
"""The prompt type of the prompt."""
|
|
95
|
+
return self._type
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def format(self, **kwargs: Any) -> Union[str, List[Dict[str, Any]]]:
|
|
99
|
+
"""
|
|
100
|
+
Replaces placeholders in the template with provided keyword arguments.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
**kwargs: Arbitrary keyword arguments where the key represents the placeholder
|
|
104
|
+
in the template and the value is the value to replace the placeholder with.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A string with all placeholders replaced by their corresponding values from kwargs.
|
|
108
|
+
"""
|
|
109
|
+
is_playground_chat_prompt = (
|
|
110
|
+
self._metadata is not None
|
|
111
|
+
and self._metadata.get("created_from") == "opik_ui"
|
|
112
|
+
and self._metadata.get("type") == "messages_json"
|
|
113
|
+
)
|
|
114
|
+
formatted_string = self._template.format(**kwargs)
|
|
115
|
+
|
|
116
|
+
if is_playground_chat_prompt:
|
|
117
|
+
try:
|
|
118
|
+
return json.loads(formatted_string)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
LOGGER.error(
|
|
121
|
+
f"Failed to parse JSON string: {formatted_string}. Make sure chat prompt is valid JSON. Returning the raw string."
|
|
122
|
+
)
|
|
123
|
+
return formatted_string
|
|
124
|
+
|
|
125
|
+
return formatted_string
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
def __internal_api__to_info_dict__(self) -> Dict[str, Any]:
|
|
129
|
+
"""
|
|
130
|
+
Convert the prompt to an info dictionary for serialization.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Dictionary containing prompt metadata and version information.
|
|
134
|
+
"""
|
|
135
|
+
info_dict: Dict[str, Any] = {
|
|
136
|
+
"name": self.name,
|
|
137
|
+
"version": {
|
|
138
|
+
"template": self.prompt,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if self.__internal_api__prompt_id__ is not None:
|
|
143
|
+
info_dict["id"] = self.__internal_api__prompt_id__
|
|
144
|
+
|
|
145
|
+
if self.commit is not None:
|
|
146
|
+
info_dict["version"]["commit"] = self.commit
|
|
147
|
+
|
|
148
|
+
if self.__internal_api__version_id__ is not None:
|
|
149
|
+
info_dict["version"]["id"] = self.__internal_api__version_id__
|
|
150
|
+
|
|
151
|
+
return info_dict
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_fern_prompt_version(
|
|
155
|
+
cls,
|
|
156
|
+
name: str,
|
|
157
|
+
prompt_version: rest_api_types.PromptVersionDetail,
|
|
158
|
+
) -> "Prompt":
|
|
159
|
+
# will not call __init__ to avoid API calls, create new instance with __new__
|
|
160
|
+
prompt = cls.__new__(cls)
|
|
161
|
+
|
|
162
|
+
prompt.__internal_api__version_id__ = prompt_version.id
|
|
163
|
+
prompt.__internal_api__prompt_id__ = prompt_version.prompt_id
|
|
164
|
+
|
|
165
|
+
prompt._name = name
|
|
166
|
+
prompt._template = prompt_template.PromptTemplate(
|
|
167
|
+
template=prompt_version.template,
|
|
168
|
+
type=prompt_types.PromptType(prompt_version.type)
|
|
169
|
+
or prompt_types.PromptType.MUSTACHE,
|
|
170
|
+
)
|
|
171
|
+
prompt._commit = prompt_version.commit
|
|
172
|
+
prompt._metadata = prompt_version.metadata
|
|
173
|
+
prompt._type = prompt_version.type
|
|
174
|
+
return prompt
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Any, Set
|
|
3
|
+
from typing_extensions import override
|
|
3
4
|
import jinja2
|
|
4
5
|
|
|
5
6
|
import opik.exceptions as exceptions
|
|
6
|
-
from
|
|
7
|
+
from .. import types as prompt_types
|
|
8
|
+
from .. import base_prompt_template
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
class PromptTemplate:
|
|
11
|
+
class PromptTemplate(base_prompt_template.BasePromptTemplate):
|
|
10
12
|
def __init__(
|
|
11
13
|
self,
|
|
12
14
|
template: str,
|
|
13
15
|
validate_placeholders: bool = True,
|
|
14
|
-
type: PromptType = PromptType.MUSTACHE,
|
|
16
|
+
type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
|
|
15
17
|
) -> None:
|
|
16
18
|
self._template = template
|
|
17
19
|
self._type = type
|
|
@@ -21,8 +23,9 @@ class PromptTemplate:
|
|
|
21
23
|
def text(self) -> str:
|
|
22
24
|
return self._template
|
|
23
25
|
|
|
26
|
+
@override
|
|
24
27
|
def format(self, **kwargs: Any) -> str:
|
|
25
|
-
if self._type == PromptType.MUSTACHE:
|
|
28
|
+
if self._type == prompt_types.PromptType.MUSTACHE:
|
|
26
29
|
template = self._template
|
|
27
30
|
placeholders = _extract_mustache_placeholder_keys(self._template)
|
|
28
31
|
kwargs_keys: Set[str] = set(kwargs.keys())
|
|
@@ -33,9 +36,10 @@ class PromptTemplate:
|
|
|
33
36
|
)
|
|
34
37
|
|
|
35
38
|
for key, value in kwargs.items():
|
|
36
|
-
|
|
39
|
+
replacement = "" if value is None else str(value)
|
|
40
|
+
template = template.replace(f"{{{{{key}}}}}", replacement)
|
|
37
41
|
|
|
38
|
-
elif self._type == PromptType.JINJA2:
|
|
42
|
+
elif self._type == prompt_types.PromptType.JINJA2:
|
|
39
43
|
template = jinja2.Template(self._template).render(**kwargs)
|
|
40
44
|
else:
|
|
41
45
|
template = self._template
|
opik/api_objects/prompt/types.py
CHANGED
|
@@ -14,7 +14,7 @@ class PromptType(str, enum.Enum):
|
|
|
14
14
|
MessageContent = Union[str, List[Dict[str, Any]]]
|
|
15
15
|
ContentPart = Dict[str, Any]
|
|
16
16
|
RendererFn = Callable[[ContentPart, Dict[str, Any], PromptType], Optional[ContentPart]]
|
|
17
|
-
ModalityName = Literal["vision"]
|
|
17
|
+
ModalityName = Literal["vision", "video"]
|
|
18
18
|
SupportedModalities = Mapping[ModalityName, bool]
|
|
19
19
|
ModalitySet = Set[ModalityName]
|
|
20
20
|
|
opik/cli/export.py
CHANGED
|
@@ -457,14 +457,18 @@ def _export_prompts(
|
|
|
457
457
|
prompt_data = {
|
|
458
458
|
"name": prompt.name,
|
|
459
459
|
"current_version": {
|
|
460
|
-
"prompt": prompt.prompt
|
|
460
|
+
"prompt": prompt.prompt
|
|
461
|
+
if isinstance(prompt, opik.Prompt)
|
|
462
|
+
else None, # TODO: add support for chat prompts
|
|
461
463
|
"metadata": prompt.metadata,
|
|
462
464
|
"type": prompt.type if prompt.type else None,
|
|
463
465
|
"commit": prompt.commit,
|
|
464
466
|
},
|
|
465
467
|
"history": [
|
|
466
468
|
{
|
|
467
|
-
"prompt": version.prompt
|
|
469
|
+
"prompt": version.prompt
|
|
470
|
+
if isinstance(version, opik.Prompt)
|
|
471
|
+
else None, # TODO: add support for chat prompts
|
|
468
472
|
"metadata": version.metadata,
|
|
469
473
|
"type": version.type if version.type else None,
|
|
470
474
|
"commit": version.commit,
|
opik/cli/usage_report/charts.py
CHANGED
|
@@ -513,7 +513,7 @@ def create_individual_chart(
|
|
|
513
513
|
else:
|
|
514
514
|
period_labels.append(period)
|
|
515
515
|
|
|
516
|
-
# Create figure
|
|
516
|
+
# Create figure with consistent size for all charts (same as reference implementation)
|
|
517
517
|
fig, ax = plt.subplots(figsize=(14, 8))
|
|
518
518
|
unit_label = unit.capitalize()
|
|
519
519
|
x = range(n_periods)
|
|
@@ -681,19 +681,44 @@ def create_individual_chart(
|
|
|
681
681
|
ax.set_xlabel(unit_label)
|
|
682
682
|
ax.set_xticks(x)
|
|
683
683
|
ax.set_xticklabels(period_labels, rotation=45, ha="right")
|
|
684
|
-
|
|
685
|
-
|
|
684
|
+
# Set x-axis limits to use full width, with small padding on edges
|
|
685
|
+
ax.set_xlim(-0.5, n_periods - 0.5)
|
|
686
|
+
|
|
687
|
+
ax.grid(axis="y", alpha=0.3)
|
|
688
|
+
|
|
689
|
+
# Configure legend for charts that need it - place inside figure bounds
|
|
690
|
+
has_legend = chart_type in ["trace_count", "token_count", "cost", "span_count"]
|
|
691
|
+
if has_legend:
|
|
692
|
+
# Truncate legend labels to maximum length to prevent overly wide legends
|
|
693
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
694
|
+
max_label_length = 40 # Maximum characters per legend label
|
|
695
|
+
truncated_labels = []
|
|
696
|
+
for label in labels:
|
|
697
|
+
if len(label) > max_label_length:
|
|
698
|
+
truncated_labels.append(label[: max_label_length - 3] + "...")
|
|
699
|
+
else:
|
|
700
|
+
truncated_labels.append(label)
|
|
701
|
+
|
|
702
|
+
# Position legend inside the plot area at the bottom, with more space below
|
|
703
|
+
# This allows us to use bbox_inches=None for fixed image sizes
|
|
704
|
+
# Use 3 columns to ensure items wrap into multiple rows
|
|
686
705
|
ax.legend(
|
|
687
|
-
|
|
706
|
+
handles,
|
|
707
|
+
truncated_labels,
|
|
688
708
|
loc="upper center",
|
|
689
|
-
|
|
690
|
-
|
|
709
|
+
bbox_to_anchor=(0.5, -0.35), # Lower in plot area, ~1.5 inches below chart
|
|
710
|
+
ncol=3, # 3 columns ensures wrapping into multiple rows
|
|
711
|
+
fontsize=8,
|
|
691
712
|
framealpha=0.9,
|
|
692
713
|
)
|
|
693
|
-
ax.grid(axis="y", alpha=0.3)
|
|
694
714
|
|
|
695
|
-
#
|
|
696
|
-
|
|
715
|
+
# Explicitly set margins to ensure chart uses full width consistently
|
|
716
|
+
# Left margin (10%) accounts for y-axis labels (including formatted labels like "500.00M" or "$350.00")
|
|
717
|
+
# Right margin (5%) is minimal to maximize chart width
|
|
718
|
+
# Bottom margin (42.5%) accommodates legend positioned below the plot area (outside axes bounds) with ~1 inch of space below
|
|
719
|
+
# Top margin (8%) for title
|
|
720
|
+
# This ensures ALL charts have identical dimensions regardless of y-axis formatter
|
|
721
|
+
fig.subplots_adjust(left=0.10, right=0.95, top=0.92, bottom=0.425)
|
|
697
722
|
|
|
698
723
|
# Save chart to temporary file (use absolute path)
|
|
699
724
|
chart_filename = os.path.join(
|
|
@@ -707,7 +732,11 @@ def create_individual_chart(
|
|
|
707
732
|
os.makedirs(chart_dir, exist_ok=True)
|
|
708
733
|
|
|
709
734
|
try:
|
|
710
|
-
|
|
735
|
+
# Use bbox_inches=None to preserve exact figure size (14x8 inches)
|
|
736
|
+
# Since legend is now inside figure bounds, we can use fixed dimensions
|
|
737
|
+
# This ensures ALL charts have identical dimensions (4200x2400 pixels at 300 DPI)
|
|
738
|
+
# regardless of y-axis label widths or content
|
|
739
|
+
plt.savefig(chart_filename, dpi=300, bbox_inches=None)
|
|
711
740
|
plt.close()
|
|
712
741
|
|
|
713
742
|
# Ensure file is fully written to disk using file system sync operations
|