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
@@ -1,21 +1,26 @@
1
+ import contextvars
1
2
  import logging
2
- from typing import Optional, Dict, List, Any
3
+ from typing import Optional, Dict, Any, List, Union
3
4
  import uuid
4
5
 
5
6
  from llama_index.core.callbacks import schema as llama_index_schema
6
7
  from llama_index.core.callbacks import base_handler
7
8
 
8
- import opik.opik_context as opik_context
9
- import opik.decorator.tracing_runtime_config as tracing_runtime_config
9
+ from opik import context_storage, tracing_runtime_config
10
+ from opik.decorator import arguments_helpers, span_creation_handler
10
11
  from opik.api_objects import opik_client, span, trace
11
12
 
12
13
  from . import event_parsing_utils
13
- from ...api_objects import helpers
14
14
 
15
15
  LOGGER = logging.getLogger(__name__)
16
16
 
17
-
18
17
  INDEX_CONSTRUCTION_TRACE_NAME = "index_construction"
18
+ LLAMA_INDEX_METADATA = {"created_from": "llama_index"}
19
+
20
+ # Context variable for root trace/span created by LlamaIndex
21
+ _llama_root: contextvars.ContextVar[Optional[Union[span.SpanData, trace.TraceData]]] = (
22
+ contextvars.ContextVar("_llama_root", default=None)
23
+ )
19
24
 
20
25
 
21
26
  def _get_last_event(trace_map: Dict[str, List[str]]) -> str:
@@ -36,20 +41,13 @@ class LlamaIndexCallbackHandler(base_handler.BaseCallbackHandler):
36
41
  project_name: Optional[str] = None,
37
42
  skip_index_construction_trace: bool = False,
38
43
  ):
39
- """
40
- Initialize the instance with optional customization to define event filters and project-
41
- specific data handling. The constructor sets up the necessary client and data mappings
42
- for operational processing.
43
-
44
- Parameters:
45
- event_starts_to_ignore: Optional list of event start types to be ignored during
46
- processing.
47
- event_ends_to_ignore: Optional list of event end types to be ignored during
48
- processing.
49
- project_name: Optional string representing the project name to establish context in
50
- client operations.
51
- skip_index_construction_trace: A boolean value determining whether to skip creation of trace/spans of index
52
- construction.
44
+ """Initialize LlamaIndex callback handler for Opik tracing.
45
+
46
+ Args:
47
+ event_starts_to_ignore: Event start types to be ignored during processing.
48
+ event_ends_to_ignore: Event end types to be ignored during processing.
49
+ project_name: Project name for trace/span context.
50
+ skip_index_construction_trace: Whether to skip index construction traces.
53
51
  """
54
52
  event_starts_to_ignore = (
55
53
  event_starts_to_ignore if event_starts_to_ignore else []
@@ -62,31 +60,13 @@ class LlamaIndexCallbackHandler(base_handler.BaseCallbackHandler):
62
60
 
63
61
  self._skip_index_construction_trace = skip_index_construction_trace
64
62
  self._project_name = project_name
65
- self._opik_client = opik_client.Opik(
66
- _use_batching=True,
67
- project_name=project_name,
68
- )
69
-
70
- self._opik_trace_data: Optional[trace.TraceData] = None
63
+ self._opik_client = opik_client.get_client_cached()
64
+ self._opik_context_storage = context_storage.get_current_context_instance()
71
65
 
66
+ # Event tracking - shared across contexts, but events have unique IDs
72
67
  self._map_event_id_to_span_data: Dict[str, span.SpanData] = {}
73
68
  self._map_event_id_to_output: Dict[str, Any] = {}
74
69
 
75
- def _create_trace_data(self, trace_name: Optional[str]) -> trace.TraceData:
76
- trace_data = trace.TraceData(
77
- name=trace_name,
78
- metadata={"created_from": "llama_index"},
79
- project_name=self._project_name,
80
- )
81
-
82
- if (
83
- self._opik_client.config.log_start_trace_span
84
- and tracing_runtime_config.is_tracing_active()
85
- ):
86
- self._opik_client.trace(**trace_data.as_start_parameters)
87
-
88
- return trace_data
89
-
90
70
  def start_trace(self, trace_id: Optional[str] = None) -> None:
91
71
  if (
92
72
  self._skip_index_construction_trace
@@ -94,18 +74,29 @@ class LlamaIndexCallbackHandler(base_handler.BaseCallbackHandler):
94
74
  ):
95
75
  return
96
76
 
97
- # When a new LLama Index trace is started, create a new trace in Opik
98
- existing_trace_data = opik_context.get_current_trace_data()
99
- if existing_trace_data is not None:
100
- self._opik_trace_data = existing_trace_data
101
- else:
102
- self._opik_trace_data = self._create_trace_data(trace_name=trace_id)
77
+ trace_name = trace_id if trace_id else "llama_index_operation"
103
78
 
104
- if (
105
- self._opik_client.config.log_start_trace_span
106
- and tracing_runtime_config.is_tracing_active()
107
- ):
108
- self._opik_client.trace(**self._opik_trace_data.as_start_parameters)
79
+ span_creation_result = span_creation_handler.create_span_respecting_context(
80
+ start_span_arguments=arguments_helpers.StartSpanParameters(
81
+ name=trace_name,
82
+ type="general",
83
+ project_name=self._project_name,
84
+ metadata=LLAMA_INDEX_METADATA,
85
+ ),
86
+ distributed_trace_headers=None,
87
+ opik_context_storage=self._opik_context_storage,
88
+ )
89
+
90
+ if span_creation_result.trace_data is not None:
91
+ self._opik_context_storage.set_trace_data(span_creation_result.trace_data)
92
+ self._opik_client.trace(
93
+ **span_creation_result.trace_data.as_start_parameters
94
+ )
95
+ _llama_root.set(span_creation_result.trace_data)
96
+ else:
97
+ self._opik_context_storage.add_span_data(span_creation_result.span_data)
98
+ self._opik_client.span(**span_creation_result.span_data.as_start_parameters)
99
+ _llama_root.set(span_creation_result.span_data)
109
100
 
110
101
  def end_trace(
111
102
  self,
@@ -115,21 +106,26 @@ class LlamaIndexCallbackHandler(base_handler.BaseCallbackHandler):
115
106
  if not trace_map:
116
107
  return
117
108
 
118
- # When a trace finishes, we first get the last event output
109
+ root = _llama_root.get()
110
+ if root is None:
111
+ return
112
+
119
113
  last_event = _get_last_event(trace_map)
120
114
  last_event_output = self._map_event_id_to_output.get(last_event, None)
121
115
 
122
- # And then end the trace with the optional output
123
- if self._opik_trace_data is not None:
124
- self._opik_trace_data.init_end_time().update(output=last_event_output)
125
- if tracing_runtime_config.is_tracing_active():
126
- self._opik_client.trace(**self._opik_trace_data.as_parameters)
127
- self._opik_trace_data = None
116
+ root.init_end_time().update(output=last_event_output)
117
+
118
+ if isinstance(root, span.SpanData):
119
+ # We created a wrapper span (external trace existed)
120
+ self._opik_client.span(**root.as_parameters)
121
+ self._opik_context_storage.pop_span_data(ensure_id=root.id)
122
+ elif isinstance(root, trace.TraceData):
123
+ # We created a trace (no external trace existed)
124
+ self._opik_client.trace(**root.as_parameters)
125
+ self._opik_context_storage.pop_trace_data(ensure_id=root.id)
128
126
 
129
- # Do not clean _map_event_id_to_span_data as streaming LLM events can
130
- # end after this method is called. _map_event_id_to_span_data is
131
- # individually cleaned after each event is ended
132
- self._map_event_id_to_output.clear()
127
+ # Clean up
128
+ _llama_root.set(None)
133
129
 
134
130
  def on_event_start(
135
131
  self,
@@ -142,53 +138,60 @@ class LlamaIndexCallbackHandler(base_handler.BaseCallbackHandler):
142
138
  if not event_id:
143
139
  event_id = str(uuid.uuid4())
144
140
 
145
- # the event is not part of a trace probably because we are skipping the index construction trace
146
- if self._opik_trace_data is None:
141
+ root_span_or_trace = _llama_root.get()
142
+
143
+ if root_span_or_trace is None:
147
144
  if not self._skip_index_construction_trace:
148
145
  LOGGER.warning(
149
- "No trace data found in context for event start. "
150
- "This is likely due to the fact that the trace is not started properly. "
151
- f"The parent_id: {parent_id}, event_type: {event_type}, event_id: {event_id}."
146
+ "No active LlamaIndex trace/span found in context. "
147
+ "parent_id=%s, event_type=%s, event_id=%s",
148
+ parent_id,
149
+ event_type,
150
+ event_id,
152
151
  )
153
-
154
152
  return event_id
155
153
 
156
- # Get parent span Id if it exists
157
- if parent_id and parent_id in self._map_event_id_to_span_data:
158
- opik_parent_id = self._map_event_id_to_span_data[parent_id].id
159
- else:
160
- opik_parent_id = None
161
-
162
- # Compute the span input based on the event payload
163
154
  span_input = event_parsing_utils.get_span_input_from_events(event_type, payload)
164
155
 
165
- project_name = helpers.resolve_child_span_project_name(
166
- parent_project_name=self._opik_trace_data.project_name,
167
- child_project_name=self._project_name,
168
- show_warning=self._opik_trace_data.created_by != "evaluation",
156
+ # Skip creating span if event duplicates root operation name
157
+ root_name = root_span_or_trace.name if root_span_or_trace else None
158
+ event_duplicates_root = (
159
+ parent_id == llama_index_schema.BASE_TRACE_EVENT
160
+ and event_type.value == root_name
169
161
  )
162
+ if event_duplicates_root:
163
+ if span_input:
164
+ root_span_or_trace.update(input=span_input)
165
+ return event_id
170
166
 
171
- # Create a new span for this event
172
- span_data = span.SpanData(
173
- trace_id=self._opik_trace_data.id,
174
- name=event_type.value,
175
- parent_span_id=opik_parent_id,
176
- type=(
177
- "llm" if event_type == llama_index_schema.CBEventType.LLM else "general"
167
+ span_creation_result = span_creation_handler.create_span_respecting_context(
168
+ start_span_arguments=arguments_helpers.StartSpanParameters(
169
+ name=event_type.value,
170
+ input=span_input,
171
+ type=(
172
+ "llm"
173
+ if event_type == llama_index_schema.CBEventType.LLM
174
+ else "general"
175
+ ),
176
+ project_name=self._project_name,
177
+ metadata=LLAMA_INDEX_METADATA,
178
178
  ),
179
- input=span_input,
180
- project_name=project_name,
179
+ distributed_trace_headers=None,
180
+ opik_context_storage=self._opik_context_storage,
181
181
  )
182
+ span_data = span_creation_result.span_data
182
183
  self._map_event_id_to_span_data[event_id] = span_data
184
+ self._opik_context_storage.add_span_data(span_data)
185
+
183
186
  if (
184
187
  self._opik_client.config.log_start_trace_span
185
188
  and tracing_runtime_config.is_tracing_active()
186
189
  ):
187
190
  self._opik_client.span(**span_data.as_start_parameters)
188
191
 
189
- # If the parent_id is a BASE_TRACE_EVENT, update the trace with the span input
190
- if parent_id == llama_index_schema.BASE_TRACE_EVENT and span_input:
191
- self._opik_trace_data.update(input=span_input)
192
+ # Update root input from first child event
193
+ if parent_id == llama_index_schema.BASE_TRACE_EVENT and span_input is not None:
194
+ root_span_or_trace.update(input=span_input)
192
195
 
193
196
  return event_id
194
197
 
@@ -199,32 +202,31 @@ class LlamaIndexCallbackHandler(base_handler.BaseCallbackHandler):
199
202
  event_id: Optional[str] = None,
200
203
  **kwargs: Any,
201
204
  ) -> None:
202
- # Get the span output from the event and store it so we can use it if needed
203
- # when finishing the trace
204
205
  span_output = event_parsing_utils.get_span_output_from_event(
205
206
  event_type, payload
206
207
  )
207
-
208
208
  error_info = event_parsing_utils.get_span_error_info(payload)
209
209
 
210
- if event_id:
211
- self._map_event_id_to_output[event_id] = span_output
210
+ if not event_id:
211
+ return
212
212
 
213
- # Log the output to the span with the matching id
214
- if event_id in self._map_event_id_to_span_data:
215
- span_data = self._map_event_id_to_span_data[event_id]
213
+ # Store output for end_trace
214
+ self._map_event_id_to_output[event_id] = span_output
216
215
 
217
- llm_usage_info = event_parsing_utils.get_usage_data(payload)
218
- span_data.update(**llm_usage_info.__dict__)
216
+ # Finalize span if it exists
217
+ if event_id in self._map_event_id_to_span_data:
218
+ span_data = self._map_event_id_to_span_data[event_id]
219
219
 
220
- span_data.update(
221
- output=span_output, error_info=error_info
222
- ).init_end_time()
223
- if tracing_runtime_config.is_tracing_active():
224
- self._opik_client.span(**span_data.as_parameters)
220
+ llm_usage_info = event_parsing_utils.get_usage_data(payload)
221
+ span_data.update(**llm_usage_info.__dict__)
222
+ span_data.update(output=span_output, error_info=error_info).init_end_time()
223
+
224
+ if tracing_runtime_config.is_tracing_active():
225
+ self._opik_client.span(**span_data.as_parameters)
225
226
 
226
- del self._map_event_id_to_span_data[event_id]
227
+ self._opik_context_storage.pop_span_data(ensure_id=span_data.id)
228
+ del self._map_event_id_to_span_data[event_id]
227
229
 
228
230
  def flush(self) -> None:
229
- """Sends pending Opik data to the backend"""
231
+ """Flush pending Opik data to backend."""
230
232
  self._opik_client.flush()
@@ -3,12 +3,11 @@ from agents import tracing
3
3
 
4
4
  import logging
5
5
 
6
+ from opik import context_storage, tracing_runtime_config
6
7
  from opik.api_objects.span import span_data
7
8
  from opik.api_objects.trace import trace_data
8
9
  from opik.api_objects import opik_client
9
10
  from opik.decorator import span_creation_handler, arguments_helpers
10
- import opik.decorator.tracing_runtime_config as tracing_runtime_config
11
- import opik.context_storage as context_storage
12
11
 
13
12
  from . import span_data_parsers
14
13
 
@@ -18,7 +18,7 @@ def track_openai(
18
18
  """Adds Opik tracking wrappers to an OpenAI client.
19
19
 
20
20
  The client is always patched; however every wrapped call checks
21
- `opik.decorator.tracing_runtime_config.is_tracing_active()` before emitting
21
+ `opik.is_tracing_active()` before emitting
22
22
  any telemetry. If tracing is disabled at call time, the wrapped function
23
23
  executes normally but no span/trace is sent.
24
24
 
@@ -1,11 +1,11 @@
1
1
  from typing import Union, cast, List
2
2
 
3
- from . import base_batcher, sequence_splitter
4
- from .. import messages
5
3
  from opik.rest_api.types import span_write, trace_write
6
- import opik.jsonable_encoder as jsonable_encoder
7
4
  import opik.dict_utils as dict_utils
8
5
 
6
+ from . import base_batcher, sequence_splitter
7
+ from .. import messages, encoder_helpers
8
+
9
9
 
10
10
  class CreateSpanMessageBatcher(base_batcher.BaseBatcher):
11
11
  def _create_batches_from_accumulated_messages( # type: ignore
@@ -18,8 +18,10 @@ class CreateSpanMessageBatcher(base_batcher.BaseBatcher):
18
18
  cleaned_span_write_kwargs = dict_utils.remove_none_from_dict(
19
19
  span_write_kwargs
20
20
  )
21
- cleaned_span_write_kwargs = jsonable_encoder.encode(
22
- cleaned_span_write_kwargs
21
+ cleaned_span_write_kwargs = encoder_helpers.encode_and_anonymize(
22
+ cleaned_span_write_kwargs,
23
+ fields_to_anonymize=messages.CreateSpansBatchMessage.fields_to_anonymize(),
24
+ object_type="span",
23
25
  )
24
26
  rest_spans.append(span_write.SpanWrite(**cleaned_span_write_kwargs))
25
27
 
@@ -52,8 +54,10 @@ class CreateTraceMessageBatcher(base_batcher.BaseBatcher):
52
54
  cleaned_trace_write_kwargs = dict_utils.remove_none_from_dict(
53
55
  trace_write_kwargs
54
56
  )
55
- cleaned_trace_write_kwargs = jsonable_encoder.encode(
56
- cleaned_trace_write_kwargs
57
+ cleaned_trace_write_kwargs = encoder_helpers.encode_and_anonymize(
58
+ cleaned_trace_write_kwargs,
59
+ fields_to_anonymize=messages.CreateTraceBatchMessage.fields_to_anonymize(),
60
+ object_type="trace",
57
61
  )
58
62
  rest_traces.append(trace_write.TraceWrite(**cleaned_trace_write_kwargs))
59
63
 
@@ -0,0 +1,79 @@
1
+ from typing import Dict, Any, Set, List, Literal
2
+
3
+ import opik.hooks
4
+
5
+ from .. import jsonable_encoder
6
+ from ..anonymizer import anonymizer
7
+
8
+
9
+ def encode_and_anonymize(
10
+ kwargs_dict: Dict[str, Any],
11
+ fields_to_anonymize: Set[str],
12
+ object_type: Literal["span", "trace"],
13
+ ) -> Dict[str, Any]:
14
+ """
15
+ Encodes and anonymizes the data in the given dictionary based on the specified
16
+ fields using registered anonymizers. If no anonymizers are registered, the
17
+ function simply encodes the dictionary without anonymization.
18
+
19
+ Args:
20
+ kwargs_dict: The dictionary containing the data to encode
21
+ and anonymize.
22
+ fields_to_anonymize: The set of fields within the dictionary to
23
+ anonymize.
24
+ object_type: A string indicating the type of object ('span' or 'trace')
25
+ that was used to create the kwargs_dict. This is passed to anonymizers
26
+ to provide context about the source object.
27
+
28
+ Returns:
29
+ A dictionary that has been encoded and, if applicable, anonymized.
30
+ """
31
+ # check if any anonymizer was registered
32
+ encoded_obj = jsonable_encoder.encode(kwargs_dict)
33
+ if not opik.hooks.has_anonymizers():
34
+ return encoded_obj
35
+
36
+ anonymizers = opik.hooks.get_anonymizers()
37
+ return anonymize_encoded_obj(
38
+ obj=encoded_obj,
39
+ fields_to_anonymize=fields_to_anonymize,
40
+ anonymizers=anonymizers,
41
+ object_type=object_type,
42
+ )
43
+
44
+
45
+ def anonymize_encoded_obj(
46
+ obj: Dict[str, Any],
47
+ fields_to_anonymize: Set[str],
48
+ anonymizers: List[anonymizer.Anonymizer],
49
+ object_type: Literal["span", "trace"],
50
+ ) -> Dict[str, Any]:
51
+ """
52
+ Anonymizes specified fields in an encoded dictionary using the provided anonymizers.
53
+ This function iterates over the given set of field names and applies each anonymizer
54
+ to the corresponding field in the dictionary, if present. The anonymizers are expected
55
+ to implement an `anonymize` method that takes the field value, field name, and object type
56
+ as arguments. Only fields present in the dictionary and listed in `fields_to_anonymize`
57
+ are anonymized.
58
+
59
+ Args:
60
+ obj: The encoded dictionary whose fields are to be anonymized.
61
+ fields_to_anonymize: A set of field names within the dictionary to anonymize.
62
+ anonymizers: A list of anonymizer instances to apply to each field.
63
+ object_type: A string indicating the type of object ('span' or 'trace'),
64
+ providing context for anonymization.
65
+
66
+ Returns:
67
+ The dictionary with specified fields anonymized using the provided anonymizers.
68
+ """
69
+ if isinstance(obj, dict):
70
+ for field_name in fields_to_anonymize:
71
+ if field_name in obj:
72
+ for anonymizer_instance in anonymizers:
73
+ obj[field_name] = anonymizer_instance.anonymize(
74
+ obj[field_name],
75
+ field_name=field_name,
76
+ object_type=object_type,
77
+ )
78
+
79
+ return obj
@@ -1,7 +1,7 @@
1
1
  import dataclasses
2
2
  import datetime
3
3
  from dataclasses import field
4
- from typing import Optional, Any, Dict, List, Union, Literal
4
+ from typing import Optional, Any, Dict, List, Union, Literal, Set
5
5
 
6
6
  from . import arguments_utils
7
7
  from ..rest_api.types import span_write, trace_write
@@ -50,6 +50,10 @@ class CreateTraceMessage(BaseMessage):
50
50
  data["id"] = data.pop("trace_id")
51
51
  return data
52
52
 
53
+ @staticmethod
54
+ def fields_to_anonymize() -> Set[str]:
55
+ return {"input", "output", "metadata"}
56
+
53
57
 
54
58
  @dataclasses.dataclass
55
59
  class UpdateTraceMessage(BaseMessage):
@@ -78,6 +82,10 @@ class UpdateTraceMessage(BaseMessage):
78
82
  data["id"] = data.pop("trace_id")
79
83
  return data
80
84
 
85
+ @staticmethod
86
+ def fields_to_anonymize() -> Set[str]:
87
+ return {"input", "output", "metadata"}
88
+
81
89
 
82
90
  @dataclasses.dataclass
83
91
  class CreateSpanMessage(BaseMessage):
@@ -112,6 +120,10 @@ class CreateSpanMessage(BaseMessage):
112
120
  data["total_estimated_cost"] = data.pop("total_cost")
113
121
  return data
114
122
 
123
+ @staticmethod
124
+ def fields_to_anonymize() -> Set[str]:
125
+ return {"input", "output", "metadata"}
126
+
115
127
 
116
128
  @dataclasses.dataclass
117
129
  class UpdateSpanMessage(BaseMessage):
@@ -144,6 +156,10 @@ class UpdateSpanMessage(BaseMessage):
144
156
  data["total_estimated_cost"] = data.pop("total_cost")
145
157
  return data
146
158
 
159
+ @staticmethod
160
+ def fields_to_anonymize() -> Set[str]:
161
+ return {"input", "output", "metadata"}
162
+
147
163
 
148
164
  @dataclasses.dataclass
149
165
  class FeedbackScoreMessage(BaseMessage):
@@ -200,11 +216,19 @@ class AddThreadsFeedbackScoresBatchMessage(BaseMessage):
200
216
  class CreateSpansBatchMessage(BaseMessage):
201
217
  batch: List[span_write.SpanWrite]
202
218
 
219
+ @staticmethod
220
+ def fields_to_anonymize() -> Set[str]:
221
+ return {"input", "output", "metadata"}
222
+
203
223
 
204
224
  @dataclasses.dataclass
205
225
  class CreateTraceBatchMessage(BaseMessage):
206
226
  batch: List[trace_write.TraceWrite]
207
227
 
228
+ @staticmethod
229
+ def fields_to_anonymize() -> Set[str]:
230
+ return {"input", "output", "metadata"}
231
+
208
232
 
209
233
  @dataclasses.dataclass
210
234
  class GuardrailBatchItemMessage(BaseMessage):
@@ -4,12 +4,11 @@ from typing import Callable, Dict, Type, Any
4
4
  import pydantic
5
5
  import tenacity
6
6
 
7
- from . import messages, message_processors
7
+ from . import encoder_helpers, messages, message_processors
8
8
  from .. import (
9
9
  exceptions as exceptions,
10
10
  logging_messages as logging_messages,
11
11
  dict_utils,
12
- jsonable_encoder,
13
12
  )
14
13
  from ..rate_limit import rate_limit
15
14
  from ..rest_api import client as rest_api_client, core as rest_api_core
@@ -128,7 +127,12 @@ class OpikMessageProcessor(message_processors.BaseMessageProcessor):
128
127
  cleaned_create_span_kwargs = dict_utils.remove_none_from_dict(
129
128
  create_span_kwargs
130
129
  )
131
- cleaned_create_span_kwargs = jsonable_encoder.encode(cleaned_create_span_kwargs)
130
+ cleaned_create_span_kwargs = encoder_helpers.encode_and_anonymize(
131
+ cleaned_create_span_kwargs,
132
+ fields_to_anonymize=message.fields_to_anonymize(),
133
+ object_type="span",
134
+ )
135
+
132
136
  LOGGER.debug("Create span request: %s", cleaned_create_span_kwargs)
133
137
  self._rest_client.spans.create_span(**cleaned_create_span_kwargs)
134
138
 
@@ -140,9 +144,12 @@ class OpikMessageProcessor(message_processors.BaseMessageProcessor):
140
144
  cleaned_create_trace_kwargs = dict_utils.remove_none_from_dict(
141
145
  create_trace_kwargs
142
146
  )
143
- cleaned_create_trace_kwargs = jsonable_encoder.encode(
144
- cleaned_create_trace_kwargs
147
+ cleaned_create_trace_kwargs = encoder_helpers.encode_and_anonymize(
148
+ cleaned_create_trace_kwargs,
149
+ fields_to_anonymize=message.fields_to_anonymize(),
150
+ object_type="trace",
145
151
  )
152
+
146
153
  LOGGER.debug("Create trace request: %s", cleaned_create_trace_kwargs)
147
154
  self._rest_client.traces.create_trace(**cleaned_create_trace_kwargs)
148
155
 
@@ -155,7 +162,12 @@ class OpikMessageProcessor(message_processors.BaseMessageProcessor):
155
162
  cleaned_update_span_kwargs = dict_utils.remove_none_from_dict(
156
163
  update_span_kwargs
157
164
  )
158
- cleaned_update_span_kwargs = jsonable_encoder.encode(cleaned_update_span_kwargs)
165
+ cleaned_update_span_kwargs = encoder_helpers.encode_and_anonymize(
166
+ cleaned_update_span_kwargs,
167
+ fields_to_anonymize=message.fields_to_anonymize(),
168
+ object_type="span",
169
+ )
170
+
159
171
  LOGGER.debug("Update span request: %s", cleaned_update_span_kwargs)
160
172
  self._rest_client.spans.update_span(**cleaned_update_span_kwargs)
161
173
 
@@ -168,9 +180,12 @@ class OpikMessageProcessor(message_processors.BaseMessageProcessor):
168
180
  cleaned_update_trace_kwargs = dict_utils.remove_none_from_dict(
169
181
  update_trace_kwargs
170
182
  )
171
- cleaned_update_trace_kwargs = jsonable_encoder.encode(
172
- cleaned_update_trace_kwargs
183
+ cleaned_update_trace_kwargs = encoder_helpers.encode_and_anonymize(
184
+ cleaned_update_trace_kwargs,
185
+ fields_to_anonymize=message.fields_to_anonymize(),
186
+ object_type="trace",
173
187
  )
188
+
174
189
  LOGGER.debug("Update trace request: %s", cleaned_update_trace_kwargs)
175
190
  self._rest_client.traces.update_trace(**cleaned_update_trace_kwargs)
176
191
  LOGGER.debug("Sent trace %s", message.trace_id)
opik/opik_context.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
- from typing import Any, Dict, List, Optional, Union, Iterator
2
+ from typing import Any, Dict, List, Optional, Iterator, Union
3
3
 
4
- import opik.llm_usage as llm_usage
4
+ from opik import llm_usage
5
5
  from opik.api_objects import span, trace, opik_client, prompt
6
6
  from opik.api_objects.attachment import Attachment
7
7
  from opik.types import (
@@ -11,7 +11,7 @@ from opik.types import (
11
11
  ErrorInfoDict,
12
12
  )
13
13
 
14
- import opik.decorator.tracing_runtime_config as tracing_runtime_config
14
+ from opik import tracing_runtime_config
15
15
 
16
16
  from . import context_storage, exceptions
17
17
  from .decorator import error_info_collector
@@ -68,7 +68,7 @@ def update_current_span(
68
68
  total_cost: Optional[float] = None,
69
69
  attachments: Optional[List[Attachment]] = None,
70
70
  error_info: Optional[ErrorInfoDict] = None,
71
- prompts: Optional[List[prompt.Prompt]] = None,
71
+ prompts: Optional[List[prompt.BasePrompt]] = None,
72
72
  ) -> None:
73
73
  """
74
74
  Update the current span with the provided parameters. This method is usually called within a tracked function.
@@ -97,7 +97,7 @@ def update_current_span(
97
97
  return
98
98
 
99
99
  if prompts is not None:
100
- prompts = [prompt.prompt.to_info_dict(p) for p in prompts]
100
+ prompts = [p.__internal_api__to_info_dict__() for p in prompts]
101
101
 
102
102
  new_params = {
103
103
  "name": name,
@@ -130,7 +130,7 @@ def update_current_trace(
130
130
  feedback_scores: Optional[List[FeedbackScoreDict]] = None,
131
131
  thread_id: Optional[str] = None,
132
132
  attachments: Optional[List[Attachment]] = None,
133
- prompts: Optional[List[prompt.Prompt]] = None,
133
+ prompts: Optional[List[prompt.BasePrompt]] = None,
134
134
  ) -> None:
135
135
  """
136
136
  Update the current trace with the provided parameters. This method is usually called within a tracked function.
@@ -151,7 +151,7 @@ def update_current_trace(
151
151
  return
152
152
 
153
153
  if prompts is not None:
154
- prompts = [prompt.prompt.to_info_dict(p) for p in prompts]
154
+ prompts = [p.__internal_api__to_info_dict__() for p in prompts]
155
155
 
156
156
  new_params = {
157
157
  "name": name,