opik 1.9.26__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/api_objects/dataset/rest_operations.py +2 -0
- opik/api_objects/experiment/experiment.py +31 -5
- opik/api_objects/experiment/helpers.py +34 -10
- opik/api_objects/local_recording.py +8 -3
- opik/api_objects/opik_client.py +218 -46
- 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} +31 -34
- opik/api_objects/prompt/client.py +101 -30
- opik/api_objects/prompt/text/__init__.py +1 -0
- opik/api_objects/prompt/{prompt.py → text/prompt.py} +55 -32
- opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +8 -5
- opik/cli/export.py +6 -2
- opik/config.py +0 -5
- opik/decorator/base_track_decorator.py +37 -40
- 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 +309 -23
- opik/evaluation/models/litellm/util.py +78 -6
- opik/evaluation/report.py +14 -2
- opik/evaluation/rest_operations.py +6 -9
- opik/evaluation/test_case.py +2 -2
- opik/evaluation/types.py +9 -1
- opik/exceptions.py +17 -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/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 +1 -4
- opik/integrations/llama_index/callback.py +2 -4
- opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
- opik/integrations/openai/opik_tracker.py +1 -1
- opik/opik_context.py +7 -7
- opik/rest_api/__init__.py +123 -11
- opik/rest_api/dashboards/client.py +65 -2
- opik/rest_api/dashboards/raw_client.py +82 -0
- opik/rest_api/datasets/client.py +441 -2
- opik/rest_api/datasets/raw_client.py +1225 -505
- opik/rest_api/experiments/client.py +30 -2
- opik/rest_api/experiments/raw_client.py +26 -0
- 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/traces/client.py +6 -6
- opik/rest_api/traces/raw_client.py +4 -4
- opik/rest_api/types/__init__.py +121 -11
- opik/rest_api/types/aggregation_data.py +1 -0
- 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/dashboard_page_public.py +1 -0
- opik/rest_api/types/dataset.py +2 -0
- opik/rest_api/types/dataset_item.py +1 -0
- opik/rest_api/types/dataset_item_compare.py +1 -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 +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_score_public.py +4 -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 +5 -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/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/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/synchronization.py +5 -6
- opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/METADATA +2 -1
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/RECORD +177 -119
- opik/api_objects/prompt/chat_prompt_template.py +0 -200
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
- {opik-1.9.26.dist-info → opik-1.9.39.dist-info}/top_level.txt +0 -0
opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py}
RENAMED
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Tuple, Union
|
|
4
2
|
|
|
5
|
-
from
|
|
6
|
-
ContentPart,
|
|
7
|
-
MessageContent,
|
|
8
|
-
ModalityName,
|
|
9
|
-
RendererFn,
|
|
10
|
-
SupportedModalities,
|
|
11
|
-
PromptType,
|
|
12
|
-
)
|
|
3
|
+
from .. import types as prompt_types
|
|
13
4
|
|
|
14
5
|
|
|
15
6
|
class ChatContentRendererRegistry:
|
|
@@ -18,9 +9,13 @@ class ChatContentRendererRegistry:
|
|
|
18
9
|
"""
|
|
19
10
|
|
|
20
11
|
def __init__(self) -> None:
|
|
21
|
-
self._part_renderers: MutableMapping[str, RendererFn] = {}
|
|
22
|
-
self._part_modalities: MutableMapping[
|
|
23
|
-
|
|
12
|
+
self._part_renderers: MutableMapping[str, prompt_types.RendererFn] = {}
|
|
13
|
+
self._part_modalities: MutableMapping[
|
|
14
|
+
str, Optional[prompt_types.ModalityName]
|
|
15
|
+
] = {}
|
|
16
|
+
self._modality_placeholders: MutableMapping[
|
|
17
|
+
prompt_types.ModalityName, Tuple[str, str]
|
|
18
|
+
] = {
|
|
24
19
|
"vision": ("<<<image>>>", "<<</image>>>"),
|
|
25
20
|
"video": ("<<<video>>>", "<<</video>>>"),
|
|
26
21
|
}
|
|
@@ -30,9 +25,9 @@ class ChatContentRendererRegistry:
|
|
|
30
25
|
def register_part_renderer(
|
|
31
26
|
self,
|
|
32
27
|
part_type: str,
|
|
33
|
-
renderer: RendererFn,
|
|
28
|
+
renderer: prompt_types.RendererFn,
|
|
34
29
|
*,
|
|
35
|
-
modality: Optional[ModalityName] = None,
|
|
30
|
+
modality: Optional[prompt_types.ModalityName] = None,
|
|
36
31
|
placeholder: Optional[Tuple[str, str]] = None,
|
|
37
32
|
) -> None:
|
|
38
33
|
"""
|
|
@@ -47,12 +42,12 @@ class ChatContentRendererRegistry:
|
|
|
47
42
|
|
|
48
43
|
def render_content(
|
|
49
44
|
self,
|
|
50
|
-
content: MessageContent,
|
|
45
|
+
content: prompt_types.MessageContent,
|
|
51
46
|
variables: Dict[str, Any],
|
|
52
|
-
template_type: PromptType,
|
|
47
|
+
template_type: prompt_types.PromptType,
|
|
53
48
|
*,
|
|
54
|
-
supported_modalities: Optional[SupportedModalities] = None,
|
|
55
|
-
) -> MessageContent:
|
|
49
|
+
supported_modalities: Optional[prompt_types.SupportedModalities] = None,
|
|
50
|
+
) -> prompt_types.MessageContent:
|
|
56
51
|
if supported_modalities is None:
|
|
57
52
|
modality_flags: Dict[str, bool] = {}
|
|
58
53
|
else:
|
|
@@ -84,19 +79,21 @@ class ChatContentRendererRegistry:
|
|
|
84
79
|
return rendered_parts
|
|
85
80
|
|
|
86
81
|
def normalize_template_type(
|
|
87
|
-
self, template_type: Union[str, PromptType]
|
|
88
|
-
) -> PromptType:
|
|
89
|
-
if isinstance(template_type, PromptType):
|
|
82
|
+
self, template_type: Union[str, prompt_types.PromptType]
|
|
83
|
+
) -> prompt_types.PromptType:
|
|
84
|
+
if isinstance(template_type, prompt_types.PromptType):
|
|
90
85
|
return template_type
|
|
91
86
|
try:
|
|
92
|
-
return PromptType(template_type)
|
|
87
|
+
return prompt_types.PromptType(template_type)
|
|
93
88
|
except ValueError:
|
|
94
|
-
return PromptType.MUSTACHE
|
|
89
|
+
return prompt_types.PromptType.MUSTACHE
|
|
95
90
|
|
|
96
|
-
def infer_modalities(
|
|
91
|
+
def infer_modalities(
|
|
92
|
+
self, content: prompt_types.MessageContent
|
|
93
|
+
) -> set[prompt_types.ModalityName]:
|
|
97
94
|
if not isinstance(content, list):
|
|
98
95
|
return set()
|
|
99
|
-
modalities: set[ModalityName] = set()
|
|
96
|
+
modalities: set[prompt_types.ModalityName] = set()
|
|
100
97
|
for part in content:
|
|
101
98
|
if not isinstance(part, dict):
|
|
102
99
|
continue
|
|
@@ -110,9 +107,9 @@ class ChatContentRendererRegistry:
|
|
|
110
107
|
self,
|
|
111
108
|
content: List[Any],
|
|
112
109
|
variables: Dict[str, Any],
|
|
113
|
-
template_type: PromptType,
|
|
114
|
-
) -> List[ContentPart]:
|
|
115
|
-
rendered_parts: List[ContentPart] = []
|
|
110
|
+
template_type: prompt_types.PromptType,
|
|
111
|
+
) -> List[prompt_types.ContentPart]:
|
|
112
|
+
rendered_parts: List[prompt_types.ContentPart] = []
|
|
116
113
|
for part in content:
|
|
117
114
|
if not isinstance(part, dict):
|
|
118
115
|
continue
|
|
@@ -127,7 +124,7 @@ class ChatContentRendererRegistry:
|
|
|
127
124
|
return rendered_parts
|
|
128
125
|
|
|
129
126
|
def _should_flatten(
|
|
130
|
-
self, parts: List[ContentPart], modality_flags: Mapping[str, bool]
|
|
127
|
+
self, parts: List[prompt_types.ContentPart], modality_flags: Mapping[str, bool]
|
|
131
128
|
) -> bool:
|
|
132
129
|
for part in parts:
|
|
133
130
|
modality = self._part_modalities.get(part.get("type", "").lower())
|
|
@@ -136,7 +133,7 @@ class ChatContentRendererRegistry:
|
|
|
136
133
|
return False
|
|
137
134
|
|
|
138
135
|
def _flatten_parts_to_text(
|
|
139
|
-
self, parts: List[ContentPart], modality_flags: Mapping[str, bool]
|
|
136
|
+
self, parts: List[prompt_types.ContentPart], modality_flags: Mapping[str, bool]
|
|
140
137
|
) -> str:
|
|
141
138
|
segments: List[str] = []
|
|
142
139
|
for part in parts:
|
|
@@ -161,7 +158,7 @@ class ChatContentRendererRegistry:
|
|
|
161
158
|
return "\n\n".join(segment for segment in segments if segment)
|
|
162
159
|
|
|
163
160
|
@staticmethod
|
|
164
|
-
def _extract_placeholder_value(part: ContentPart) -> str:
|
|
161
|
+
def _extract_placeholder_value(part: prompt_types.ContentPart) -> str:
|
|
165
162
|
part_type = part.get("type", "").lower()
|
|
166
163
|
if part_type == "image_url":
|
|
167
164
|
image_dict = part.get("image_url", {})
|
|
@@ -186,9 +183,9 @@ DEFAULT_CHAT_RENDERER_REGISTRY = ChatContentRendererRegistry()
|
|
|
186
183
|
|
|
187
184
|
def register_default_chat_part_renderer(
|
|
188
185
|
part_type: str,
|
|
189
|
-
renderer: RendererFn,
|
|
186
|
+
renderer: prompt_types.RendererFn,
|
|
190
187
|
*,
|
|
191
|
-
modality: Optional[ModalityName] = None,
|
|
188
|
+
modality: Optional[prompt_types.ModalityName] = None,
|
|
192
189
|
placeholder: Optional[Tuple[str, str]] = None,
|
|
193
190
|
) -> None:
|
|
194
191
|
DEFAULT_CHAT_RENDERER_REGISTRY.register_part_renderer(
|
|
@@ -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
|
|
@@ -2,16 +2,17 @@ import copy
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any, Dict, Optional, Union, List
|
|
5
|
-
|
|
6
|
-
from opik.rest_api
|
|
7
|
-
from .
|
|
8
|
-
from
|
|
9
|
-
from
|
|
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
|
|
10
11
|
|
|
11
12
|
LOGGER = logging.getLogger(__name__)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
class Prompt:
|
|
15
|
+
class Prompt(base_prompt.BasePrompt):
|
|
15
16
|
"""
|
|
16
17
|
Prompt class represents a prompt with a name, prompt text/template and commit hash.
|
|
17
18
|
"""
|
|
@@ -21,18 +22,27 @@ class Prompt:
|
|
|
21
22
|
name: str,
|
|
22
23
|
prompt: str,
|
|
23
24
|
metadata: Optional[Dict[str, Any]] = None,
|
|
24
|
-
type: PromptType = PromptType.MUSTACHE,
|
|
25
|
+
type: prompt_types.PromptType = prompt_types.PromptType.MUSTACHE,
|
|
26
|
+
validate_placeholders: bool = True,
|
|
25
27
|
) -> None:
|
|
26
28
|
"""
|
|
27
29
|
Initializes a new instance of the class with the given parameters.
|
|
28
|
-
Creates a new prompt using the opik client and sets the initial state of the instance attributes based on the created prompt.
|
|
30
|
+
Creates a new text prompt using the opik client and sets the initial state of the instance attributes based on the created prompt.
|
|
29
31
|
|
|
30
32
|
Parameters:
|
|
31
33
|
name: The name for the prompt.
|
|
32
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).
|
|
33
41
|
"""
|
|
34
42
|
|
|
35
|
-
self._template = PromptTemplate(
|
|
43
|
+
self._template = prompt_template.PromptTemplate(
|
|
44
|
+
template=prompt, type=type, validate_placeholders=validate_placeholders
|
|
45
|
+
)
|
|
36
46
|
self._name = name
|
|
37
47
|
self._metadata = metadata
|
|
38
48
|
self._type = type
|
|
@@ -56,6 +66,7 @@ class Prompt:
|
|
|
56
66
|
self.__internal_api__version_id__ = prompt_version.id
|
|
57
67
|
|
|
58
68
|
@property
|
|
69
|
+
@override
|
|
59
70
|
def name(self) -> str:
|
|
60
71
|
"""The name of the prompt."""
|
|
61
72
|
return self._name
|
|
@@ -66,20 +77,24 @@ class Prompt:
|
|
|
66
77
|
return str(self._template)
|
|
67
78
|
|
|
68
79
|
@property
|
|
80
|
+
@override
|
|
69
81
|
def commit(self) -> Optional[str]:
|
|
70
82
|
"""The commit hash of the prompt."""
|
|
71
83
|
return self._commit
|
|
72
84
|
|
|
73
85
|
@property
|
|
86
|
+
@override
|
|
74
87
|
def metadata(self) -> Optional[Dict[str, Any]]:
|
|
75
88
|
"""The metadata dictionary associated with the prompt"""
|
|
76
89
|
return copy.deepcopy(self._metadata)
|
|
77
90
|
|
|
78
91
|
@property
|
|
79
|
-
|
|
92
|
+
@override
|
|
93
|
+
def type(self) -> prompt_types.PromptType:
|
|
80
94
|
"""The prompt type of the prompt."""
|
|
81
95
|
return self._type
|
|
82
96
|
|
|
97
|
+
@override
|
|
83
98
|
def format(self, **kwargs: Any) -> Union[str, List[Dict[str, Any]]]:
|
|
84
99
|
"""
|
|
85
100
|
Replaces placeholders in the template with provided keyword arguments.
|
|
@@ -109,11 +124,37 @@ class Prompt:
|
|
|
109
124
|
|
|
110
125
|
return formatted_string
|
|
111
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
|
+
|
|
112
153
|
@classmethod
|
|
113
154
|
def from_fern_prompt_version(
|
|
114
155
|
cls,
|
|
115
156
|
name: str,
|
|
116
|
-
prompt_version: PromptVersionDetail,
|
|
157
|
+
prompt_version: rest_api_types.PromptVersionDetail,
|
|
117
158
|
) -> "Prompt":
|
|
118
159
|
# will not call __init__ to avoid API calls, create new instance with __new__
|
|
119
160
|
prompt = cls.__new__(cls)
|
|
@@ -122,30 +163,12 @@ class Prompt:
|
|
|
122
163
|
prompt.__internal_api__prompt_id__ = prompt_version.prompt_id
|
|
123
164
|
|
|
124
165
|
prompt._name = name
|
|
125
|
-
prompt._template = PromptTemplate(
|
|
166
|
+
prompt._template = prompt_template.PromptTemplate(
|
|
126
167
|
template=prompt_version.template,
|
|
127
|
-
type=PromptType(prompt_version.type)
|
|
168
|
+
type=prompt_types.PromptType(prompt_version.type)
|
|
169
|
+
or prompt_types.PromptType.MUSTACHE,
|
|
128
170
|
)
|
|
129
171
|
prompt._commit = prompt_version.commit
|
|
130
172
|
prompt._metadata = prompt_version.metadata
|
|
131
173
|
prompt._type = prompt_version.type
|
|
132
174
|
return prompt
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def to_info_dict(prompt: Prompt) -> Dict[str, Any]:
|
|
136
|
-
info_dict: Dict[str, Any] = {
|
|
137
|
-
"name": prompt.name,
|
|
138
|
-
"version": {
|
|
139
|
-
"template": prompt.prompt,
|
|
140
|
-
},
|
|
141
|
-
}
|
|
142
|
-
if prompt.__internal_api__prompt_id__ is not None:
|
|
143
|
-
info_dict["id"] = prompt.__internal_api__prompt_id__
|
|
144
|
-
|
|
145
|
-
if prompt.commit is not None:
|
|
146
|
-
info_dict["version"]["commit"] = prompt.commit
|
|
147
|
-
|
|
148
|
-
if prompt.__internal_api__version_id__ is not None:
|
|
149
|
-
info_dict["version"]["id"] = prompt.__internal_api__version_id__
|
|
150
|
-
|
|
151
|
-
return info_dict
|
|
@@ -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())
|
|
@@ -36,7 +39,7 @@ class PromptTemplate:
|
|
|
36
39
|
replacement = "" if value is None else str(value)
|
|
37
40
|
template = template.replace(f"{{{{{key}}}}}", replacement)
|
|
38
41
|
|
|
39
|
-
elif self._type == PromptType.JINJA2:
|
|
42
|
+
elif self._type == prompt_types.PromptType.JINJA2:
|
|
40
43
|
template = jinja2.Template(self._template).render(**kwargs)
|
|
41
44
|
else:
|
|
42
45
|
template = self._template
|
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/config.py
CHANGED
|
@@ -7,7 +7,6 @@ import pathlib
|
|
|
7
7
|
import urllib.parse
|
|
8
8
|
from typing import Any, Dict, Final, List, Literal, Optional, Tuple, Type, Union
|
|
9
9
|
|
|
10
|
-
import opik.decorator.tracing_runtime_config as tracing_runtime_config
|
|
11
10
|
import pydantic
|
|
12
11
|
import pydantic_settings
|
|
13
12
|
from pydantic_settings import BaseSettings, InitSettingsSource
|
|
@@ -257,10 +256,6 @@ class OpikConfig(pydantic_settings.BaseSettings):
|
|
|
257
256
|
def guardrails_backend_host(self) -> str:
|
|
258
257
|
return url_helpers.get_base_url(self.url_override) + "guardrails/"
|
|
259
258
|
|
|
260
|
-
@property
|
|
261
|
-
def runtime(self) -> tracing_runtime_config.TracingRuntimeConfig:
|
|
262
|
-
return tracing_runtime_config.runtime_cfg
|
|
263
|
-
|
|
264
259
|
@pydantic.model_validator(mode="after")
|
|
265
260
|
def _set_url_override_from_api_key(self) -> "OpikConfig":
|
|
266
261
|
url_was_not_provided = (
|