opik 1.9.26__py3-none-any.whl → 1.9.41__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.
Files changed (181) hide show
  1. opik/__init__.py +10 -3
  2. opik/api_objects/dataset/rest_operations.py +2 -0
  3. opik/api_objects/experiment/experiment.py +31 -5
  4. opik/api_objects/experiment/helpers.py +34 -10
  5. opik/api_objects/local_recording.py +8 -3
  6. opik/api_objects/opik_client.py +218 -46
  7. opik/api_objects/opik_query_language.py +9 -0
  8. opik/api_objects/prompt/__init__.py +11 -3
  9. opik/api_objects/prompt/base_prompt.py +69 -0
  10. opik/api_objects/prompt/base_prompt_template.py +29 -0
  11. opik/api_objects/prompt/chat/__init__.py +1 -0
  12. opik/api_objects/prompt/chat/chat_prompt.py +193 -0
  13. opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
  14. opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +31 -34
  15. opik/api_objects/prompt/client.py +101 -30
  16. opik/api_objects/prompt/text/__init__.py +1 -0
  17. opik/api_objects/prompt/{prompt.py → text/prompt.py} +55 -32
  18. opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +8 -5
  19. opik/cli/export.py +6 -2
  20. opik/config.py +0 -5
  21. opik/decorator/base_track_decorator.py +37 -40
  22. opik/evaluation/__init__.py +13 -2
  23. opik/evaluation/engine/engine.py +195 -223
  24. opik/evaluation/engine/helpers.py +8 -7
  25. opik/evaluation/engine/metrics_evaluator.py +237 -0
  26. opik/evaluation/evaluation_result.py +35 -1
  27. opik/evaluation/evaluator.py +309 -23
  28. opik/evaluation/models/litellm/util.py +78 -6
  29. opik/evaluation/report.py +14 -2
  30. opik/evaluation/rest_operations.py +6 -9
  31. opik/evaluation/test_case.py +2 -2
  32. opik/evaluation/types.py +9 -1
  33. opik/exceptions.py +17 -0
  34. opik/id_helpers.py +18 -0
  35. opik/integrations/adk/helpers.py +16 -7
  36. opik/integrations/adk/legacy_opik_tracer.py +7 -4
  37. opik/integrations/adk/opik_tracer.py +3 -1
  38. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
  39. opik/integrations/dspy/callback.py +1 -4
  40. opik/integrations/haystack/opik_connector.py +2 -2
  41. opik/integrations/haystack/opik_tracer.py +2 -4
  42. opik/integrations/langchain/opik_tracer.py +1 -4
  43. opik/integrations/llama_index/callback.py +2 -4
  44. opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
  45. opik/integrations/openai/opik_tracker.py +1 -1
  46. opik/opik_context.py +7 -7
  47. opik/rest_api/__init__.py +127 -11
  48. opik/rest_api/dashboards/client.py +65 -2
  49. opik/rest_api/dashboards/raw_client.py +82 -0
  50. opik/rest_api/datasets/client.py +538 -2
  51. opik/rest_api/datasets/raw_client.py +1347 -441
  52. opik/rest_api/experiments/client.py +30 -2
  53. opik/rest_api/experiments/raw_client.py +26 -0
  54. opik/rest_api/optimizations/client.py +302 -0
  55. opik/rest_api/optimizations/raw_client.py +463 -0
  56. opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
  57. opik/rest_api/prompts/__init__.py +2 -2
  58. opik/rest_api/prompts/client.py +34 -4
  59. opik/rest_api/prompts/raw_client.py +32 -2
  60. opik/rest_api/prompts/types/__init__.py +3 -1
  61. opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
  62. opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
  63. opik/rest_api/traces/client.py +6 -6
  64. opik/rest_api/traces/raw_client.py +4 -4
  65. opik/rest_api/types/__init__.py +125 -11
  66. opik/rest_api/types/aggregation_data.py +1 -0
  67. opik/rest_api/types/automation_rule_evaluator.py +23 -1
  68. opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
  69. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
  70. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
  71. opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
  72. opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
  73. opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
  74. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
  75. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
  76. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
  77. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
  78. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
  79. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
  80. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
  81. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
  82. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
  83. opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
  84. opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
  85. opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
  86. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
  87. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
  88. opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
  89. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
  90. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
  91. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
  92. opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
  93. opik/rest_api/types/dashboard_page_public.py +1 -0
  94. opik/rest_api/types/dataset.py +4 -0
  95. opik/rest_api/types/dataset_item.py +1 -0
  96. opik/rest_api/types/dataset_item_compare.py +1 -0
  97. opik/rest_api/types/dataset_item_page_compare.py +1 -0
  98. opik/rest_api/types/dataset_item_page_public.py +1 -0
  99. opik/rest_api/types/dataset_item_public.py +1 -0
  100. opik/rest_api/types/dataset_public.py +4 -0
  101. opik/rest_api/types/dataset_public_status.py +5 -0
  102. opik/rest_api/types/dataset_status.py +5 -0
  103. opik/rest_api/types/dataset_version_diff.py +22 -0
  104. opik/rest_api/types/dataset_version_diff_stats.py +24 -0
  105. opik/rest_api/types/dataset_version_page_public.py +23 -0
  106. opik/rest_api/types/dataset_version_public.py +54 -0
  107. opik/rest_api/types/dataset_version_summary.py +41 -0
  108. opik/rest_api/types/dataset_version_summary_public.py +41 -0
  109. opik/rest_api/types/experiment.py +2 -0
  110. opik/rest_api/types/experiment_public.py +2 -0
  111. opik/rest_api/types/experiment_score.py +20 -0
  112. opik/rest_api/types/experiment_score_public.py +20 -0
  113. opik/rest_api/types/experiment_score_write.py +20 -0
  114. opik/rest_api/types/feedback_score_public.py +4 -0
  115. opik/rest_api/types/group_content_with_aggregations.py +1 -0
  116. opik/rest_api/types/optimization.py +2 -0
  117. opik/rest_api/types/optimization_public.py +2 -0
  118. opik/rest_api/types/optimization_public_status.py +3 -1
  119. opik/rest_api/types/optimization_status.py +3 -1
  120. opik/rest_api/types/optimization_studio_config.py +27 -0
  121. opik/rest_api/types/optimization_studio_config_public.py +27 -0
  122. opik/rest_api/types/optimization_studio_config_write.py +27 -0
  123. opik/rest_api/types/optimization_studio_log.py +22 -0
  124. opik/rest_api/types/optimization_write.py +2 -0
  125. opik/rest_api/types/optimization_write_status.py +3 -1
  126. opik/rest_api/types/prompt.py +6 -0
  127. opik/rest_api/types/prompt_detail.py +6 -0
  128. opik/rest_api/types/prompt_detail_template_structure.py +5 -0
  129. opik/rest_api/types/prompt_public.py +6 -0
  130. opik/rest_api/types/prompt_public_template_structure.py +5 -0
  131. opik/rest_api/types/prompt_template_structure.py +5 -0
  132. opik/rest_api/types/prompt_version.py +2 -0
  133. opik/rest_api/types/prompt_version_detail.py +2 -0
  134. opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
  135. opik/rest_api/types/prompt_version_public.py +2 -0
  136. opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
  137. opik/rest_api/types/prompt_version_template_structure.py +5 -0
  138. opik/rest_api/types/score_name.py +1 -0
  139. opik/rest_api/types/service_toggles_config.py +5 -0
  140. opik/rest_api/types/span_filter.py +23 -0
  141. opik/rest_api/types/span_filter_operator.py +21 -0
  142. opik/rest_api/types/span_filter_write.py +23 -0
  143. opik/rest_api/types/span_filter_write_operator.py +21 -0
  144. opik/rest_api/types/span_llm_as_judge_code.py +27 -0
  145. opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
  146. opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
  147. opik/rest_api/types/studio_evaluation.py +20 -0
  148. opik/rest_api/types/studio_evaluation_public.py +20 -0
  149. opik/rest_api/types/studio_evaluation_write.py +20 -0
  150. opik/rest_api/types/studio_llm_model.py +21 -0
  151. opik/rest_api/types/studio_llm_model_public.py +21 -0
  152. opik/rest_api/types/studio_llm_model_write.py +21 -0
  153. opik/rest_api/types/studio_message.py +20 -0
  154. opik/rest_api/types/studio_message_public.py +20 -0
  155. opik/rest_api/types/studio_message_write.py +20 -0
  156. opik/rest_api/types/studio_metric.py +21 -0
  157. opik/rest_api/types/studio_metric_public.py +21 -0
  158. opik/rest_api/types/studio_metric_write.py +21 -0
  159. opik/rest_api/types/studio_optimizer.py +21 -0
  160. opik/rest_api/types/studio_optimizer_public.py +21 -0
  161. opik/rest_api/types/studio_optimizer_write.py +21 -0
  162. opik/rest_api/types/studio_prompt.py +20 -0
  163. opik/rest_api/types/studio_prompt_public.py +20 -0
  164. opik/rest_api/types/studio_prompt_write.py +20 -0
  165. opik/rest_api/types/trace.py +6 -0
  166. opik/rest_api/types/trace_public.py +6 -0
  167. opik/rest_api/types/trace_thread_filter_write.py +23 -0
  168. opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
  169. opik/rest_api/types/value_entry.py +2 -0
  170. opik/rest_api/types/value_entry_compare.py +2 -0
  171. opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
  172. opik/rest_api/types/value_entry_public.py +2 -0
  173. opik/synchronization.py +5 -6
  174. opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
  175. {opik-1.9.26.dist-info → opik-1.9.41.dist-info}/METADATA +4 -3
  176. {opik-1.9.26.dist-info → opik-1.9.41.dist-info}/RECORD +180 -120
  177. opik/api_objects/prompt/chat_prompt_template.py +0 -200
  178. {opik-1.9.26.dist-info → opik-1.9.41.dist-info}/WHEEL +0 -0
  179. {opik-1.9.26.dist-info → opik-1.9.41.dist-info}/entry_points.txt +0 -0
  180. {opik-1.9.26.dist-info → opik-1.9.41.dist-info}/licenses/LICENSE +0 -0
  181. {opik-1.9.26.dist-info → opik-1.9.41.dist-info}/top_level.txt +0 -0
opik/__init__.py CHANGED
@@ -6,18 +6,23 @@ from .api_objects.experiment.experiment_item import (
6
6
  ExperimentItemReferences,
7
7
  )
8
8
  from .api_objects.opik_client import Opik
9
- from .api_objects.prompt import Prompt
9
+ from .api_objects.prompt import Prompt, ChatPrompt
10
10
  from .api_objects.prompt.types import PromptType
11
11
  from .api_objects.span import Span
12
12
  from .api_objects.trace import Trace
13
13
  from .configurator.configure import configure
14
14
  from .decorator.tracker import flush_tracker, track
15
- from .evaluation import evaluate, evaluate_experiment, evaluate_prompt
15
+ from .evaluation import (
16
+ evaluate,
17
+ evaluate_experiment,
18
+ evaluate_on_dict_items,
19
+ evaluate_prompt,
20
+ )
16
21
  from .integrations.sagemaker import auth as sagemaker_auth
17
22
  from .plugins.pytest.decorator import llm_unit
18
23
  from .types import LLMProvider
19
24
  from . import opik_context
20
- from .decorator.tracing_runtime_config import (
25
+ from .tracing_runtime_config import (
21
26
  is_tracing_active,
22
27
  reset_tracing_to_config_default,
23
28
  set_tracing_active,
@@ -37,6 +42,7 @@ __all__ = [
37
42
  "evaluate",
38
43
  "evaluate_prompt",
39
44
  "evaluate_experiment",
45
+ "evaluate_on_dict_items",
40
46
  "ExperimentItemContent",
41
47
  "ExperimentItemReferences",
42
48
  "track",
@@ -49,6 +55,7 @@ __all__ = [
49
55
  "llm_unit",
50
56
  "configure",
51
57
  "Prompt",
58
+ "ChatPrompt",
52
59
  "PromptType",
53
60
  "LLMProvider",
54
61
  "reset_tracing_to_config_default",
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import List
2
4
  from opik.rest_api import OpikApi
3
5
  import opik.exceptions as exceptions
@@ -1,14 +1,17 @@
1
1
  import functools
2
2
  import logging
3
- from typing import List, Optional
3
+ from typing import List, Optional, TYPE_CHECKING
4
4
 
5
5
  from opik.message_processing.batching import sequence_splitter
6
6
  from opik.message_processing import messages, streamer
7
7
  from opik.rest_api import client as rest_api_client
8
- from opik.rest_api.types import experiment_public
8
+ from opik.rest_api import types as rest_api_types
9
9
  from . import experiment_item, experiments_client
10
10
  from .. import constants, helpers
11
- from ...api_objects.prompt import Prompt
11
+ from ...api_objects.prompt import base_prompt
12
+
13
+ if TYPE_CHECKING:
14
+ from opik.evaluation.metrics import score_result
12
15
 
13
16
  LOGGER = logging.getLogger(__name__)
14
17
 
@@ -22,7 +25,7 @@ class Experiment:
22
25
  rest_client: rest_api_client.OpikApi,
23
26
  streamer: streamer.Streamer,
24
27
  experiments_client: experiments_client.ExperimentsClient,
25
- prompts: Optional[List[Prompt]] = None,
28
+ prompts: Optional[List[base_prompt.BasePrompt]] = None,
26
29
  ) -> None:
27
30
  self._id = id
28
31
  self._name = name
@@ -60,7 +63,7 @@ class Experiment:
60
63
  def experiments_rest_client(self) -> rest_api_client.ExperimentsClient:
61
64
  return self._rest_client.experiments
62
65
 
63
- def get_experiment_data(self) -> experiment_public.ExperimentPublic:
66
+ def get_experiment_data(self) -> rest_api_types.experiment_public.ExperimentPublic:
64
67
  return self._rest_client.experiments.get_experiment_by_id(id=self.id)
65
68
 
66
69
  def insert(
@@ -123,3 +126,26 @@ class Experiment:
123
126
  truncate=truncate,
124
127
  max_results=max_results,
125
128
  )
129
+
130
+ def log_experiment_scores(
131
+ self,
132
+ score_results: List["score_result.ScoreResult"],
133
+ ) -> None:
134
+ """Log experiment-level scores to the backend."""
135
+ experiment_scores: List[rest_api_types.ExperimentScore] = []
136
+
137
+ for score_result_ in score_results:
138
+ if score_result_.scoring_failed:
139
+ continue
140
+
141
+ experiment_score = rest_api_types.ExperimentScore(
142
+ name=score_result_.name,
143
+ value=score_result_.value,
144
+ )
145
+ experiment_scores.append(experiment_score)
146
+
147
+ if experiment_scores:
148
+ self._rest_client.experiments.update_experiment(
149
+ id=self.id,
150
+ experiment_scores=experiment_scores,
151
+ )
@@ -1,8 +1,12 @@
1
+ import copy
1
2
  import logging
2
- import opik.jsonable_encoder as jsonable_encoder
3
3
  from typing import Any, Dict, List, Mapping, Optional, Tuple
4
- import copy
5
- from .. import prompt
4
+
5
+ from opik import id_helpers
6
+ import opik.jsonable_encoder as jsonable_encoder
7
+
8
+ from ..prompt import base_prompt
9
+
6
10
 
7
11
  LOGGER = logging.getLogger(__name__)
8
12
 
@@ -11,7 +15,7 @@ PromptVersion = Dict[str, str]
11
15
 
12
16
  def build_metadata_and_prompt_versions(
13
17
  experiment_config: Optional[Dict[str, Any]],
14
- prompts: Optional[List[prompt.Prompt]],
18
+ prompts: Optional[List[base_prompt.BasePrompt]],
15
19
  ) -> Tuple[Optional[Dict[str, Any]], Optional[List[PromptVersion]]]:
16
20
  prompt_versions: Optional[List[PromptVersion]] = None
17
21
 
@@ -37,9 +41,20 @@ def build_metadata_and_prompt_versions(
37
41
  prompt_versions = []
38
42
  experiment_config["prompts"] = {}
39
43
 
40
- for prompt in prompts:
41
- prompt_versions.append({"id": prompt.__internal_api__version_id__})
42
- experiment_config["prompts"][prompt.name] = prompt.prompt
44
+ for prompt_obj in prompts:
45
+ prompt_versions.append({"id": prompt_obj.__internal_api__version_id__})
46
+ # Use __internal_api__to_info_dict__() to get the prompt content in a consistent way
47
+ prompt_info = prompt_obj.__internal_api__to_info_dict__()
48
+ # Extract the template/messages from the version dict
49
+ if "version" in prompt_info:
50
+ if "template" in prompt_info["version"]:
51
+ experiment_config["prompts"][prompt_obj.name] = prompt_info[
52
+ "version"
53
+ ]["template"]
54
+ elif "messages" in prompt_info["version"]:
55
+ experiment_config["prompts"][prompt_obj.name] = prompt_info[
56
+ "version"
57
+ ]["messages"]
43
58
 
44
59
  if experiment_config == {}:
45
60
  return None, None
@@ -50,9 +65,9 @@ def build_metadata_and_prompt_versions(
50
65
 
51
66
 
52
67
  def handle_prompt_args(
53
- prompt: Optional[prompt.Prompt] = None,
54
- prompts: Optional[List[prompt.Prompt]] = None,
55
- ) -> Optional[List[prompt.Prompt]]:
68
+ prompt: Optional[base_prompt.BasePrompt] = None,
69
+ prompts: Optional[List[base_prompt.BasePrompt]] = None,
70
+ ) -> Optional[List[base_prompt.BasePrompt]]:
56
71
  if prompts is not None and len(prompts) > 0 and prompt is not None:
57
72
  LOGGER.warning(
58
73
  "Arguments `prompt` and `prompts` are mutually exclusive, `prompts` will be used`."
@@ -63,3 +78,12 @@ def handle_prompt_args(
63
78
  prompts = None
64
79
 
65
80
  return prompts
81
+
82
+
83
+ def generate_unique_experiment_name(experiment_name_prefix: Optional[str]) -> str:
84
+ if experiment_name_prefix is None:
85
+ return id_helpers.generate_random_alphanumeric_string(12)
86
+
87
+ return (
88
+ f"{experiment_name_prefix}-{id_helpers.generate_random_alphanumeric_string(6)}"
89
+ )
@@ -1,6 +1,6 @@
1
1
  import contextlib
2
2
  from typing import Iterator, List
3
-
3
+ from typing import Optional
4
4
  from . import opik_client
5
5
  from ..message_processing import message_processors_chain
6
6
  from ..message_processing.emulation import local_emulator_message_processor, models
@@ -33,9 +33,13 @@ class _LocalRecordingHandle:
33
33
 
34
34
 
35
35
  @contextlib.contextmanager
36
- def record_traces_locally() -> Iterator[_LocalRecordingHandle]:
36
+ def record_traces_locally(
37
+ client: Optional[opik_client.Opik] = None,
38
+ ) -> Iterator[_LocalRecordingHandle]:
37
39
  """Enable local recording of traces/spans within the context.
38
40
 
41
+ Args:
42
+ client: Optional Opik client to use for recording. If not provided, the default session client will be used.
39
43
  Usage:
40
44
  with opik.record_traces_locally() as storage:
41
45
  # your code that creates traces/spans
@@ -44,7 +48,8 @@ def record_traces_locally() -> Iterator[_LocalRecordingHandle]:
44
48
  Yields a handle with `span_trees` and `trace_trees` properties that flush
45
49
  the client before reading, ensuring all events are captured.
46
50
  """
47
- client = opik_client.get_client_cached()
51
+ if client is None:
52
+ client = opik_client.get_client_cached()
48
53
 
49
54
  # Disallow nested/local concurrent recordings in the same process
50
55
  existing_local = message_processors_chain.get_local_emulator_message_processor(
@@ -24,8 +24,8 @@ from .dataset import rest_operations as dataset_rest_operations
24
24
  from .experiment import experiments_client
25
25
  from .experiment import helpers as experiment_helpers
26
26
  from .experiment import rest_operations as experiment_rest_operations
27
- from .prompt import Prompt, PromptType
28
- from .prompt.client import PromptClient
27
+ from . import prompt as prompt_module
28
+ from .prompt import client as prompt_client
29
29
  from .threads import threads_client
30
30
  from .trace import migration as trace_migration, trace_client
31
31
  from .. import (
@@ -917,8 +917,8 @@ class Opik:
917
917
  dataset_name: str,
918
918
  name: Optional[str] = None,
919
919
  experiment_config: Optional[Dict[str, Any]] = None,
920
- prompt: Optional[Prompt] = None,
921
- prompts: Optional[List[Prompt]] = None,
920
+ prompt: Optional[prompt_module.base_prompt.BasePrompt] = None,
921
+ prompts: Optional[List[prompt_module.base_prompt.BasePrompt]] = None,
922
922
  type: Literal["regular", "trial", "mini-batch"] = "regular",
923
923
  optimization_id: Optional[str] = None,
924
924
  ) -> experiment.Experiment:
@@ -972,6 +972,42 @@ class Opik:
972
972
 
973
973
  return experiment_
974
974
 
975
+ def update_experiment(
976
+ self,
977
+ id: str,
978
+ name: Optional[str] = None,
979
+ experiment_config: Optional[Dict[str, Any]] = None,
980
+ ) -> None:
981
+ """
982
+ Update an experiment's name and/or configuration.
983
+
984
+ Args:
985
+ id: The experiment ID.
986
+ name: The new name for the experiment. If None, the name will not be updated.
987
+ experiment_config: The new configuration for the experiment. If None, the configuration will not be updated.
988
+
989
+ Raises:
990
+ ValueError: if id is None or empty, or if both name and experiment_config are None
991
+ """
992
+ if not id:
993
+ raise ValueError(
994
+ f"id must be provided and can not be None or empty, id: {id}"
995
+ )
996
+
997
+ if name is None and experiment_config is None:
998
+ raise ValueError(
999
+ "At least one of 'name' or 'experiment_config' must be provided"
1000
+ )
1001
+
1002
+ # Only include parameters that are provided to avoid clearing fields
1003
+ request_params: Dict[str, Any] = {}
1004
+ if name is not None:
1005
+ request_params["name"] = name
1006
+ if experiment_config is not None:
1007
+ request_params["metadata"] = experiment_config
1008
+
1009
+ self._rest_client.experiments.update_experiment(id, **request_params)
1010
+
975
1011
  def get_experiment_by_name(self, name: str) -> experiment.Experiment:
976
1012
  """
977
1013
  Returns an existing experiment by its name.
@@ -991,7 +1027,7 @@ class Opik:
991
1027
 
992
1028
  return experiment.Experiment(
993
1029
  id=experiment_public.id,
994
- name=name,
1030
+ name=experiment_public.name,
995
1031
  dataset_name=experiment_public.dataset_name,
996
1032
  rest_client=self._rest_client,
997
1033
  streamer=self._streamer,
@@ -1000,10 +1036,11 @@ class Opik:
1000
1036
 
1001
1037
  def get_experiments_by_name(self, name: str) -> List[experiment.Experiment]:
1002
1038
  """
1003
- Returns a list of existing experiments by its name.
1039
+ Returns a list of existing experiments containing the given string in their name.
1040
+ Search is case-insensitive.
1004
1041
 
1005
1042
  Args:
1006
- name: The name of the experiment(s).
1043
+ name: The string to search for in the experiment names.
1007
1044
 
1008
1045
  Returns:
1009
1046
  List[experiment.Experiment]: List of existing experiments.
@@ -1016,7 +1053,7 @@ class Opik:
1016
1053
  for public_experiment in experiments_public:
1017
1054
  experiment_ = experiment.Experiment(
1018
1055
  id=public_experiment.id,
1019
- name=name,
1056
+ name=public_experiment.name,
1020
1057
  dataset_name=public_experiment.dataset_name,
1021
1058
  rest_client=self._rest_client,
1022
1059
  streamer=self._streamer,
@@ -1357,70 +1394,188 @@ class Opik:
1357
1394
  name: str,
1358
1395
  prompt: str,
1359
1396
  metadata: Optional[Dict[str, Any]] = None,
1360
- type: PromptType = PromptType.MUSTACHE,
1361
- ) -> Prompt:
1397
+ type: prompt_module.PromptType = prompt_module.PromptType.MUSTACHE,
1398
+ ) -> prompt_module.Prompt:
1362
1399
  """
1363
- Creates a new prompt with the given name and template.
1364
- If a prompt with the same name already exists, it will create a new version of the existing prompt if the templates differ.
1400
+ Creates a new text prompt with the given name and template.
1401
+ If a text prompt with the same name already exists, it will create a new version of the existing prompt if the templates differ.
1365
1402
 
1366
1403
  Parameters:
1367
1404
  name: The name of the prompt.
1368
1405
  prompt: The template content of the prompt.
1369
1406
  metadata: Optional metadata to be included in the prompt.
1407
+ type: The template type (MUSTACHE or JINJA2).
1370
1408
 
1371
1409
  Returns:
1372
1410
  A Prompt object containing details of the created or retrieved prompt.
1373
1411
 
1374
1412
  Raises:
1375
- ApiError: If there is an error during the creation of the prompt and the status code is not 409.
1413
+ PromptTemplateStructureMismatch: If a chat prompt with the same name already exists (template structure is immutable).
1414
+ ApiError: If there is an error during the creation of the prompt.
1376
1415
  """
1377
- prompt_client = PromptClient(self._rest_client)
1378
- prompt_version = prompt_client.create_prompt(
1416
+ prompt_client_ = prompt_client.PromptClient(self._rest_client)
1417
+ prompt_version = prompt_client_.create_prompt(
1379
1418
  name=name, prompt=prompt, metadata=metadata, type=type
1380
1419
  )
1381
- return Prompt.from_fern_prompt_version(name, prompt_version)
1420
+ return prompt_module.Prompt.from_fern_prompt_version(name, prompt_version)
1421
+
1422
+ def create_chat_prompt(
1423
+ self,
1424
+ name: str,
1425
+ messages: List[Dict[str, Any]],
1426
+ metadata: Optional[Dict[str, Any]] = None,
1427
+ type: prompt_module.PromptType = prompt_module.PromptType.MUSTACHE,
1428
+ ) -> prompt_module.ChatPrompt:
1429
+ """
1430
+ Creates a new chat prompt with the given name and message templates.
1431
+ If a chat prompt with the same name already exists, it will create a new version if the messages differ.
1432
+
1433
+ Parameters:
1434
+ name: The name of the chat prompt.
1435
+ messages: List of message dictionaries with 'role' and 'content' fields.
1436
+ metadata: Optional metadata to be included in the prompt.
1437
+ type: The template type (MUSTACHE or JINJA2).
1438
+
1439
+ Returns:
1440
+ A ChatPrompt object containing details of the created or retrieved chat prompt.
1441
+
1442
+ Raises:
1443
+ PromptTemplateStructureMismatch: If a text prompt with the same name already exists (template structure is immutable).
1444
+ ApiError: If there is an error during the creation of the prompt.
1445
+ """
1446
+ return prompt_module.ChatPrompt(
1447
+ name=name, messages=messages, metadata=metadata, type=type
1448
+ )
1382
1449
 
1383
1450
  def get_prompt(
1384
1451
  self,
1385
1452
  name: str,
1386
1453
  commit: Optional[str] = None,
1387
- ) -> Optional[Prompt]:
1454
+ ) -> Optional[prompt_module.Prompt]:
1388
1455
  """
1389
- Retrieve the prompt detail for a given prompt name and commit version.
1456
+ Retrieve a text prompt by name and optional commit version.
1457
+
1458
+ This method only returns text prompts.
1390
1459
 
1391
1460
  Parameters:
1392
1461
  name: The name of the prompt.
1393
1462
  commit: An optional commit version of the prompt. If not provided, the latest version is retrieved.
1394
1463
 
1395
1464
  Returns:
1396
- Prompt: The details of the specified prompt.
1465
+ Prompt: The details of the specified text prompt, or None if not found.
1466
+
1467
+ Raises:
1468
+ PromptTemplateStructureMismatch: If the prompt exists but is a chat prompt (template structure mismatch).
1397
1469
  """
1398
- prompt_client = PromptClient(self._rest_client)
1399
- fern_prompt_version = prompt_client.get_prompt(name=name, commit=commit)
1470
+ prompt_client_ = prompt_client.PromptClient(self._rest_client)
1471
+ fern_prompt_version = prompt_client_.get_prompt(
1472
+ name=name, commit=commit, raise_if_not_template_structure="text"
1473
+ )
1474
+
1400
1475
  if fern_prompt_version is None:
1401
1476
  return None
1402
1477
 
1403
- return Prompt.from_fern_prompt_version(name, fern_prompt_version)
1478
+ return prompt_module.Prompt.from_fern_prompt_version(name, fern_prompt_version)
1404
1479
 
1405
- def get_prompt_history(self, name: str) -> List[Prompt]:
1480
+ def get_chat_prompt(
1481
+ self,
1482
+ name: str,
1483
+ commit: Optional[str] = None,
1484
+ ) -> Optional[prompt_module.ChatPrompt]:
1406
1485
  """
1407
- Retrieve all the prompt versions history for a given prompt name.
1486
+ Retrieve a chat prompt by name and optional commit version.
1487
+
1488
+ This method only returns chat prompts.
1489
+
1490
+ Parameters:
1491
+ name: The name of the prompt.
1492
+ commit: An optional commit version of the prompt. If not provided, the latest version is retrieved.
1493
+
1494
+ Returns:
1495
+ ChatPrompt: The details of the specified chat prompt, or None if not found.
1496
+
1497
+ Raises:
1498
+ PromptTemplateStructureMismatch: If the prompt exists but is a text prompt (template structure mismatch).
1499
+ """
1500
+ prompt_client_ = prompt_client.PromptClient(self._rest_client)
1501
+ fern_prompt_version = prompt_client_.get_prompt(
1502
+ name=name, commit=commit, raise_if_not_template_structure="chat"
1503
+ )
1504
+
1505
+ if fern_prompt_version is None:
1506
+ return None
1507
+
1508
+ return prompt_module.ChatPrompt.from_fern_prompt_version(
1509
+ name, fern_prompt_version
1510
+ )
1511
+
1512
+ def get_prompt_history(self, name: str) -> List[prompt_module.Prompt]:
1513
+ """
1514
+ Retrieve all text prompt versions history for a given prompt name.
1408
1515
 
1409
1516
  Parameters:
1410
1517
  name: The name of the prompt.
1411
1518
 
1412
1519
  Returns:
1413
- List[Prompt]: A list of Prompt instances for the given name.
1520
+ List[Prompt]: A list of text Prompt instances for the given name, or an empty list if not found.
1521
+
1522
+ Raises:
1523
+ PromptTemplateStructureMismatch: If the prompt exists but is a chat prompt (template structure mismatch).
1414
1524
  """
1415
- prompt_client = PromptClient(self._rest_client)
1416
- fern_prompt_versions = prompt_client.get_all_prompt_versions(name=name)
1525
+ prompt_client_ = prompt_client.PromptClient(self._rest_client)
1526
+
1527
+ # First, validate that this is a text prompt by trying to get the latest version
1528
+ # Let PromptTemplateStructureMismatch exception propagate - this is a hard error
1529
+ latest_version = prompt_client_.get_prompt(
1530
+ name=name, raise_if_not_template_structure="text"
1531
+ )
1532
+
1533
+ if latest_version is None:
1534
+ return []
1535
+
1536
+ # Now get all versions (we know it's a text prompt)
1537
+ fern_prompt_versions = prompt_client_.get_all_prompt_versions(name=name)
1538
+
1417
1539
  result = [
1418
- Prompt.from_fern_prompt_version(name, version)
1540
+ prompt_module.Prompt.from_fern_prompt_version(name, version)
1419
1541
  for version in fern_prompt_versions
1420
1542
  ]
1421
1543
  return result
1422
1544
 
1423
- def get_all_prompts(self, name: str) -> List[Prompt]:
1545
+ def get_chat_prompt_history(self, name: str) -> List[prompt_module.ChatPrompt]:
1546
+ """
1547
+ Retrieve all chat prompt versions history for a given prompt name.
1548
+
1549
+ Parameters:
1550
+ name: The name of the prompt.
1551
+
1552
+ Returns:
1553
+ List[ChatPrompt]: A list of ChatPrompt instances for the given name, or an empty list if not found.
1554
+
1555
+ Raises:
1556
+ PromptTemplateStructureMismatch: If the prompt exists but is a text prompt (template structure mismatch).
1557
+ """
1558
+ prompt_client_ = prompt_client.PromptClient(self._rest_client)
1559
+
1560
+ # First, validate that this is a chat prompt by trying to get the latest version
1561
+ # Let PromptTemplateStructureMismatch exception propagate - this is a hard error
1562
+ latest_version = prompt_client_.get_prompt(
1563
+ name=name, raise_if_not_template_structure="chat"
1564
+ )
1565
+
1566
+ if latest_version is None:
1567
+ return []
1568
+
1569
+ # Now get all versions (we know it's a chat prompt)
1570
+ fern_prompt_versions = prompt_client_.get_all_prompt_versions(name=name)
1571
+
1572
+ result = [
1573
+ prompt_module.ChatPrompt.from_fern_prompt_version(name, version)
1574
+ for version in fern_prompt_versions
1575
+ ]
1576
+ return result
1577
+
1578
+ def get_all_prompts(self, name: str) -> List[prompt_module.Prompt]:
1424
1579
  """
1425
1580
  DEPRECATED: Please use Opik.get_prompt_history() instead.
1426
1581
  Retrieve all the prompt versions history for a given prompt name.
@@ -1429,16 +1584,18 @@ class Opik:
1429
1584
  name: The name of the prompt.
1430
1585
 
1431
1586
  Returns:
1432
- List[Prompt]: A list of Prompt instances for the given name.
1587
+ List[prompt_module.Prompt]: A list of Prompt instances for the given name.
1433
1588
  """
1434
1589
  LOGGER.warning(
1435
1590
  "Opik.get_all_prompts() is deprecated. Please use Opik.get_prompt_history() instead."
1436
1591
  )
1437
1592
  return self.get_prompt_history(name)
1438
1593
 
1439
- def search_prompts(self, filter_string: Optional[str] = None) -> List[Prompt]:
1594
+ def search_prompts(
1595
+ self, filter_string: Optional[str] = None
1596
+ ) -> List[Union[prompt_module.Prompt, prompt_module.ChatPrompt]]:
1440
1597
  """
1441
- Retrieve the latest prompt versions for the given search parameters.
1598
+ Retrieve the latest prompt versions (both string and chat prompts) for the given search parameters.
1442
1599
 
1443
1600
  Parameters:
1444
1601
  filter_string: A filter string to narrow down the search using Opik Query Language (OQL).
@@ -1448,11 +1605,13 @@ class Opik:
1448
1605
  - `id`, `name`: String fields
1449
1606
  - `tags`: List field (use "contains" operator only)
1450
1607
  - `created_by`: String field
1608
+ - `template_structure`: String field ("string" or "chat")
1451
1609
 
1452
1610
  Supported operators by column:
1453
1611
  - `id`: =, !=, contains, not_contains, starts_with, ends_with, >, <
1454
1612
  - `name`: =, !=, contains, not_contains, starts_with, ends_with, >, <
1455
1613
  - `created_by`: =, !=, contains, not_contains, starts_with, ends_with, >, <
1614
+ - `template_structure`: =, !=
1456
1615
  - `tags`: contains (only)
1457
1616
 
1458
1617
  Examples:
@@ -1461,23 +1620,36 @@ class Opik:
1461
1620
  - `name contains "summary"` - Filter by name substring
1462
1621
  - `created_by = "user@example.com"` - Filter by creator
1463
1622
  - `id starts_with "prompt_"` - Filter by ID prefix
1623
+ - `template_structure = "text"` - Only text prompts
1624
+ - `template_structure = "chat"` - Only chat prompts
1464
1625
 
1465
- If not provided, all prompts matching the name filter will be returned.
1626
+ If not provided, all prompts (both text and chat) will be returned.
1466
1627
 
1467
1628
  Returns:
1468
- List[Prompt]: A list of Prompt instances found.
1469
- """
1470
- parsed_filters = None
1471
- if filter_string:
1472
- oql = opik_query_language.OpikQueryLanguage(filter_string)
1473
- parsed_filters = oql.get_filter_expressions()
1474
-
1475
- prompt_client = PromptClient(self._rest_client)
1476
- name_and_versions = prompt_client.search_prompts(parsed_filters=parsed_filters)
1477
- prompts: List[Prompt] = [
1478
- Prompt.from_fern_prompt_version(prompt_name, version)
1479
- for (prompt_name, version) in name_and_versions
1480
- ]
1629
+ List[Union[Prompt, ChatPrompt]]: A list of Prompt and/or ChatPrompt instances found.
1630
+ """
1631
+ oql = opik_query_language.OpikQueryLanguage(filter_string or "")
1632
+ parsed_filters = oql.get_filter_expressions()
1633
+
1634
+ prompt_client_ = prompt_client.PromptClient(self._rest_client)
1635
+ search_results = prompt_client_.search_prompts(parsed_filters=parsed_filters)
1636
+
1637
+ # Convert to Prompt or ChatPrompt objects based on template_structure
1638
+ prompts: List[Union[prompt_module.Prompt, prompt_module.ChatPrompt]] = []
1639
+ for result in search_results:
1640
+ if result.template_structure == "chat":
1641
+ prompts.append(
1642
+ prompt_module.ChatPrompt.from_fern_prompt_version(
1643
+ result.name, result.prompt_version_detail
1644
+ )
1645
+ )
1646
+ else:
1647
+ prompts.append(
1648
+ prompt_module.Prompt.from_fern_prompt_version(
1649
+ result.name, result.prompt_version_detail
1650
+ )
1651
+ )
1652
+
1481
1653
  return prompts
1482
1654
 
1483
1655
  def create_optimization(
@@ -29,6 +29,7 @@ COLUMNS = {
29
29
  "type": "string",
30
30
  "model": "string",
31
31
  "provider": "string",
32
+ "template_structure": "string",
32
33
  }
33
34
 
34
35
  SUPPORTED_OPERATORS = {
@@ -121,6 +122,14 @@ SUPPORTED_OPERATORS = {
121
122
  ">",
122
123
  "<",
123
124
  ],
125
+ "template_structure": [
126
+ "=",
127
+ "contains",
128
+ "not_contains",
129
+ "starts_with",
130
+ "ends_with",
131
+ "!=",
132
+ ],
124
133
  }
125
134
 
126
135
 
@@ -1,9 +1,17 @@
1
- from .prompt import Prompt
1
+ from .text.prompt import Prompt
2
+ from .text.prompt_template import PromptTemplate
3
+ from .chat.chat_prompt import ChatPrompt
4
+ from .chat.chat_prompt_template import ChatPromptTemplate
2
5
  from .types import PromptType
3
- from .chat_prompt_template import ChatPromptTemplate
6
+ from .base_prompt import BasePrompt
7
+ from .base_prompt_template import BasePromptTemplate
4
8
 
5
9
  __all__ = [
6
- "Prompt",
7
10
  "PromptType",
11
+ "Prompt",
12
+ "ChatPrompt",
13
+ "PromptTemplate",
8
14
  "ChatPromptTemplate",
15
+ "BasePrompt",
16
+ "BasePromptTemplate",
9
17
  ]