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.
Files changed (248) hide show
  1. opik/__init__.py +10 -3
  2. opik/anonymizer/__init__.py +5 -0
  3. opik/anonymizer/anonymizer.py +12 -0
  4. opik/anonymizer/factory.py +80 -0
  5. opik/anonymizer/recursive_anonymizer.py +64 -0
  6. opik/anonymizer/rules.py +56 -0
  7. opik/anonymizer/rules_anonymizer.py +35 -0
  8. opik/api_objects/dataset/rest_operations.py +5 -0
  9. opik/api_objects/experiment/experiment.py +46 -49
  10. opik/api_objects/experiment/helpers.py +34 -10
  11. opik/api_objects/local_recording.py +8 -3
  12. opik/api_objects/opik_client.py +230 -48
  13. opik/api_objects/opik_query_language.py +9 -0
  14. opik/api_objects/prompt/__init__.py +11 -3
  15. opik/api_objects/prompt/base_prompt.py +69 -0
  16. opik/api_objects/prompt/base_prompt_template.py +29 -0
  17. opik/api_objects/prompt/chat/__init__.py +1 -0
  18. opik/api_objects/prompt/chat/chat_prompt.py +193 -0
  19. opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
  20. opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +37 -35
  21. opik/api_objects/prompt/client.py +101 -30
  22. opik/api_objects/prompt/text/__init__.py +1 -0
  23. opik/api_objects/prompt/text/prompt.py +174 -0
  24. opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
  25. opik/api_objects/prompt/types.py +1 -1
  26. opik/cli/export.py +6 -2
  27. opik/cli/usage_report/charts.py +39 -10
  28. opik/cli/usage_report/cli.py +164 -45
  29. opik/cli/usage_report/pdf.py +14 -1
  30. opik/config.py +0 -5
  31. opik/decorator/base_track_decorator.py +37 -40
  32. opik/decorator/context_manager/span_context_manager.py +9 -0
  33. opik/decorator/context_manager/trace_context_manager.py +5 -0
  34. opik/dict_utils.py +3 -3
  35. opik/evaluation/__init__.py +13 -2
  36. opik/evaluation/engine/engine.py +195 -223
  37. opik/evaluation/engine/helpers.py +8 -7
  38. opik/evaluation/engine/metrics_evaluator.py +237 -0
  39. opik/evaluation/evaluation_result.py +35 -1
  40. opik/evaluation/evaluator.py +318 -30
  41. opik/evaluation/models/litellm/util.py +78 -6
  42. opik/evaluation/models/model_capabilities.py +33 -0
  43. opik/evaluation/report.py +14 -2
  44. opik/evaluation/rest_operations.py +36 -33
  45. opik/evaluation/test_case.py +2 -2
  46. opik/evaluation/types.py +9 -1
  47. opik/exceptions.py +17 -0
  48. opik/hooks/__init__.py +17 -1
  49. opik/hooks/anonymizer_hook.py +36 -0
  50. opik/id_helpers.py +18 -0
  51. opik/integrations/adk/helpers.py +16 -7
  52. opik/integrations/adk/legacy_opik_tracer.py +7 -4
  53. opik/integrations/adk/opik_tracer.py +3 -1
  54. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
  55. opik/integrations/adk/recursive_callback_injector.py +1 -6
  56. opik/integrations/dspy/callback.py +1 -4
  57. opik/integrations/haystack/opik_connector.py +2 -2
  58. opik/integrations/haystack/opik_tracer.py +2 -4
  59. opik/integrations/langchain/opik_tracer.py +273 -82
  60. opik/integrations/llama_index/callback.py +110 -108
  61. opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
  62. opik/integrations/openai/opik_tracker.py +1 -1
  63. opik/message_processing/batching/batchers.py +11 -7
  64. opik/message_processing/encoder_helpers.py +79 -0
  65. opik/message_processing/messages.py +25 -1
  66. opik/message_processing/online_message_processor.py +23 -8
  67. opik/opik_context.py +7 -7
  68. opik/rest_api/__init__.py +188 -12
  69. opik/rest_api/client.py +3 -0
  70. opik/rest_api/dashboards/__init__.py +4 -0
  71. opik/rest_api/dashboards/client.py +462 -0
  72. opik/rest_api/dashboards/raw_client.py +648 -0
  73. opik/rest_api/datasets/client.py +893 -89
  74. opik/rest_api/datasets/raw_client.py +1328 -87
  75. opik/rest_api/experiments/client.py +30 -2
  76. opik/rest_api/experiments/raw_client.py +26 -0
  77. opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
  78. opik/rest_api/optimizations/client.py +302 -0
  79. opik/rest_api/optimizations/raw_client.py +463 -0
  80. opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
  81. opik/rest_api/prompts/__init__.py +2 -2
  82. opik/rest_api/prompts/client.py +34 -4
  83. opik/rest_api/prompts/raw_client.py +32 -2
  84. opik/rest_api/prompts/types/__init__.py +3 -1
  85. opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
  86. opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
  87. opik/rest_api/spans/__init__.py +0 -2
  88. opik/rest_api/spans/client.py +148 -64
  89. opik/rest_api/spans/raw_client.py +210 -83
  90. opik/rest_api/spans/types/__init__.py +0 -2
  91. opik/rest_api/traces/client.py +241 -73
  92. opik/rest_api/traces/raw_client.py +344 -90
  93. opik/rest_api/types/__init__.py +200 -15
  94. opik/rest_api/types/aggregation_data.py +1 -0
  95. opik/rest_api/types/alert_trigger_config_public_type.py +6 -1
  96. opik/rest_api/types/alert_trigger_config_type.py +6 -1
  97. opik/rest_api/types/alert_trigger_config_write_type.py +6 -1
  98. opik/rest_api/types/automation_rule_evaluator.py +23 -1
  99. opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
  100. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
  101. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
  102. opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
  103. opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
  104. opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
  105. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
  106. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
  107. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
  108. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
  109. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
  110. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
  111. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
  112. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
  113. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
  114. opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
  115. opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
  116. opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
  117. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
  118. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
  119. opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
  120. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
  121. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
  122. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
  123. opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
  124. opik/rest_api/types/boolean_feedback_definition.py +25 -0
  125. opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
  126. opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
  127. opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
  128. opik/rest_api/types/boolean_feedback_detail.py +29 -0
  129. opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
  130. opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
  131. opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
  132. opik/rest_api/types/dashboard_page_public.py +24 -0
  133. opik/rest_api/types/dashboard_public.py +30 -0
  134. opik/rest_api/types/dataset.py +2 -0
  135. opik/rest_api/types/dataset_item.py +2 -0
  136. opik/rest_api/types/dataset_item_compare.py +2 -0
  137. opik/rest_api/types/dataset_item_filter.py +23 -0
  138. opik/rest_api/types/dataset_item_filter_operator.py +21 -0
  139. opik/rest_api/types/dataset_item_page_compare.py +1 -0
  140. opik/rest_api/types/dataset_item_page_public.py +1 -0
  141. opik/rest_api/types/dataset_item_public.py +2 -0
  142. opik/rest_api/types/dataset_item_update.py +39 -0
  143. opik/rest_api/types/dataset_item_write.py +1 -0
  144. opik/rest_api/types/dataset_public.py +2 -0
  145. opik/rest_api/types/dataset_public_status.py +5 -0
  146. opik/rest_api/types/dataset_status.py +5 -0
  147. opik/rest_api/types/dataset_version_diff.py +22 -0
  148. opik/rest_api/types/dataset_version_diff_stats.py +24 -0
  149. opik/rest_api/types/dataset_version_page_public.py +23 -0
  150. opik/rest_api/types/dataset_version_public.py +49 -0
  151. opik/rest_api/types/experiment.py +2 -0
  152. opik/rest_api/types/experiment_public.py +2 -0
  153. opik/rest_api/types/experiment_score.py +20 -0
  154. opik/rest_api/types/experiment_score_public.py +20 -0
  155. opik/rest_api/types/experiment_score_write.py +20 -0
  156. opik/rest_api/types/feedback.py +20 -1
  157. opik/rest_api/types/feedback_create.py +16 -1
  158. opik/rest_api/types/feedback_object_public.py +22 -1
  159. opik/rest_api/types/feedback_public.py +20 -1
  160. opik/rest_api/types/feedback_score_public.py +4 -0
  161. opik/rest_api/types/feedback_update.py +16 -1
  162. opik/rest_api/types/image_url.py +20 -0
  163. opik/rest_api/types/image_url_public.py +20 -0
  164. opik/rest_api/types/image_url_write.py +20 -0
  165. opik/rest_api/types/llm_as_judge_message.py +5 -1
  166. opik/rest_api/types/llm_as_judge_message_content.py +24 -0
  167. opik/rest_api/types/llm_as_judge_message_content_public.py +24 -0
  168. opik/rest_api/types/llm_as_judge_message_content_write.py +24 -0
  169. opik/rest_api/types/llm_as_judge_message_public.py +5 -1
  170. opik/rest_api/types/llm_as_judge_message_write.py +5 -1
  171. opik/rest_api/types/llm_as_judge_model_parameters.py +2 -0
  172. opik/rest_api/types/llm_as_judge_model_parameters_public.py +2 -0
  173. opik/rest_api/types/llm_as_judge_model_parameters_write.py +2 -0
  174. opik/rest_api/types/optimization.py +2 -0
  175. opik/rest_api/types/optimization_public.py +2 -0
  176. opik/rest_api/types/optimization_public_status.py +3 -1
  177. opik/rest_api/types/optimization_status.py +3 -1
  178. opik/rest_api/types/optimization_studio_config.py +27 -0
  179. opik/rest_api/types/optimization_studio_config_public.py +27 -0
  180. opik/rest_api/types/optimization_studio_config_write.py +27 -0
  181. opik/rest_api/types/optimization_studio_log.py +22 -0
  182. opik/rest_api/types/optimization_write.py +2 -0
  183. opik/rest_api/types/optimization_write_status.py +3 -1
  184. opik/rest_api/types/prompt.py +6 -0
  185. opik/rest_api/types/prompt_detail.py +6 -0
  186. opik/rest_api/types/prompt_detail_template_structure.py +5 -0
  187. opik/rest_api/types/prompt_public.py +6 -0
  188. opik/rest_api/types/prompt_public_template_structure.py +5 -0
  189. opik/rest_api/types/prompt_template_structure.py +5 -0
  190. opik/rest_api/types/prompt_version.py +2 -0
  191. opik/rest_api/types/prompt_version_detail.py +2 -0
  192. opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
  193. opik/rest_api/types/prompt_version_public.py +2 -0
  194. opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
  195. opik/rest_api/types/prompt_version_template_structure.py +5 -0
  196. opik/rest_api/types/score_name.py +1 -0
  197. opik/rest_api/types/service_toggles_config.py +6 -0
  198. opik/rest_api/types/span_enrichment_options.py +31 -0
  199. opik/rest_api/types/span_filter.py +23 -0
  200. opik/rest_api/types/span_filter_operator.py +21 -0
  201. opik/rest_api/types/span_filter_write.py +23 -0
  202. opik/rest_api/types/span_filter_write_operator.py +21 -0
  203. opik/rest_api/types/span_llm_as_judge_code.py +27 -0
  204. opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
  205. opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
  206. opik/rest_api/types/span_update.py +46 -0
  207. opik/rest_api/types/studio_evaluation.py +20 -0
  208. opik/rest_api/types/studio_evaluation_public.py +20 -0
  209. opik/rest_api/types/studio_evaluation_write.py +20 -0
  210. opik/rest_api/types/studio_llm_model.py +21 -0
  211. opik/rest_api/types/studio_llm_model_public.py +21 -0
  212. opik/rest_api/types/studio_llm_model_write.py +21 -0
  213. opik/rest_api/types/studio_message.py +20 -0
  214. opik/rest_api/types/studio_message_public.py +20 -0
  215. opik/rest_api/types/studio_message_write.py +20 -0
  216. opik/rest_api/types/studio_metric.py +21 -0
  217. opik/rest_api/types/studio_metric_public.py +21 -0
  218. opik/rest_api/types/studio_metric_write.py +21 -0
  219. opik/rest_api/types/studio_optimizer.py +21 -0
  220. opik/rest_api/types/studio_optimizer_public.py +21 -0
  221. opik/rest_api/types/studio_optimizer_write.py +21 -0
  222. opik/rest_api/types/studio_prompt.py +20 -0
  223. opik/rest_api/types/studio_prompt_public.py +20 -0
  224. opik/rest_api/types/studio_prompt_write.py +20 -0
  225. opik/rest_api/types/trace.py +6 -0
  226. opik/rest_api/types/trace_public.py +6 -0
  227. opik/rest_api/types/trace_thread_filter_write.py +23 -0
  228. opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
  229. opik/rest_api/types/trace_thread_update.py +19 -0
  230. opik/rest_api/types/trace_update.py +39 -0
  231. opik/rest_api/types/value_entry.py +2 -0
  232. opik/rest_api/types/value_entry_compare.py +2 -0
  233. opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
  234. opik/rest_api/types/value_entry_public.py +2 -0
  235. opik/rest_api/types/video_url.py +19 -0
  236. opik/rest_api/types/video_url_public.py +19 -0
  237. opik/rest_api/types/video_url_write.py +19 -0
  238. opik/synchronization.py +5 -6
  239. opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
  240. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/METADATA +5 -4
  241. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/RECORD +246 -151
  242. opik/api_objects/prompt/chat_prompt_template.py +0 -164
  243. opik/api_objects/prompt/prompt.py +0 -131
  244. /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
  245. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
  246. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
  247. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
  248. {opik-1.9.5.dist-info → opik-1.9.39.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",
@@ -0,0 +1,5 @@
1
+ from .anonymizer import Anonymizer
2
+ from .factory import create_anonymizer
3
+ from .recursive_anonymizer import RecursiveAnonymizer
4
+
5
+ __all__ = ["Anonymizer", "create_anonymizer", "RecursiveAnonymizer"]
@@ -0,0 +1,12 @@
1
+ import abc
2
+ from typing import Dict, Any, Union, List
3
+
4
+ AnonymizerDataType = Union[Dict[str, Any], str, List[Any]]
5
+
6
+
7
+ class Anonymizer(abc.ABC):
8
+ """Abstract base class for anonymizing sensitive data in various data structures."""
9
+
10
+ @abc.abstractmethod
11
+ def anonymize(self, data: AnonymizerDataType, **kwargs: Any) -> AnonymizerDataType:
12
+ pass
@@ -0,0 +1,80 @@
1
+ from typing import Union, List, Dict, Callable, Tuple
2
+
3
+ from . import anonymizer, rules_anonymizer, rules
4
+
5
+ RulesType = Union[
6
+ List[Dict[str, str]],
7
+ List[Tuple[str, str]],
8
+ List[Callable[[str], str]],
9
+ List[Union[Dict[str, str], Tuple[str, str], Callable[[str], str]]],
10
+ Dict[str, str],
11
+ Tuple[str, str],
12
+ Callable[[str], str],
13
+ ]
14
+
15
+
16
+ def create_anonymizer(
17
+ anonymizer_rules: RulesType, max_depth: int = 10
18
+ ) -> anonymizer.Anonymizer:
19
+ """Create an anonymizer with the specified rules.
20
+
21
+ Args:
22
+ anonymizer_rules: Anonymizer rules specification in various formats:
23
+ - Dict with "regex" and "replace" keys for a single regex rule
24
+ - Tuple with (regex, replacement) for a single regex rule
25
+ - Callable that takes a string and returns anonymized string
26
+ - List of any of the above for multiple rules
27
+ max_depth: Maximum recursion depth for nested data structures.
28
+
29
+ Returns:
30
+ An Anonymizer instance configured with the specified rules.
31
+
32
+ Raises:
33
+ ValueError: If a rule format is invalid.
34
+ """
35
+ rule_objects: List[rules.Rule] = []
36
+
37
+ if callable(anonymizer_rules):
38
+ # Single function rule
39
+ rule_objects.append(rules.FunctionRule(anonymizer_rules))
40
+ elif isinstance(anonymizer_rules, dict):
41
+ # Single dictionary rule
42
+ _check_dictionary_rule(anonymizer_rules)
43
+ rule_objects.append(
44
+ rules.RegexRule(anonymizer_rules["regex"], anonymizer_rules["replace"])
45
+ )
46
+ elif isinstance(anonymizer_rules, tuple):
47
+ # Single tuple rule
48
+ _check_tuple_rule(anonymizer_rules)
49
+ regex_pattern, replacement = anonymizer_rules
50
+ rule_objects.append(rules.RegexRule(regex_pattern, replacement))
51
+ elif isinstance(anonymizer_rules, list):
52
+ # List of rules
53
+ for rule in anonymizer_rules:
54
+ if callable(rule) and not isinstance(rule, (dict, tuple)):
55
+ rule_objects.append(rules.FunctionRule(rule))
56
+ elif isinstance(rule, dict):
57
+ _check_dictionary_rule(rule)
58
+ rule_objects.append(rules.RegexRule(rule["regex"], rule["replace"]))
59
+ elif isinstance(rule, tuple):
60
+ _check_tuple_rule(rule)
61
+ regex_pattern, replacement = rule
62
+ rule_objects.append(rules.RegexRule(regex_pattern, replacement))
63
+ else:
64
+ raise ValueError(f"Unsupported rule type in list: {type(rule)}")
65
+ else:
66
+ raise ValueError(f"Unsupported rules type: {type(anonymizer_rules)}")
67
+
68
+ return rules_anonymizer.RulesAnonymizer(rule_objects, max_depth=max_depth)
69
+
70
+
71
+ def _check_dictionary_rule(rule: Dict[str, str]) -> None:
72
+ if "regex" not in rule or "replace" not in rule:
73
+ raise ValueError("Dictionary rule must have 'regex' and 'replace' keys")
74
+
75
+
76
+ def _check_tuple_rule(rule: Tuple[str, str]) -> None:
77
+ if len(rule) != 2:
78
+ raise ValueError(
79
+ "Tuple rule must have exactly 2 elements: (regex, replacement)"
80
+ )
@@ -0,0 +1,64 @@
1
+ import abc
2
+ from typing import Any, Optional
3
+
4
+ from . import anonymizer
5
+
6
+
7
+ class RecursiveAnonymizer(anonymizer.Anonymizer):
8
+ """Abstract base class for anonymizing sensitive data in various data structures.
9
+
10
+ This class provides a framework for recursively anonymizing text content within
11
+ nested data structures such as dictionaries, lists, and strings. Subclasses must
12
+ implement the anonymize_text() method to define the specific anonymization logic.
13
+ """
14
+
15
+ def __init__(self, max_depth: int = 10):
16
+ """Initialize the Anonymizer with depth limiting.
17
+
18
+ Args:
19
+ max_depth: Maximum recursion depth to prevent infinite loops when
20
+ processing deeply nested or circular data structures.
21
+ Defaults to 10.
22
+ """
23
+ self.max_depth = max_depth
24
+
25
+ def anonymize(
26
+ self, data: anonymizer.AnonymizerDataType, **kwargs: Any
27
+ ) -> anonymizer.AnonymizerDataType:
28
+ return self._recursive_anonymize(data, **kwargs)
29
+
30
+ @abc.abstractmethod
31
+ def anonymize_text(self, data: str, **kwargs: Any) -> str:
32
+ pass
33
+
34
+ def _recursive_anonymize(
35
+ self,
36
+ data: anonymizer.AnonymizerDataType,
37
+ depth: int = 0,
38
+ field_name: Optional[str] = None,
39
+ **kwargs: Any,
40
+ ) -> anonymizer.AnonymizerDataType:
41
+ if depth >= self.max_depth:
42
+ return data
43
+
44
+ if field_name is None:
45
+ field_name = ""
46
+
47
+ if isinstance(data, str):
48
+ return self.anonymize_text(data, field_name=field_name, **kwargs)
49
+ elif isinstance(data, dict):
50
+ return {
51
+ key: self._recursive_anonymize(
52
+ value, depth + 1, field_name=f"{field_name}.{key}", **kwargs
53
+ )
54
+ for key, value in data.items()
55
+ }
56
+ elif isinstance(data, list):
57
+ return [
58
+ self._recursive_anonymize(
59
+ item, depth + 1, field_name=f"{field_name}.{i}", **kwargs
60
+ )
61
+ for i, item in enumerate(data)
62
+ ]
63
+ else:
64
+ return data
@@ -0,0 +1,56 @@
1
+ import abc
2
+ import re
3
+ from typing import Callable
4
+
5
+
6
+ class Rule(abc.ABC):
7
+ """Abstract base class for text anonymization rules.
8
+
9
+ Rules define specific patterns or conditions for anonymizing sensitive
10
+ information in text. Subclasses must implement the apply() method to
11
+ define the anonymization logic.
12
+ """
13
+
14
+ @abc.abstractmethod
15
+ def apply(self, text: str) -> str:
16
+ pass
17
+
18
+
19
+ class RegexRule(Rule):
20
+ """A rule that uses regular expressions to find and replace patterns in text.
21
+
22
+ This rule compiles a regular expression pattern and applies it to input text,
23
+ replacing all matches with a specified replacement string.
24
+ """
25
+
26
+ def __init__(self, regex: str, replacement: str):
27
+ """Initialize the regex rule with a pattern and replacement.
28
+
29
+ Args:
30
+ regex: Regular expression pattern to match sensitive data.
31
+ replacement: String to replace matched patterns with.
32
+ """
33
+ self.pattern = re.compile(regex)
34
+ self.replacement = replacement
35
+
36
+ def apply(self, text: str) -> str:
37
+ return self.pattern.sub(self.replacement, text)
38
+
39
+
40
+ class FunctionRule(Rule):
41
+ """A rule that applies a custom function to anonymize text.
42
+
43
+ This rule allows for flexible anonymization by accepting any callable
44
+ that takes a string as input and returns an anonymized string.
45
+ """
46
+
47
+ def __init__(self, func: Callable[[str], str]):
48
+ """Initialize the function rule with a custom anonymization function.
49
+
50
+ Args:
51
+ func: A callable that takes a string and returns an anonymized version.
52
+ """
53
+ self.func = func
54
+
55
+ def apply(self, text: str) -> str:
56
+ return self.func(text)
@@ -0,0 +1,35 @@
1
+ from typing import List, Any
2
+
3
+ from . import recursive_anonymizer, rules
4
+
5
+
6
+ class RulesAnonymizer(recursive_anonymizer.RecursiveAnonymizer):
7
+ """An anonymizer that applies a list of rules sequentially to text data.
8
+
9
+ This class takes a list of Rule objects and applies them to
10
+ anonymize sensitive information in text.
11
+ """
12
+
13
+ def __init__(self, anonymizer_rules: List[rules.Rule], max_depth: int = 10):
14
+ """Initialize the RulesAnonymizer with a list of rules.
15
+
16
+ Args:
17
+ anonymizer_rules: List of Rule objects to apply for anonymization.
18
+ max_depth: Maximum recursion depth for nested data structures.
19
+ """
20
+ super().__init__(max_depth)
21
+ self.rules = anonymizer_rules
22
+
23
+ def anonymize_text(self, data: str, **kwargs: Any) -> str:
24
+ """Apply all rules sequentially to the input text.
25
+
26
+ Args:
27
+ data: The text to anonymize.
28
+
29
+ Returns:
30
+ The anonymized text after applying all rules.
31
+ """
32
+ result = data
33
+ for rule in self.rules:
34
+ result = rule.apply(result)
35
+ return result
@@ -1,9 +1,12 @@
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
4
6
  from opik.message_processing import streamer
5
7
  from . import dataset
6
8
  from .. import experiment
9
+ from ..experiment import experiments_client
7
10
  from ...rest_api.core.api_error import ApiError
8
11
 
9
12
 
@@ -60,6 +63,7 @@ def get_dataset_experiments(
60
63
  dataset_id: str,
61
64
  max_results: int,
62
65
  streamer: streamer.Streamer,
66
+ experiments_client: experiments_client.ExperimentsClient,
63
67
  ) -> List[experiment.Experiment]:
64
68
  page_size = 100
65
69
  experiments: List[experiment.Experiment] = []
@@ -83,6 +87,7 @@ def get_dataset_experiments(
83
87
  dataset_name=experiment_.dataset_name,
84
88
  rest_client=rest_client,
85
89
  streamer=streamer,
90
+ experiments_client=experiments_client,
86
91
  )
87
92
  )
88
93
 
@@ -1,15 +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
- import opik.rest_api
6
5
  from opik.message_processing.batching import sequence_splitter
7
6
  from opik.message_processing import messages, streamer
8
7
  from opik.rest_api import client as rest_api_client
9
- from opik.rest_api.types import experiment_public
10
- from . import experiment_item
11
- from .. import constants, helpers, rest_stream_parser
12
- from ...api_objects.prompt import Prompt
8
+ from opik.rest_api import types as rest_api_types
9
+ from . import experiment_item, experiments_client
10
+ from .. import constants, helpers
11
+ from ...api_objects.prompt import base_prompt
12
+
13
+ if TYPE_CHECKING:
14
+ from opik.evaluation.metrics import score_result
13
15
 
14
16
  LOGGER = logging.getLogger(__name__)
15
17
 
@@ -22,7 +24,8 @@ class Experiment:
22
24
  dataset_name: str,
23
25
  rest_client: rest_api_client.OpikApi,
24
26
  streamer: streamer.Streamer,
25
- prompts: Optional[List[Prompt]] = None,
27
+ experiments_client: experiments_client.ExperimentsClient,
28
+ prompts: Optional[List[base_prompt.BasePrompt]] = None,
26
29
  ) -> None:
27
30
  self._id = id
28
31
  self._name = name
@@ -30,6 +33,7 @@ class Experiment:
30
33
  self._rest_client = rest_client
31
34
  self._prompts = prompts
32
35
  self._streamer = streamer
36
+ self._experiments_client = experiments_client
33
37
 
34
38
  @property
35
39
  def id(self) -> str:
@@ -59,7 +63,7 @@ class Experiment:
59
63
  def experiments_rest_client(self) -> rest_api_client.ExperimentsClient:
60
64
  return self._rest_client.experiments
61
65
 
62
- def get_experiment_data(self) -> experiment_public.ExperimentPublic:
66
+ def get_experiment_data(self) -> rest_api_types.experiment_public.ExperimentPublic:
63
67
  return self._rest_client.experiments.get_experiment_by_id(id=self.id)
64
68
 
65
69
  def insert(
@@ -100,55 +104,48 @@ class Experiment:
100
104
 
101
105
  def get_items(
102
106
  self,
103
- max_results: Optional[int] = None,
107
+ max_results: Optional[int] = 10000,
104
108
  truncate: bool = False,
105
109
  ) -> List[experiment_item.ExperimentItemContent]:
106
110
  """
107
- Retrieves and returns a list of experiment items by streaming from the backend in batches, with an option to
108
- truncate the results for each batch.
109
-
110
- This method streams experiment items from a backend service in chunks up to the specified `max_results`
111
- or until the available items are exhausted. It handles batch-wise retrieval and parsing, ensuring the client
112
- receives a list of `ExperimentItemContent` objects, while respecting the constraints on maximum retrieval size
113
- from the backend. If truncation is enabled, the backend may return truncated details for each item.
111
+ Retrieves and returns a list of experiment items for this experiment.
114
112
 
115
113
  Args:
116
- max_results: Maximum number of experiment items to retrieve.
117
- truncate: Whether to truncate the items returned by the backend.
114
+ max_results: Maximum number of experiment items to retrieve. Defaults to 10000 if not specified.
115
+ truncate: Whether to truncate the items returned by the backend. Defaults to False.
118
116
 
117
+ Returns:
118
+ List of ExperimentItemContent objects for this experiment.
119
119
  """
120
- result: List[experiment_item.ExperimentItemContent] = []
121
- max_endpoint_batch_size = rest_stream_parser.MAX_ENDPOINT_BATCH_SIZE
122
-
123
- while True:
124
- if max_results is None:
125
- current_batch_size = max_endpoint_batch_size
126
- else:
127
- current_batch_size = min(
128
- max_results - len(result), max_endpoint_batch_size
129
- )
130
-
131
- items_stream = self._rest_client.experiments.stream_experiment_items(
132
- experiment_name=self.name,
133
- limit=current_batch_size,
134
- last_retrieved_id=result[-1].id if len(result) > 0 else None,
135
- truncate=truncate,
136
- )
120
+ if max_results is None:
121
+ max_results = 10000 # TODO: remove this once we have a proper way to get all experiment items
122
+
123
+ return self._experiments_client.find_experiment_items_for_dataset(
124
+ dataset_name=self.dataset_name,
125
+ experiment_ids=[self.id],
126
+ truncate=truncate,
127
+ max_results=max_results,
128
+ )
137
129
 
138
- experiment_item_compare_current_batch = (
139
- rest_stream_parser.read_and_parse_stream(
140
- stream=items_stream,
141
- item_class=opik.rest_api.ExperimentItemCompare,
142
- )
143
- )
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] = []
144
136
 
145
- for item in experiment_item_compare_current_batch:
146
- converted_item = experiment_item.ExperimentItemContent.from_rest_experiment_item_compare(
147
- value=item
148
- )
149
- result.append(converted_item)
137
+ for score_result_ in score_results:
138
+ if score_result_.scoring_failed:
139
+ continue
150
140
 
151
- if current_batch_size > len(experiment_item_compare_current_batch):
152
- break
141
+ experiment_score = rest_api_types.ExperimentScore(
142
+ name=score_result_.name,
143
+ value=score_result_.value,
144
+ )
145
+ experiment_scores.append(experiment_score)
153
146
 
154
- return result
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(