opik 1.8.39__py3-none-any.whl → 1.9.71__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 (592) hide show
  1. opik/__init__.py +19 -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/attachment/attachment_context.py +36 -0
  9. opik/api_objects/attachment/attachments_extractor.py +153 -0
  10. opik/api_objects/attachment/client.py +1 -0
  11. opik/api_objects/attachment/converters.py +2 -0
  12. opik/api_objects/attachment/decoder.py +18 -0
  13. opik/api_objects/attachment/decoder_base64.py +83 -0
  14. opik/api_objects/attachment/decoder_helpers.py +137 -0
  15. opik/api_objects/data_helpers.py +79 -0
  16. opik/api_objects/dataset/dataset.py +64 -4
  17. opik/api_objects/dataset/rest_operations.py +11 -2
  18. opik/api_objects/experiment/experiment.py +57 -57
  19. opik/api_objects/experiment/experiment_item.py +2 -1
  20. opik/api_objects/experiment/experiments_client.py +64 -0
  21. opik/api_objects/experiment/helpers.py +35 -11
  22. opik/api_objects/experiment/rest_operations.py +65 -5
  23. opik/api_objects/helpers.py +8 -5
  24. opik/api_objects/local_recording.py +81 -0
  25. opik/api_objects/opik_client.py +600 -108
  26. opik/api_objects/opik_query_language.py +39 -5
  27. opik/api_objects/prompt/__init__.py +12 -2
  28. opik/api_objects/prompt/base_prompt.py +69 -0
  29. opik/api_objects/prompt/base_prompt_template.py +29 -0
  30. opik/api_objects/prompt/chat/__init__.py +1 -0
  31. opik/api_objects/prompt/chat/chat_prompt.py +210 -0
  32. opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
  33. opik/api_objects/prompt/chat/content_renderer_registry.py +203 -0
  34. opik/api_objects/prompt/client.py +189 -47
  35. opik/api_objects/prompt/text/__init__.py +1 -0
  36. opik/api_objects/prompt/text/prompt.py +174 -0
  37. opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
  38. opik/api_objects/prompt/types.py +23 -0
  39. opik/api_objects/search_helpers.py +89 -0
  40. opik/api_objects/span/span_data.py +35 -25
  41. opik/api_objects/threads/threads_client.py +39 -5
  42. opik/api_objects/trace/trace_client.py +52 -2
  43. opik/api_objects/trace/trace_data.py +15 -24
  44. opik/api_objects/validation_helpers.py +3 -3
  45. opik/cli/__init__.py +5 -0
  46. opik/cli/__main__.py +6 -0
  47. opik/cli/configure.py +66 -0
  48. opik/cli/exports/__init__.py +131 -0
  49. opik/cli/exports/dataset.py +278 -0
  50. opik/cli/exports/experiment.py +784 -0
  51. opik/cli/exports/project.py +685 -0
  52. opik/cli/exports/prompt.py +578 -0
  53. opik/cli/exports/utils.py +406 -0
  54. opik/cli/harbor.py +39 -0
  55. opik/cli/healthcheck.py +21 -0
  56. opik/cli/imports/__init__.py +439 -0
  57. opik/cli/imports/dataset.py +143 -0
  58. opik/cli/imports/experiment.py +1192 -0
  59. opik/cli/imports/project.py +262 -0
  60. opik/cli/imports/prompt.py +177 -0
  61. opik/cli/imports/utils.py +280 -0
  62. opik/cli/main.py +49 -0
  63. opik/cli/proxy.py +93 -0
  64. opik/cli/usage_report/__init__.py +16 -0
  65. opik/cli/usage_report/charts.py +783 -0
  66. opik/cli/usage_report/cli.py +274 -0
  67. opik/cli/usage_report/constants.py +9 -0
  68. opik/cli/usage_report/extraction.py +749 -0
  69. opik/cli/usage_report/pdf.py +244 -0
  70. opik/cli/usage_report/statistics.py +78 -0
  71. opik/cli/usage_report/utils.py +235 -0
  72. opik/config.py +13 -7
  73. opik/configurator/configure.py +17 -0
  74. opik/datetime_helpers.py +12 -0
  75. opik/decorator/arguments_helpers.py +9 -1
  76. opik/decorator/base_track_decorator.py +205 -133
  77. opik/decorator/context_manager/span_context_manager.py +123 -0
  78. opik/decorator/context_manager/trace_context_manager.py +84 -0
  79. opik/decorator/opik_args/__init__.py +13 -0
  80. opik/decorator/opik_args/api_classes.py +71 -0
  81. opik/decorator/opik_args/helpers.py +120 -0
  82. opik/decorator/span_creation_handler.py +25 -6
  83. opik/dict_utils.py +3 -3
  84. opik/evaluation/__init__.py +13 -2
  85. opik/evaluation/engine/engine.py +272 -75
  86. opik/evaluation/engine/evaluation_tasks_executor.py +6 -3
  87. opik/evaluation/engine/helpers.py +31 -6
  88. opik/evaluation/engine/metrics_evaluator.py +237 -0
  89. opik/evaluation/evaluation_result.py +168 -2
  90. opik/evaluation/evaluator.py +533 -62
  91. opik/evaluation/metrics/__init__.py +103 -4
  92. opik/evaluation/metrics/aggregated_metric.py +35 -6
  93. opik/evaluation/metrics/base_metric.py +1 -1
  94. opik/evaluation/metrics/conversation/__init__.py +48 -0
  95. opik/evaluation/metrics/conversation/conversation_thread_metric.py +56 -2
  96. opik/evaluation/metrics/conversation/g_eval_wrappers.py +19 -0
  97. opik/evaluation/metrics/conversation/helpers.py +14 -15
  98. opik/evaluation/metrics/conversation/heuristics/__init__.py +14 -0
  99. opik/evaluation/metrics/conversation/heuristics/degeneration/__init__.py +3 -0
  100. opik/evaluation/metrics/conversation/heuristics/degeneration/metric.py +189 -0
  101. opik/evaluation/metrics/conversation/heuristics/degeneration/phrases.py +12 -0
  102. opik/evaluation/metrics/conversation/heuristics/knowledge_retention/__init__.py +3 -0
  103. opik/evaluation/metrics/conversation/heuristics/knowledge_retention/metric.py +172 -0
  104. opik/evaluation/metrics/conversation/llm_judges/__init__.py +32 -0
  105. opik/evaluation/metrics/conversation/{conversational_coherence → llm_judges/conversational_coherence}/metric.py +22 -17
  106. opik/evaluation/metrics/conversation/{conversational_coherence → llm_judges/conversational_coherence}/templates.py +1 -1
  107. opik/evaluation/metrics/conversation/llm_judges/g_eval_wrappers.py +442 -0
  108. opik/evaluation/metrics/conversation/{session_completeness → llm_judges/session_completeness}/metric.py +13 -7
  109. opik/evaluation/metrics/conversation/{session_completeness → llm_judges/session_completeness}/templates.py +1 -1
  110. opik/evaluation/metrics/conversation/llm_judges/user_frustration/__init__.py +0 -0
  111. opik/evaluation/metrics/conversation/{user_frustration → llm_judges/user_frustration}/metric.py +21 -14
  112. opik/evaluation/metrics/conversation/{user_frustration → llm_judges/user_frustration}/templates.py +1 -1
  113. opik/evaluation/metrics/conversation/types.py +4 -5
  114. opik/evaluation/metrics/conversation_types.py +9 -0
  115. opik/evaluation/metrics/heuristics/bertscore.py +107 -0
  116. opik/evaluation/metrics/heuristics/bleu.py +35 -15
  117. opik/evaluation/metrics/heuristics/chrf.py +127 -0
  118. opik/evaluation/metrics/heuristics/contains.py +47 -11
  119. opik/evaluation/metrics/heuristics/distribution_metrics.py +331 -0
  120. opik/evaluation/metrics/heuristics/gleu.py +113 -0
  121. opik/evaluation/metrics/heuristics/language_adherence.py +123 -0
  122. opik/evaluation/metrics/heuristics/meteor.py +119 -0
  123. opik/evaluation/metrics/heuristics/prompt_injection.py +150 -0
  124. opik/evaluation/metrics/heuristics/readability.py +129 -0
  125. opik/evaluation/metrics/heuristics/rouge.py +26 -9
  126. opik/evaluation/metrics/heuristics/spearman.py +88 -0
  127. opik/evaluation/metrics/heuristics/tone.py +155 -0
  128. opik/evaluation/metrics/heuristics/vader_sentiment.py +77 -0
  129. opik/evaluation/metrics/llm_judges/answer_relevance/metric.py +20 -5
  130. opik/evaluation/metrics/llm_judges/context_precision/metric.py +20 -6
  131. opik/evaluation/metrics/llm_judges/context_recall/metric.py +20 -6
  132. opik/evaluation/metrics/llm_judges/g_eval/__init__.py +5 -0
  133. opik/evaluation/metrics/llm_judges/g_eval/metric.py +219 -68
  134. opik/evaluation/metrics/llm_judges/g_eval/parser.py +102 -52
  135. opik/evaluation/metrics/llm_judges/g_eval/presets.py +209 -0
  136. opik/evaluation/metrics/llm_judges/g_eval_presets/__init__.py +36 -0
  137. opik/evaluation/metrics/llm_judges/g_eval_presets/agent_assessment.py +77 -0
  138. opik/evaluation/metrics/llm_judges/g_eval_presets/bias_classifier.py +181 -0
  139. opik/evaluation/metrics/llm_judges/g_eval_presets/compliance_risk.py +41 -0
  140. opik/evaluation/metrics/llm_judges/g_eval_presets/prompt_uncertainty.py +41 -0
  141. opik/evaluation/metrics/llm_judges/g_eval_presets/qa_suite.py +146 -0
  142. opik/evaluation/metrics/llm_judges/hallucination/metric.py +16 -3
  143. opik/evaluation/metrics/llm_judges/llm_juries/__init__.py +3 -0
  144. opik/evaluation/metrics/llm_judges/llm_juries/metric.py +76 -0
  145. opik/evaluation/metrics/llm_judges/moderation/metric.py +16 -4
  146. opik/evaluation/metrics/llm_judges/structure_output_compliance/__init__.py +0 -0
  147. opik/evaluation/metrics/llm_judges/structure_output_compliance/metric.py +144 -0
  148. opik/evaluation/metrics/llm_judges/structure_output_compliance/parser.py +79 -0
  149. opik/evaluation/metrics/llm_judges/structure_output_compliance/schema.py +15 -0
  150. opik/evaluation/metrics/llm_judges/structure_output_compliance/template.py +50 -0
  151. opik/evaluation/metrics/llm_judges/syc_eval/__init__.py +0 -0
  152. opik/evaluation/metrics/llm_judges/syc_eval/metric.py +252 -0
  153. opik/evaluation/metrics/llm_judges/syc_eval/parser.py +82 -0
  154. opik/evaluation/metrics/llm_judges/syc_eval/template.py +155 -0
  155. opik/evaluation/metrics/llm_judges/trajectory_accuracy/metric.py +20 -5
  156. opik/evaluation/metrics/llm_judges/usefulness/metric.py +16 -4
  157. opik/evaluation/metrics/ragas_metric.py +43 -23
  158. opik/evaluation/models/__init__.py +8 -0
  159. opik/evaluation/models/base_model.py +107 -1
  160. opik/evaluation/models/langchain/langchain_chat_model.py +15 -7
  161. opik/evaluation/models/langchain/message_converters.py +97 -15
  162. opik/evaluation/models/litellm/litellm_chat_model.py +156 -29
  163. opik/evaluation/models/litellm/util.py +125 -0
  164. opik/evaluation/models/litellm/warning_filters.py +16 -4
  165. opik/evaluation/models/model_capabilities.py +187 -0
  166. opik/evaluation/models/models_factory.py +25 -3
  167. opik/evaluation/preprocessing.py +92 -0
  168. opik/evaluation/report.py +70 -12
  169. opik/evaluation/rest_operations.py +49 -45
  170. opik/evaluation/samplers/__init__.py +4 -0
  171. opik/evaluation/samplers/base_dataset_sampler.py +40 -0
  172. opik/evaluation/samplers/random_dataset_sampler.py +48 -0
  173. opik/evaluation/score_statistics.py +66 -0
  174. opik/evaluation/scorers/__init__.py +4 -0
  175. opik/evaluation/scorers/scorer_function.py +55 -0
  176. opik/evaluation/scorers/scorer_wrapper_metric.py +130 -0
  177. opik/evaluation/test_case.py +3 -2
  178. opik/evaluation/test_result.py +1 -0
  179. opik/evaluation/threads/evaluator.py +31 -3
  180. opik/evaluation/threads/helpers.py +3 -2
  181. opik/evaluation/types.py +9 -1
  182. opik/exceptions.py +33 -0
  183. opik/file_upload/file_uploader.py +13 -0
  184. opik/file_upload/upload_options.py +2 -0
  185. opik/hooks/__init__.py +23 -0
  186. opik/hooks/anonymizer_hook.py +36 -0
  187. opik/hooks/httpx_client_hook.py +112 -0
  188. opik/httpx_client.py +12 -9
  189. opik/id_helpers.py +18 -0
  190. opik/integrations/adk/graph/subgraph_edges_builders.py +1 -2
  191. opik/integrations/adk/helpers.py +16 -7
  192. opik/integrations/adk/legacy_opik_tracer.py +7 -4
  193. opik/integrations/adk/opik_tracer.py +14 -1
  194. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
  195. opik/integrations/adk/recursive_callback_injector.py +4 -7
  196. opik/integrations/bedrock/converse/__init__.py +0 -0
  197. opik/integrations/bedrock/converse/chunks_aggregator.py +188 -0
  198. opik/integrations/bedrock/{converse_decorator.py → converse/converse_decorator.py} +4 -3
  199. opik/integrations/bedrock/invoke_agent_decorator.py +5 -4
  200. opik/integrations/bedrock/invoke_model/__init__.py +0 -0
  201. opik/integrations/bedrock/invoke_model/chunks_aggregator/__init__.py +78 -0
  202. opik/integrations/bedrock/invoke_model/chunks_aggregator/api.py +45 -0
  203. opik/integrations/bedrock/invoke_model/chunks_aggregator/base.py +23 -0
  204. opik/integrations/bedrock/invoke_model/chunks_aggregator/claude.py +121 -0
  205. opik/integrations/bedrock/invoke_model/chunks_aggregator/format_detector.py +107 -0
  206. opik/integrations/bedrock/invoke_model/chunks_aggregator/llama.py +108 -0
  207. opik/integrations/bedrock/invoke_model/chunks_aggregator/mistral.py +118 -0
  208. opik/integrations/bedrock/invoke_model/chunks_aggregator/nova.py +99 -0
  209. opik/integrations/bedrock/invoke_model/invoke_model_decorator.py +178 -0
  210. opik/integrations/bedrock/invoke_model/response_types.py +34 -0
  211. opik/integrations/bedrock/invoke_model/stream_wrappers.py +122 -0
  212. opik/integrations/bedrock/invoke_model/usage_converters.py +87 -0
  213. opik/integrations/bedrock/invoke_model/usage_extraction.py +108 -0
  214. opik/integrations/bedrock/opik_tracker.py +42 -4
  215. opik/integrations/bedrock/types.py +19 -0
  216. opik/integrations/crewai/crewai_decorator.py +8 -51
  217. opik/integrations/crewai/opik_tracker.py +31 -10
  218. opik/integrations/crewai/patchers/__init__.py +5 -0
  219. opik/integrations/crewai/patchers/flow.py +118 -0
  220. opik/integrations/crewai/patchers/litellm_completion.py +30 -0
  221. opik/integrations/crewai/patchers/llm_client.py +207 -0
  222. opik/integrations/dspy/callback.py +80 -17
  223. opik/integrations/dspy/parsers.py +168 -0
  224. opik/integrations/harbor/__init__.py +17 -0
  225. opik/integrations/harbor/experiment_service.py +269 -0
  226. opik/integrations/harbor/opik_tracker.py +528 -0
  227. opik/integrations/haystack/opik_connector.py +2 -2
  228. opik/integrations/haystack/opik_tracer.py +3 -7
  229. opik/integrations/langchain/__init__.py +3 -1
  230. opik/integrations/langchain/helpers.py +96 -0
  231. opik/integrations/langchain/langgraph_async_context_bridge.py +131 -0
  232. opik/integrations/langchain/langgraph_tracer_injector.py +88 -0
  233. opik/integrations/langchain/opik_encoder_extension.py +1 -1
  234. opik/integrations/langchain/opik_tracer.py +474 -229
  235. opik/integrations/litellm/__init__.py +5 -0
  236. opik/integrations/litellm/completion_chunks_aggregator.py +115 -0
  237. opik/integrations/litellm/litellm_completion_decorator.py +242 -0
  238. opik/integrations/litellm/opik_tracker.py +43 -0
  239. opik/integrations/litellm/stream_patchers.py +151 -0
  240. opik/integrations/llama_index/callback.py +146 -107
  241. opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
  242. opik/integrations/openai/openai_chat_completions_decorator.py +2 -16
  243. opik/integrations/openai/opik_tracker.py +1 -1
  244. opik/integrations/sagemaker/auth.py +5 -1
  245. opik/llm_usage/google_usage.py +3 -1
  246. opik/llm_usage/opik_usage.py +7 -8
  247. opik/llm_usage/opik_usage_factory.py +4 -2
  248. opik/logging_messages.py +6 -0
  249. opik/message_processing/batching/base_batcher.py +14 -21
  250. opik/message_processing/batching/batch_manager.py +22 -10
  251. opik/message_processing/batching/batch_manager_constuctors.py +10 -0
  252. opik/message_processing/batching/batchers.py +59 -27
  253. opik/message_processing/batching/flushing_thread.py +0 -3
  254. opik/message_processing/emulation/__init__.py +0 -0
  255. opik/message_processing/emulation/emulator_message_processor.py +578 -0
  256. opik/message_processing/emulation/local_emulator_message_processor.py +140 -0
  257. opik/message_processing/emulation/models.py +162 -0
  258. opik/message_processing/encoder_helpers.py +79 -0
  259. opik/message_processing/messages.py +56 -1
  260. opik/message_processing/preprocessing/__init__.py +0 -0
  261. opik/message_processing/preprocessing/attachments_preprocessor.py +70 -0
  262. opik/message_processing/preprocessing/batching_preprocessor.py +53 -0
  263. opik/message_processing/preprocessing/constants.py +1 -0
  264. opik/message_processing/preprocessing/file_upload_preprocessor.py +38 -0
  265. opik/message_processing/preprocessing/preprocessor.py +36 -0
  266. opik/message_processing/processors/__init__.py +0 -0
  267. opik/message_processing/processors/attachments_extraction_processor.py +146 -0
  268. opik/message_processing/processors/message_processors.py +92 -0
  269. opik/message_processing/processors/message_processors_chain.py +96 -0
  270. opik/message_processing/{message_processors.py → processors/online_message_processor.py} +85 -29
  271. opik/message_processing/queue_consumer.py +9 -3
  272. opik/message_processing/streamer.py +71 -33
  273. opik/message_processing/streamer_constructors.py +43 -10
  274. opik/opik_context.py +16 -4
  275. opik/plugins/pytest/hooks.py +5 -3
  276. opik/rest_api/__init__.py +346 -15
  277. opik/rest_api/alerts/__init__.py +7 -0
  278. opik/rest_api/alerts/client.py +667 -0
  279. opik/rest_api/alerts/raw_client.py +1015 -0
  280. opik/rest_api/alerts/types/__init__.py +7 -0
  281. opik/rest_api/alerts/types/get_webhook_examples_request_alert_type.py +5 -0
  282. opik/rest_api/annotation_queues/__init__.py +4 -0
  283. opik/rest_api/annotation_queues/client.py +668 -0
  284. opik/rest_api/annotation_queues/raw_client.py +1019 -0
  285. opik/rest_api/automation_rule_evaluators/client.py +34 -2
  286. opik/rest_api/automation_rule_evaluators/raw_client.py +24 -0
  287. opik/rest_api/client.py +15 -0
  288. opik/rest_api/dashboards/__init__.py +4 -0
  289. opik/rest_api/dashboards/client.py +462 -0
  290. opik/rest_api/dashboards/raw_client.py +648 -0
  291. opik/rest_api/datasets/client.py +1310 -44
  292. opik/rest_api/datasets/raw_client.py +2269 -358
  293. opik/rest_api/experiments/__init__.py +2 -2
  294. opik/rest_api/experiments/client.py +191 -5
  295. opik/rest_api/experiments/raw_client.py +301 -7
  296. opik/rest_api/experiments/types/__init__.py +4 -1
  297. opik/rest_api/experiments/types/experiment_update_status.py +5 -0
  298. opik/rest_api/experiments/types/experiment_update_type.py +5 -0
  299. opik/rest_api/experiments/types/experiment_write_status.py +5 -0
  300. opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
  301. opik/rest_api/llm_provider_key/client.py +20 -0
  302. opik/rest_api/llm_provider_key/raw_client.py +20 -0
  303. opik/rest_api/llm_provider_key/types/provider_api_key_write_provider.py +1 -1
  304. opik/rest_api/manual_evaluation/__init__.py +4 -0
  305. opik/rest_api/manual_evaluation/client.py +347 -0
  306. opik/rest_api/manual_evaluation/raw_client.py +543 -0
  307. opik/rest_api/optimizations/client.py +145 -9
  308. opik/rest_api/optimizations/raw_client.py +237 -13
  309. opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
  310. opik/rest_api/prompts/__init__.py +2 -2
  311. opik/rest_api/prompts/client.py +227 -6
  312. opik/rest_api/prompts/raw_client.py +331 -2
  313. opik/rest_api/prompts/types/__init__.py +3 -1
  314. opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
  315. opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
  316. opik/rest_api/spans/__init__.py +0 -2
  317. opik/rest_api/spans/client.py +238 -76
  318. opik/rest_api/spans/raw_client.py +307 -95
  319. opik/rest_api/spans/types/__init__.py +0 -2
  320. opik/rest_api/traces/client.py +572 -161
  321. opik/rest_api/traces/raw_client.py +736 -229
  322. opik/rest_api/types/__init__.py +352 -17
  323. opik/rest_api/types/aggregation_data.py +1 -0
  324. opik/rest_api/types/alert.py +33 -0
  325. opik/rest_api/types/alert_alert_type.py +5 -0
  326. opik/rest_api/types/alert_page_public.py +24 -0
  327. opik/rest_api/types/alert_public.py +33 -0
  328. opik/rest_api/types/alert_public_alert_type.py +5 -0
  329. opik/rest_api/types/alert_trigger.py +27 -0
  330. opik/rest_api/types/alert_trigger_config.py +28 -0
  331. opik/rest_api/types/alert_trigger_config_public.py +28 -0
  332. opik/rest_api/types/alert_trigger_config_public_type.py +10 -0
  333. opik/rest_api/types/alert_trigger_config_type.py +10 -0
  334. opik/rest_api/types/alert_trigger_config_write.py +22 -0
  335. opik/rest_api/types/alert_trigger_config_write_type.py +10 -0
  336. opik/rest_api/types/alert_trigger_event_type.py +19 -0
  337. opik/rest_api/types/alert_trigger_public.py +27 -0
  338. opik/rest_api/types/alert_trigger_public_event_type.py +19 -0
  339. opik/rest_api/types/alert_trigger_write.py +23 -0
  340. opik/rest_api/types/alert_trigger_write_event_type.py +19 -0
  341. opik/rest_api/types/alert_write.py +28 -0
  342. opik/rest_api/types/alert_write_alert_type.py +5 -0
  343. opik/rest_api/types/annotation_queue.py +42 -0
  344. opik/rest_api/types/annotation_queue_batch.py +27 -0
  345. opik/rest_api/types/annotation_queue_item_ids.py +19 -0
  346. opik/rest_api/types/annotation_queue_page_public.py +28 -0
  347. opik/rest_api/types/annotation_queue_public.py +38 -0
  348. opik/rest_api/types/annotation_queue_public_scope.py +5 -0
  349. opik/rest_api/types/annotation_queue_reviewer.py +20 -0
  350. opik/rest_api/types/annotation_queue_reviewer_public.py +20 -0
  351. opik/rest_api/types/annotation_queue_scope.py +5 -0
  352. opik/rest_api/types/annotation_queue_write.py +31 -0
  353. opik/rest_api/types/annotation_queue_write_scope.py +5 -0
  354. opik/rest_api/types/audio_url.py +19 -0
  355. opik/rest_api/types/audio_url_public.py +19 -0
  356. opik/rest_api/types/audio_url_write.py +19 -0
  357. opik/rest_api/types/automation_rule_evaluator.py +62 -2
  358. opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
  359. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
  360. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
  361. opik/rest_api/types/automation_rule_evaluator_object_object_public.py +155 -0
  362. opik/rest_api/types/automation_rule_evaluator_page_public.py +3 -2
  363. opik/rest_api/types/automation_rule_evaluator_public.py +57 -2
  364. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
  365. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
  366. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
  367. opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python.py +22 -0
  368. opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_public.py +22 -0
  369. opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_write.py +22 -0
  370. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
  371. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
  372. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
  373. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
  374. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
  375. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
  376. opik/rest_api/types/automation_rule_evaluator_update.py +51 -1
  377. opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
  378. opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
  379. opik/rest_api/types/automation_rule_evaluator_update_span_user_defined_metric_python.py +22 -0
  380. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
  381. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
  382. opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
  383. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
  384. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
  385. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
  386. opik/rest_api/types/automation_rule_evaluator_write.py +51 -1
  387. opik/rest_api/types/boolean_feedback_definition.py +25 -0
  388. opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
  389. opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
  390. opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
  391. opik/rest_api/types/boolean_feedback_detail.py +29 -0
  392. opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
  393. opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
  394. opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
  395. opik/rest_api/types/dashboard_page_public.py +24 -0
  396. opik/rest_api/types/dashboard_public.py +30 -0
  397. opik/rest_api/types/dataset.py +4 -0
  398. opik/rest_api/types/dataset_expansion.py +42 -0
  399. opik/rest_api/types/dataset_expansion_response.py +39 -0
  400. opik/rest_api/types/dataset_item.py +2 -0
  401. opik/rest_api/types/dataset_item_changes_public.py +5 -0
  402. opik/rest_api/types/dataset_item_compare.py +2 -0
  403. opik/rest_api/types/dataset_item_filter.py +27 -0
  404. opik/rest_api/types/dataset_item_filter_operator.py +21 -0
  405. opik/rest_api/types/dataset_item_page_compare.py +5 -0
  406. opik/rest_api/types/dataset_item_page_public.py +5 -0
  407. opik/rest_api/types/dataset_item_public.py +2 -0
  408. opik/rest_api/types/dataset_item_update.py +39 -0
  409. opik/rest_api/types/dataset_item_write.py +1 -0
  410. opik/rest_api/types/dataset_public.py +4 -0
  411. opik/rest_api/types/dataset_public_status.py +5 -0
  412. opik/rest_api/types/dataset_status.py +5 -0
  413. opik/rest_api/types/dataset_version_diff.py +22 -0
  414. opik/rest_api/types/dataset_version_diff_stats.py +24 -0
  415. opik/rest_api/types/dataset_version_page_public.py +23 -0
  416. opik/rest_api/types/dataset_version_public.py +59 -0
  417. opik/rest_api/types/dataset_version_summary.py +46 -0
  418. opik/rest_api/types/dataset_version_summary_public.py +46 -0
  419. opik/rest_api/types/experiment.py +7 -2
  420. opik/rest_api/types/experiment_group_response.py +2 -0
  421. opik/rest_api/types/experiment_public.py +7 -2
  422. opik/rest_api/types/experiment_public_status.py +5 -0
  423. opik/rest_api/types/experiment_score.py +20 -0
  424. opik/rest_api/types/experiment_score_public.py +20 -0
  425. opik/rest_api/types/experiment_score_write.py +20 -0
  426. opik/rest_api/types/experiment_status.py +5 -0
  427. opik/rest_api/types/feedback.py +25 -1
  428. opik/rest_api/types/feedback_create.py +20 -1
  429. opik/rest_api/types/feedback_object_public.py +27 -1
  430. opik/rest_api/types/feedback_public.py +25 -1
  431. opik/rest_api/types/feedback_score_batch_item.py +2 -1
  432. opik/rest_api/types/feedback_score_batch_item_thread.py +2 -1
  433. opik/rest_api/types/feedback_score_public.py +4 -0
  434. opik/rest_api/types/feedback_update.py +20 -1
  435. opik/rest_api/types/group_content_with_aggregations.py +1 -0
  436. opik/rest_api/types/group_detail.py +19 -0
  437. opik/rest_api/types/group_details.py +20 -0
  438. opik/rest_api/types/guardrail.py +1 -0
  439. opik/rest_api/types/guardrail_write.py +1 -0
  440. opik/rest_api/types/ids_holder.py +19 -0
  441. opik/rest_api/types/image_url.py +20 -0
  442. opik/rest_api/types/image_url_public.py +20 -0
  443. opik/rest_api/types/image_url_write.py +20 -0
  444. opik/rest_api/types/llm_as_judge_message.py +5 -1
  445. opik/rest_api/types/llm_as_judge_message_content.py +26 -0
  446. opik/rest_api/types/llm_as_judge_message_content_public.py +26 -0
  447. opik/rest_api/types/llm_as_judge_message_content_write.py +26 -0
  448. opik/rest_api/types/llm_as_judge_message_public.py +5 -1
  449. opik/rest_api/types/llm_as_judge_message_write.py +5 -1
  450. opik/rest_api/types/llm_as_judge_model_parameters.py +3 -0
  451. opik/rest_api/types/llm_as_judge_model_parameters_public.py +3 -0
  452. opik/rest_api/types/llm_as_judge_model_parameters_write.py +3 -0
  453. opik/rest_api/types/manual_evaluation_request.py +38 -0
  454. opik/rest_api/types/manual_evaluation_request_entity_type.py +5 -0
  455. opik/rest_api/types/manual_evaluation_response.py +27 -0
  456. opik/rest_api/types/optimization.py +4 -2
  457. opik/rest_api/types/optimization_public.py +4 -2
  458. opik/rest_api/types/optimization_public_status.py +3 -1
  459. opik/rest_api/types/optimization_status.py +3 -1
  460. opik/rest_api/types/optimization_studio_config.py +27 -0
  461. opik/rest_api/types/optimization_studio_config_public.py +27 -0
  462. opik/rest_api/types/optimization_studio_config_write.py +27 -0
  463. opik/rest_api/types/optimization_studio_log.py +22 -0
  464. opik/rest_api/types/optimization_write.py +4 -2
  465. opik/rest_api/types/optimization_write_status.py +3 -1
  466. opik/rest_api/types/project.py +1 -0
  467. opik/rest_api/types/project_detailed.py +1 -0
  468. opik/rest_api/types/project_reference.py +31 -0
  469. opik/rest_api/types/project_reference_public.py +31 -0
  470. opik/rest_api/types/project_stats_summary_item.py +1 -0
  471. opik/rest_api/types/prompt.py +6 -0
  472. opik/rest_api/types/prompt_detail.py +6 -0
  473. opik/rest_api/types/prompt_detail_template_structure.py +5 -0
  474. opik/rest_api/types/prompt_public.py +6 -0
  475. opik/rest_api/types/prompt_public_template_structure.py +5 -0
  476. opik/rest_api/types/prompt_template_structure.py +5 -0
  477. opik/rest_api/types/prompt_version.py +3 -0
  478. opik/rest_api/types/prompt_version_detail.py +3 -0
  479. opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
  480. opik/rest_api/types/prompt_version_link.py +1 -0
  481. opik/rest_api/types/prompt_version_link_public.py +1 -0
  482. opik/rest_api/types/prompt_version_page_public.py +5 -0
  483. opik/rest_api/types/prompt_version_public.py +3 -0
  484. opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
  485. opik/rest_api/types/prompt_version_template_structure.py +5 -0
  486. opik/rest_api/types/prompt_version_update.py +33 -0
  487. opik/rest_api/types/provider_api_key.py +9 -0
  488. opik/rest_api/types/provider_api_key_provider.py +1 -1
  489. opik/rest_api/types/provider_api_key_public.py +9 -0
  490. opik/rest_api/types/provider_api_key_public_provider.py +1 -1
  491. opik/rest_api/types/score_name.py +1 -0
  492. opik/rest_api/types/service_toggles_config.py +18 -0
  493. opik/rest_api/types/span.py +1 -2
  494. opik/rest_api/types/span_enrichment_options.py +31 -0
  495. opik/rest_api/types/span_experiment_item_bulk_write_view.py +1 -2
  496. opik/rest_api/types/span_filter.py +23 -0
  497. opik/rest_api/types/span_filter_operator.py +21 -0
  498. opik/rest_api/types/span_filter_write.py +23 -0
  499. opik/rest_api/types/span_filter_write_operator.py +21 -0
  500. opik/rest_api/types/span_llm_as_judge_code.py +27 -0
  501. opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
  502. opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
  503. opik/rest_api/types/span_public.py +1 -2
  504. opik/rest_api/types/span_update.py +46 -0
  505. opik/rest_api/types/span_user_defined_metric_python_code.py +20 -0
  506. opik/rest_api/types/span_user_defined_metric_python_code_public.py +20 -0
  507. opik/rest_api/types/span_user_defined_metric_python_code_write.py +20 -0
  508. opik/rest_api/types/span_write.py +1 -2
  509. opik/rest_api/types/studio_evaluation.py +20 -0
  510. opik/rest_api/types/studio_evaluation_public.py +20 -0
  511. opik/rest_api/types/studio_evaluation_write.py +20 -0
  512. opik/rest_api/types/studio_llm_model.py +21 -0
  513. opik/rest_api/types/studio_llm_model_public.py +21 -0
  514. opik/rest_api/types/studio_llm_model_write.py +21 -0
  515. opik/rest_api/types/studio_message.py +20 -0
  516. opik/rest_api/types/studio_message_public.py +20 -0
  517. opik/rest_api/types/studio_message_write.py +20 -0
  518. opik/rest_api/types/studio_metric.py +21 -0
  519. opik/rest_api/types/studio_metric_public.py +21 -0
  520. opik/rest_api/types/studio_metric_write.py +21 -0
  521. opik/rest_api/types/studio_optimizer.py +21 -0
  522. opik/rest_api/types/studio_optimizer_public.py +21 -0
  523. opik/rest_api/types/studio_optimizer_write.py +21 -0
  524. opik/rest_api/types/studio_prompt.py +20 -0
  525. opik/rest_api/types/studio_prompt_public.py +20 -0
  526. opik/rest_api/types/studio_prompt_write.py +20 -0
  527. opik/rest_api/types/trace.py +11 -2
  528. opik/rest_api/types/trace_enrichment_options.py +32 -0
  529. opik/rest_api/types/trace_experiment_item_bulk_write_view.py +1 -2
  530. opik/rest_api/types/trace_filter.py +23 -0
  531. opik/rest_api/types/trace_filter_operator.py +21 -0
  532. opik/rest_api/types/trace_filter_write.py +23 -0
  533. opik/rest_api/types/trace_filter_write_operator.py +21 -0
  534. opik/rest_api/types/trace_public.py +11 -2
  535. opik/rest_api/types/trace_thread_filter_write.py +23 -0
  536. opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
  537. opik/rest_api/types/trace_thread_identifier.py +1 -0
  538. opik/rest_api/types/trace_update.py +39 -0
  539. opik/rest_api/types/trace_write.py +1 -2
  540. opik/rest_api/types/value_entry.py +2 -0
  541. opik/rest_api/types/value_entry_compare.py +2 -0
  542. opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
  543. opik/rest_api/types/value_entry_public.py +2 -0
  544. opik/rest_api/types/video_url.py +19 -0
  545. opik/rest_api/types/video_url_public.py +19 -0
  546. opik/rest_api/types/video_url_write.py +19 -0
  547. opik/rest_api/types/webhook.py +28 -0
  548. opik/rest_api/types/webhook_examples.py +19 -0
  549. opik/rest_api/types/webhook_public.py +28 -0
  550. opik/rest_api/types/webhook_test_result.py +23 -0
  551. opik/rest_api/types/webhook_test_result_status.py +5 -0
  552. opik/rest_api/types/webhook_write.py +23 -0
  553. opik/rest_api/types/welcome_wizard_tracking.py +22 -0
  554. opik/rest_api/types/workspace_configuration.py +5 -0
  555. opik/rest_api/welcome_wizard/__init__.py +4 -0
  556. opik/rest_api/welcome_wizard/client.py +195 -0
  557. opik/rest_api/welcome_wizard/raw_client.py +208 -0
  558. opik/rest_api/workspaces/client.py +14 -2
  559. opik/rest_api/workspaces/raw_client.py +10 -0
  560. opik/s3_httpx_client.py +14 -1
  561. opik/simulation/__init__.py +6 -0
  562. opik/simulation/simulated_user.py +99 -0
  563. opik/simulation/simulator.py +108 -0
  564. opik/synchronization.py +5 -6
  565. opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
  566. opik/types.py +36 -0
  567. opik/validation/chat_prompt_messages.py +241 -0
  568. opik/validation/feedback_score.py +3 -3
  569. opik/validation/validator.py +28 -0
  570. opik-1.9.71.dist-info/METADATA +370 -0
  571. opik-1.9.71.dist-info/RECORD +1110 -0
  572. opik/api_objects/prompt/prompt.py +0 -112
  573. opik/cli.py +0 -193
  574. opik/hooks.py +0 -13
  575. opik/integrations/bedrock/chunks_aggregator.py +0 -55
  576. opik/integrations/bedrock/helpers.py +0 -8
  577. opik/rest_api/types/automation_rule_evaluator_object_public.py +0 -100
  578. opik/rest_api/types/json_node_experiment_item_bulk_write_view.py +0 -5
  579. opik-1.8.39.dist-info/METADATA +0 -339
  580. opik-1.8.39.dist-info/RECORD +0 -790
  581. /opik/{evaluation/metrics/conversation/conversational_coherence → decorator/context_manager}/__init__.py +0 -0
  582. /opik/evaluation/metrics/conversation/{session_completeness → llm_judges/conversational_coherence}/__init__.py +0 -0
  583. /opik/evaluation/metrics/conversation/{conversational_coherence → llm_judges/conversational_coherence}/schema.py +0 -0
  584. /opik/evaluation/metrics/conversation/{user_frustration → llm_judges/session_completeness}/__init__.py +0 -0
  585. /opik/evaluation/metrics/conversation/{session_completeness → llm_judges/session_completeness}/schema.py +0 -0
  586. /opik/evaluation/metrics/conversation/{user_frustration → llm_judges/user_frustration}/schema.py +0 -0
  587. /opik/integrations/bedrock/{stream_wrappers.py → converse/stream_wrappers.py} +0 -0
  588. /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
  589. {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/WHEEL +0 -0
  590. {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/entry_points.txt +0 -0
  591. {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/licenses/LICENSE +0 -0
  592. {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,40 @@
1
1
  import logging
2
2
  import datetime
3
- from typing import Any, Dict, List, Literal, Optional, Set, TYPE_CHECKING, cast, Tuple
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ List,
7
+ Literal,
8
+ Optional,
9
+ Set,
10
+ TYPE_CHECKING,
11
+ cast,
12
+ Callable,
13
+ NamedTuple,
14
+ )
4
15
  import contextvars
16
+ from uuid import UUID
17
+
5
18
  from langchain_core import language_models
6
19
  from langchain_core.tracers import BaseTracer
7
20
  from langchain_core.tracers.schemas import Run
8
21
 
9
- import opik.dict_utils as dict_utils
10
- import opik.llm_usage as llm_usage
22
+ from opik import context_storage, dict_utils, llm_usage, tracing_runtime_config
11
23
  from opik.api_objects import span, trace
24
+ from opik.decorator import arguments_helpers, span_creation_handler
12
25
  from opik.types import DistributedTraceHeadersDict, ErrorInfoDict
13
26
  from opik.validation import parameters_validator
14
27
  from . import (
15
28
  base_llm_patcher,
29
+ helpers as langchain_helpers,
16
30
  opik_encoder_extension,
17
31
  provider_usage_extractors,
18
32
  )
19
33
 
20
34
  from ...api_objects import helpers, opik_client
21
- import opik.context_storage as context_storage
22
- import opik.decorator.tracing_runtime_config as tracing_runtime_config
23
35
 
24
36
  if TYPE_CHECKING:
25
- from uuid import UUID
26
-
27
37
  from langchain_core.runnables.graph import Graph
28
-
29
38
  from langchain_core.messages import BaseMessage
30
39
 
31
40
  LOGGER = logging.getLogger(__name__)
@@ -36,6 +45,20 @@ language_models.BaseLLM.dict = base_llm_patcher.base_llm_dict_patched()
36
45
 
37
46
  SpanType = Literal["llm", "tool", "general"]
38
47
 
48
+ # A callable that receives an error string and returns True if the error should be skipped,
49
+ # or False otherwise.
50
+ SkipErrorCallback = Callable[[str], bool]
51
+
52
+ # Placeholder output dictionary used when errors are intentionally skipped
53
+ # via the skip_error_callback. This signals that the output was not produced
54
+ # due to a handled/ignored error during execution.
55
+ ERROR_SKIPPED_OUTPUTS = {"warning": "Error output skipped by skip_error_callback."}
56
+
57
+
58
+ class TrackRootRunResult(NamedTuple):
59
+ new_trace_data: Optional[trace.TraceData]
60
+ new_span_data: Optional[span.SpanData]
61
+
39
62
 
40
63
  def _get_span_type(run: Dict[str, Any]) -> SpanType:
41
64
  if run.get("run_type") in ["llm", "tool"]:
@@ -47,15 +70,16 @@ def _get_span_type(run: Dict[str, Any]) -> SpanType:
47
70
  return cast(SpanType, "general")
48
71
 
49
72
 
50
- class OpikTracer(BaseTracer):
51
- """Langchain Opik Tracer.
73
+ def _is_root_run(run_dict: Dict[str, Any]) -> bool:
74
+ return run_dict.get("parent_run_id") is None
52
75
 
53
- Args:
54
- tags: List of tags to be applied to each trace logged by the tracer.
55
- metadata: Additional metadata for each trace logged by the tracer.
56
- graph: A LangGraph Graph object to track the Graph Definition in Opik.
57
- project_name: The name of the project to log data.
58
- """
76
+
77
+ def _get_run_metadata(run_dict: Dict[str, Any]) -> Dict[str, Any]:
78
+ return run_dict["extra"].get("metadata", {})
79
+
80
+
81
+ class OpikTracer(BaseTracer):
82
+ """Langchain Opik Tracer."""
59
83
 
60
84
  def __init__(
61
85
  self,
@@ -65,8 +89,35 @@ class OpikTracer(BaseTracer):
65
89
  project_name: Optional[str] = None,
66
90
  distributed_headers: Optional[DistributedTraceHeadersDict] = None,
67
91
  thread_id: Optional[str] = None,
92
+ skip_error_callback: Optional[SkipErrorCallback] = None,
93
+ opik_context_read_only_mode: bool = False,
68
94
  **kwargs: Any,
69
95
  ) -> None:
96
+ """
97
+ Initializes an instance of the class with various parameters for traces, metadata, and project configuration.
98
+
99
+ Args:
100
+ tags: List of tags associated with logged traces.
101
+ metadata: Dictionary containing metadata information for logged traces.
102
+ graph: A LangGraph Graph object for representing dependencies or flow
103
+ to track the Graph Definition in Opik.
104
+ project_name: Name of the project associated with the traces.
105
+ distributed_headers: Headers for distributed tracing context.
106
+ thread_id: Unique identifier for the conversational thread
107
+ to be associated with traces.
108
+ skip_error_callback : Callback function to handle skip errors logic.
109
+ Allows defining custom logic for handling errors that are intentionally skipped.
110
+ Please note that in traces/spans where errors are intentionally skipped,
111
+ the output will be replaced with `ERROR_SKIPPED_OUTPUTS`. You can provide
112
+ the output manually using `opik_context.get_current_span_data().update(output=...)`.
113
+ opik_context_read_only_mode: Whether to adding/popping spans/traces to/from the context storage.
114
+ * If False (default), OpikTracer will add created spans/traces to the opik context, so if there is a @track-decorated
115
+ function called inside the LangChain runnable, it will be attached to it's parent span from LangChain automatically.
116
+ * If True, OpikTracer will not modify the context storage and only create spans/traces from LangChain's Run objects.
117
+ This might be useful when the environment doesn't support proper context isolation for concurrent operations and you
118
+ want to avoid modifying the Opik context stack due to unsafety.
119
+ **kwargs: Additional arguments passed to the parent class constructor.
120
+ """
70
121
  validator = parameters_validator.create_validator(
71
122
  method_name="__init__", class_name=self.__class__.__name__
72
123
  )
@@ -82,23 +133,26 @@ class OpikTracer(BaseTracer):
82
133
  self._trace_default_metadata["created_from"] = "langchain"
83
134
 
84
135
  if graph:
85
- self._trace_default_metadata["_opik_graph_definition"] = {
86
- "format": "mermaid",
87
- "data": graph.draw_mermaid(),
88
- }
136
+ self.set_graph(graph)
89
137
 
90
138
  self._trace_default_tags = tags
91
139
 
92
- self._span_data_map: Dict["UUID", span.SpanData] = {}
140
+ self._span_data_map: Dict[UUID, span.SpanData] = {}
93
141
  """Map from run id to span data."""
94
142
 
95
- self._created_traces_data_map: Dict["UUID", trace.TraceData] = {}
143
+ self._created_traces_data_map: Dict[UUID, trace.TraceData] = {}
96
144
  """Map from run id to trace data."""
97
145
 
98
146
  self._created_traces: List[trace.Trace] = []
99
147
 
100
148
  self._externally_created_traces_ids: Set[str] = set()
101
149
 
150
+ self._skipped_langgraph_root_run_ids: Set[UUID] = set()
151
+ """Set of run IDs for LangGraph root runs where we skip creating the span."""
152
+
153
+ self._langgraph_parent_span_ids: Dict[UUID, Optional[str]] = {}
154
+ """Map from LangGraph root run ID to parent span ID (None if attached to trace)."""
155
+
102
156
  self._project_name = project_name
103
157
 
104
158
  self._distributed_headers = distributed_headers
@@ -113,54 +167,100 @@ class OpikTracer(BaseTracer):
113
167
  Optional[str]
114
168
  ] = contextvars.ContextVar("root_run_external_parent_span_id", default=None)
115
169
 
170
+ self._skip_error_callback = skip_error_callback
171
+
172
+ self._opik_context_read_only_mode = opik_context_read_only_mode
173
+
174
+ def set_graph(self, graph: "Graph") -> None:
175
+ """
176
+ Set the LangGraph graph structure for visualization in Opik traces.
177
+
178
+ This method extracts the graph structure and stores it in trace metadata,
179
+ allowing the graph to be visualized in the Opik UI.
180
+
181
+ Args:
182
+ graph: A LangGraph Graph object (typically obtained via graph.get_graph(xray=True)).
183
+ """
184
+ self._trace_default_metadata["_opik_graph_definition"] = {
185
+ "format": "mermaid",
186
+ "data": graph.draw_mermaid(),
187
+ }
188
+
116
189
  def _is_opik_span_created_by_this_tracer(self, span_id: str) -> bool:
117
- return any(span.id == span_id for span in self._span_data_map.values())
190
+ return any(span_.id == span_id for span_ in self._span_data_map.values())
118
191
 
119
192
  def _is_opik_trace_created_by_this_tracer(self, trace_id: str) -> bool:
120
193
  return any(
121
- trace.id == trace_id for trace in self._created_traces_data_map.values()
194
+ trace_.id == trace_id for trace_ in self._created_traces_data_map.values()
122
195
  )
123
196
 
124
- def _persist_run(self, run: "Run") -> None:
197
+ def _persist_run(self, run: Run) -> None:
125
198
  run_dict: Dict[str, Any] = run.dict()
126
199
 
127
200
  error_info: Optional[ErrorInfoDict]
128
- if run_dict["error"] is not None:
129
- output = None
130
- error_info = {
131
- "exception_type": "Exception",
132
- "traceback": run_dict["error"],
133
- }
134
- else:
135
- output = run_dict["outputs"]
136
- error_info = None
201
+ trace_additional_metadata: Dict[str, Any] = {}
202
+
203
+ error_str = run_dict.get("error")
204
+ outputs = None
205
+ error_info = None
206
+
207
+ if error_str is not None:
208
+ if not self._should_skip_error(error_str):
209
+ error_info = ErrorInfoDict(
210
+ exception_type="Exception",
211
+ traceback=error_str,
212
+ )
213
+ else:
214
+ outputs = ERROR_SKIPPED_OUTPUTS
215
+ elif (outputs := run_dict.get("outputs")) is not None:
216
+ outputs, trace_additional_metadata = (
217
+ langchain_helpers.split_big_langgraph_outputs(outputs)
218
+ )
137
219
 
138
- span_data = self._span_data_map[run.id]
220
+ if not self._opik_context_read_only_mode:
221
+ self._ensure_no_hanging_opik_tracer_spans()
139
222
 
223
+ span_data = self._span_data_map.get(run.id)
140
224
  if (
141
- span_data.parent_span_id is not None
142
- and self._is_opik_span_created_by_this_tracer(span_data.parent_span_id)
225
+ span_data is None
226
+ or span_data.trace_id not in self._externally_created_traces_ids
143
227
  ):
144
- # Langchain lost parent-child relationship for Run, so it calls _persist_run
145
- # for a subchain when the ACTUAL root run is not yet persisted.
146
- # However we know that the parent span was created by this tracer, so we don't
147
- # want to finalize the trace
148
- return
228
+ self._finalize_trace(
229
+ run_id=run.id,
230
+ run_dict=run_dict,
231
+ trace_additional_metadata=trace_additional_metadata,
232
+ outputs=outputs,
233
+ error_info=error_info,
234
+ )
149
235
 
150
- self._ensure_no_hanging_opik_tracer_spans()
236
+ def _finalize_trace(
237
+ self,
238
+ run_id: UUID,
239
+ run_dict: Dict[str, Any],
240
+ trace_additional_metadata: Optional[Dict[str, Any]],
241
+ outputs: Optional[Dict[str, Any]],
242
+ error_info: Optional[ErrorInfoDict],
243
+ ) -> None:
244
+ trace_data = self._created_traces_data_map.get(run_id)
245
+ if trace_data is None:
246
+ LOGGER.warning(
247
+ f"Trace data for run '{run_id}' not found in the traces data map. Skipping processing of _finalize_trace."
248
+ )
249
+ return
151
250
 
152
- if span_data.trace_id not in self._externally_created_traces_ids:
153
- trace_data = self._created_traces_data_map[run.id]
251
+ # workaround for `.astream()` method usage
252
+ if trace_data.input == {"input": ""}:
253
+ trace_data.input = run_dict["inputs"]
154
254
 
155
- # workaround for `.astream()` method usage
156
- if trace_data.input == {"input": ""}:
157
- trace_data.input = run_dict["inputs"]
255
+ if trace_additional_metadata:
256
+ trace_data.update(metadata=trace_additional_metadata)
158
257
 
159
- trace_data.init_end_time().update(output=output, error_info=error_info)
160
- trace_ = self._opik_client.trace(**trace_data.as_parameters)
258
+ trace_data.init_end_time().update(output=outputs, error_info=error_info)
259
+ trace_ = self._opik_client.trace(**trace_data.as_parameters)
161
260
 
162
- assert trace_ is not None
163
- self._created_traces.append(trace_)
261
+ assert trace_ is not None
262
+ self._created_traces.append(trace_)
263
+ if not self._opik_context_read_only_mode:
164
264
  self._opik_context_storage.pop_trace_data(ensure_id=trace_data.id)
165
265
 
166
266
  def _ensure_no_hanging_opik_tracer_spans(self) -> None:
@@ -178,173 +278,170 @@ class OpikTracer(BaseTracer):
178
278
  )
179
279
 
180
280
  def _track_root_run(
181
- self, run_dict: Dict[str, Any]
182
- ) -> Tuple[Optional[trace.TraceData], span.SpanData]:
183
- run_metadata = run_dict["extra"].get("metadata", {})
281
+ self, run_dict: Dict[str, Any], allow_duplicating_root_span: bool
282
+ ) -> TrackRootRunResult:
283
+ run_metadata = _get_run_metadata(run_dict)
184
284
  root_metadata = dict_utils.deepmerge(self._trace_default_metadata, run_metadata)
185
285
  self._update_thread_id_from_metadata(run_dict)
186
286
 
187
- if self._distributed_headers:
188
- new_span_data = self._attach_span_to_distributed_headers(
189
- run_dict=run_dict,
190
- root_metadata=root_metadata,
191
- )
192
- return None, new_span_data
193
-
287
+ # Track the parent span ID for LangGraph cleanup later
194
288
  current_span_data = self._opik_context_storage.top_span_data()
195
- self._root_run_external_parent_span_id.set(
289
+ parent_span_id_when_langgraph_started = (
196
290
  current_span_data.id if current_span_data is not None else None
197
291
  )
198
- if current_span_data is not None:
199
- new_span_data = self._attach_span_to_existing_span(
200
- run_dict=run_dict,
201
- current_span_data=current_span_data,
202
- root_metadata=root_metadata,
203
- )
204
- return None, new_span_data
205
-
206
- current_trace_data = self._opik_context_storage.get_trace_data()
207
- if current_trace_data is not None:
208
- new_span_data = self._attach_span_to_existing_trace(
209
- run_dict=run_dict,
210
- current_trace_data=current_trace_data,
211
- root_metadata=root_metadata,
212
- )
213
- return None, new_span_data
214
-
215
- return self._initialize_span_and_trace_from_scratch(
216
- run_dict=run_dict, root_metadata=root_metadata
217
- )
218
-
219
- def _initialize_span_and_trace_from_scratch(
220
- self, run_dict: Dict[str, Any], root_metadata: Dict[str, Any]
221
- ) -> Tuple[trace.TraceData, span.SpanData]:
222
- trace_data = trace.TraceData(
223
- name=run_dict["name"],
224
- input=run_dict["inputs"],
225
- metadata=root_metadata,
226
- tags=self._trace_default_tags,
227
- project_name=self._project_name,
228
- thread_id=self._thread_id,
292
+ self._root_run_external_parent_span_id.set(
293
+ parent_span_id_when_langgraph_started
229
294
  )
230
295
 
231
- self._created_traces_data_map[run_dict["id"]] = trace_data
232
-
233
- span_data = span.SpanData(
234
- trace_id=trace_data.id,
235
- parent_span_id=None,
296
+ start_span_arguments = arguments_helpers.StartSpanParameters(
236
297
  name=run_dict["name"],
237
298
  input=run_dict["inputs"],
238
299
  type=_get_span_type(run_dict),
239
- metadata=root_metadata,
240
300
  tags=self._trace_default_tags,
301
+ metadata=root_metadata,
241
302
  project_name=self._project_name,
303
+ thread_id=self._thread_id,
242
304
  )
243
305
 
244
- self._span_data_map[run_dict["id"]] = span_data
245
-
246
- return trace_data, span_data
247
-
248
- def _attach_span_to_existing_span(
249
- self,
250
- run_dict: Dict[str, Any],
251
- current_span_data: span.SpanData,
252
- root_metadata: Dict[str, Any],
253
- ) -> span.SpanData:
254
- project_name = helpers.resolve_child_span_project_name(
255
- current_span_data.project_name,
256
- self._project_name,
306
+ span_creation_result = span_creation_handler.create_span_respecting_context(
307
+ start_span_arguments=start_span_arguments,
308
+ distributed_trace_headers=self._distributed_headers,
309
+ opik_context_storage=self._opik_context_storage,
257
310
  )
258
311
 
259
- span_data = span.SpanData(
260
- trace_id=current_span_data.trace_id,
261
- parent_span_id=current_span_data.id,
262
- name=run_dict["name"],
263
- input=run_dict["inputs"],
264
- metadata=root_metadata,
265
- tags=self._trace_default_tags,
266
- project_name=project_name,
267
- type=_get_span_type(run_dict),
312
+ trace_created_externally = (
313
+ span_creation_result.trace_data is None
314
+ and not self._is_opik_trace_created_by_this_tracer(
315
+ span_creation_result.span_data.trace_id
316
+ )
268
317
  )
269
- self._span_data_map[run_dict["id"]] = span_data
270
- if not self._is_opik_trace_created_by_this_tracer(span_data.trace_id):
271
- self._externally_created_traces_ids.add(span_data.trace_id)
272
-
273
- return span_data
318
+ if trace_created_externally:
319
+ self._externally_created_traces_ids.add(
320
+ span_creation_result.span_data.trace_id
321
+ )
274
322
 
275
- def _attach_span_to_existing_trace(
276
- self,
277
- run_dict: Dict[str, Any],
278
- current_trace_data: trace.TraceData,
279
- root_metadata: Dict[str, Any],
280
- ) -> span.SpanData:
281
- project_name = helpers.resolve_child_span_project_name(
282
- current_trace_data.project_name,
283
- self._project_name,
323
+ should_skip_root_span_creation = (
324
+ span_creation_result.trace_data is not None
325
+ and _is_root_run(run_dict)
326
+ and not allow_duplicating_root_span
284
327
  )
328
+ if should_skip_root_span_creation:
329
+ return TrackRootRunResult(
330
+ new_trace_data=span_creation_result.trace_data,
331
+ new_span_data=None,
332
+ )
285
333
 
286
- span_data = span.SpanData(
287
- trace_id=current_trace_data.id,
288
- parent_span_id=None,
289
- name=run_dict["name"],
290
- input=run_dict["inputs"],
291
- metadata=root_metadata,
292
- tags=self._trace_default_tags,
293
- project_name=project_name,
294
- type=_get_span_type(run_dict),
334
+ return TrackRootRunResult(
335
+ new_trace_data=span_creation_result.trace_data,
336
+ new_span_data=span_creation_result.span_data,
295
337
  )
296
- self._span_data_map[run_dict["id"]] = span_data
297
- if not self._is_opik_trace_created_by_this_tracer(current_trace_data.id):
298
- self._externally_created_traces_ids.add(current_trace_data.id)
299
- return span_data
300
338
 
301
- def _attach_span_to_distributed_headers(
302
- self,
303
- run_dict: Dict[str, Any],
304
- root_metadata: Dict[str, Any],
305
- ) -> span.SpanData:
306
- if self._distributed_headers is None:
307
- raise ValueError("Distributed headers are not set")
308
-
309
- span_data = span.SpanData(
310
- trace_id=self._distributed_headers["opik_trace_id"],
311
- parent_span_id=self._distributed_headers["opik_parent_span_id"],
312
- name=run_dict["name"],
313
- input=run_dict["inputs"],
314
- metadata=root_metadata,
315
- tags=self._trace_default_tags,
316
- project_name=self._project_name,
317
- type=_get_span_type(run_dict),
318
- )
319
- self._span_data_map[run_dict["id"]] = span_data
320
- self._externally_created_traces_ids.add(span_data.trace_id)
321
- return span_data
339
+ def _process_start_span(self, run: Run, allow_duplicating_root_span: bool) -> None:
340
+ try:
341
+ self._process_start_span_unsafe(run, allow_duplicating_root_span)
342
+ except Exception as e:
343
+ LOGGER.error("Failed during _process_start_span: %s", e, exc_info=True)
322
344
 
323
- def _process_start_span(self, run: "Run") -> None:
345
+ def _process_start_span_unsafe(
346
+ self, run: Run, allow_duplicating_root_span: bool
347
+ ) -> None:
324
348
  run_dict: Dict[str, Any] = run.dict()
325
- new_span_data: span.SpanData
326
- new_trace_data: Optional[trace.TraceData] = None
327
349
 
328
350
  if not run.parent_run_id:
329
- # This is the first run for the chain.
330
- new_trace_data, new_span_data = self._track_root_run(run_dict)
331
- if new_trace_data is not None:
332
- self._opik_context_storage.set_trace_data(new_trace_data)
333
- if (
334
- self._opik_client.config.log_start_trace_span
335
- and tracing_runtime_config.is_tracing_active()
336
- ):
337
- self._opik_client.trace(**new_trace_data.as_start_parameters)
351
+ self._create_root_trace_and_span(
352
+ run_id=run.id,
353
+ run_dict=run_dict,
354
+ allow_duplicating_root_span=allow_duplicating_root_span,
355
+ )
356
+ return
357
+
358
+ # Check if the parent is a skipped LangGraph/LangChain root run.
359
+ # If so, attach children directly to trace.
360
+ # Otherwise, attach to the parent span.
361
+ if run.parent_run_id in self._skipped_langgraph_root_run_ids:
362
+ self._attach_span_to_local_or_distributed_trace(
363
+ run_id=run.id,
364
+ parent_run_id=run.parent_run_id,
365
+ run_dict=run_dict,
366
+ )
367
+ else:
368
+ self._attach_span_to_parent_span(
369
+ run_id=run.id, parent_run_id=run.parent_run_id, run_dict=run_dict
370
+ )
371
+
372
+ def _create_root_trace_and_span(
373
+ self, run_id: UUID, run_dict: Dict[str, Any], allow_duplicating_root_span: bool
374
+ ) -> None:
375
+ """
376
+ Creates a root trace and span for a given run and stores the relevant trace and span
377
+ data in local storage for future reference.
378
+
379
+ The new span is only created if no new trace is created, i.e., when attached to an existing span
380
+ or distributed headers. If a new trace is created, the span is skipped and only the
381
+ trace data is stored in local storage for future reference.
382
+ """
383
+ # This is the first run for the chain.
384
+ root_run_result = self._track_root_run(run_dict, allow_duplicating_root_span)
385
+ if root_run_result.new_trace_data is not None:
386
+ if not self._opik_context_read_only_mode:
387
+ self._opik_context_storage.set_trace_data(
388
+ root_run_result.new_trace_data
389
+ )
338
390
 
339
- self._opik_context_storage.add_span_data(new_span_data)
340
391
  if (
341
392
  self._opik_client.config.log_start_trace_span
342
393
  and tracing_runtime_config.is_tracing_active()
343
394
  ):
344
- self._opik_client.span(**new_span_data.as_start_parameters)
345
- return
395
+ self._opik_client.trace(
396
+ **root_run_result.new_trace_data.as_start_parameters
397
+ )
398
+
399
+ # If this is a LangGraph/LangChain root run under fresh trace, skip creating the span
400
+ if root_run_result.new_span_data is None:
401
+ # Mark this run as skipped and store trace data for child runs
402
+ self._skipped_langgraph_root_run_ids.add(run_id)
403
+
404
+ # Store parent span ID if LangGraph was attached to the existing span
405
+ parent_span_id = self._root_run_external_parent_span_id.get()
406
+ self._langgraph_parent_span_ids[run_id] = parent_span_id
407
+
408
+ # Store trace data if we created a new trace but skip span data
409
+ if root_run_result.new_trace_data is not None:
410
+ self._save_span_trace_data_to_local_maps(
411
+ run_id=run_id,
412
+ span_data=None,
413
+ trace_data=root_run_result.new_trace_data,
414
+ )
415
+ else:
416
+ # save new span and trace data to local maps to be able to retrieve them later
417
+ self._save_span_trace_data_to_local_maps(
418
+ run_id=run_id,
419
+ span_data=root_run_result.new_span_data,
420
+ trace_data=root_run_result.new_trace_data,
421
+ )
422
+
423
+ if not self._opik_context_read_only_mode:
424
+ self._opik_context_storage.add_span_data(root_run_result.new_span_data)
425
+
426
+ if (
427
+ self._opik_client.config.log_start_trace_span
428
+ and tracing_runtime_config.is_tracing_active()
429
+ ):
430
+ self._opik_client.span(
431
+ **root_run_result.new_span_data.as_start_parameters
432
+ )
433
+
434
+ def _attach_span_to_parent_span(
435
+ self, run_id: UUID, parent_run_id: UUID, run_dict: Dict[str, Any]
436
+ ) -> None:
437
+ """
438
+ Attaches child span to a parent span and updates relevant context storage.
346
439
 
347
- parent_span_data = self._span_data_map[run.parent_run_id]
440
+ This method is responsible for creating a new span data object associated with a
441
+ run, linking it to the parent span data, and saving it to local and external maps.
442
+ Additionally, it updates the context storage and logs the span if tracing is active.
443
+ """
444
+ parent_span_data = self._span_data_map[parent_run_id]
348
445
 
349
446
  project_name = helpers.resolve_child_span_project_name(
350
447
  parent_span_data.project_name,
@@ -355,31 +452,133 @@ class OpikTracer(BaseTracer):
355
452
  trace_id=parent_span_data.trace_id,
356
453
  parent_span_id=parent_span_data.id,
357
454
  input=run_dict["inputs"],
358
- metadata=run_dict["extra"],
359
- name=run.name,
455
+ metadata=_get_run_metadata(run_dict),
456
+ name=run_dict["name"],
360
457
  type=_get_span_type(run_dict),
361
458
  project_name=project_name,
362
459
  )
363
460
  new_span_data.update(metadata={"created_from": "langchain"})
364
461
 
365
- self._span_data_map[run.id] = new_span_data
462
+ self._save_span_trace_data_to_local_maps(
463
+ run_id=run_id,
464
+ span_data=new_span_data,
465
+ trace_data=None,
466
+ )
366
467
 
367
468
  if new_span_data.trace_id not in self._externally_created_traces_ids:
368
- self._created_traces_data_map[run.id] = self._created_traces_data_map[
369
- run.parent_run_id
469
+ self._created_traces_data_map[run_id] = self._created_traces_data_map[
470
+ parent_run_id
370
471
  ]
371
472
 
372
- self._opik_context_storage.add_span_data(new_span_data)
473
+ if not self._opik_context_read_only_mode:
474
+ self._opik_context_storage.add_span_data(new_span_data)
475
+
373
476
  if (
374
477
  self._opik_client.config.log_start_trace_span
375
478
  and tracing_runtime_config.is_tracing_active()
376
479
  ):
377
480
  self._opik_client.span(**new_span_data.as_start_parameters)
378
481
 
379
- def _process_end_span(self, run: "Run") -> None:
482
+ def _attach_span_to_local_or_distributed_trace(
483
+ self, run_id: UUID, parent_run_id: UUID, run_dict: Dict[str, Any]
484
+ ) -> None:
485
+ """
486
+ Attaches child span directly to a trace by checking trace data or distributed
487
+ headers and creates new span data based on the provided run information.
488
+ """
489
+ # Check if we have trace data (new trace) or distributed headers
490
+ if parent_run_id in self._created_traces_data_map:
491
+ # LangGraph created a new trace - attach children directly to trace
492
+ trace_data = self._created_traces_data_map[parent_run_id]
493
+ project_name = helpers.resolve_child_span_project_name(
494
+ trace_data.project_name,
495
+ self._project_name,
496
+ )
497
+
498
+ new_span_data = span.SpanData(
499
+ trace_id=trace_data.id,
500
+ parent_span_id=None, # Direct child of trace
501
+ input=run_dict["inputs"],
502
+ metadata=_get_run_metadata(run_dict),
503
+ name=run_dict["name"],
504
+ type=_get_span_type(run_dict),
505
+ project_name=project_name,
506
+ )
507
+ if new_span_data.trace_id not in self._externally_created_traces_ids:
508
+ self._created_traces_data_map[run_id] = trace_data
509
+
510
+ elif self._distributed_headers:
511
+ # LangGraph with distributed headers - attach to distributed trace
512
+ new_span_data = span.SpanData(
513
+ trace_id=self._distributed_headers["opik_trace_id"],
514
+ parent_span_id=self._distributed_headers["opik_parent_span_id"],
515
+ name=run_dict["name"],
516
+ input=run_dict["inputs"],
517
+ metadata=_get_run_metadata(run_dict),
518
+ tags=self._trace_default_tags,
519
+ project_name=self._project_name,
520
+ type=_get_span_type(run_dict),
521
+ )
522
+ self._externally_created_traces_ids.add(new_span_data.trace_id)
523
+
524
+ elif (
525
+ current_trace_data := self._opik_context_storage.get_trace_data()
526
+ ) is not None:
527
+ # LangGraph attached to existing trace - attach children directly to trace
528
+ project_name = helpers.resolve_child_span_project_name(
529
+ current_trace_data.project_name,
530
+ self._project_name,
531
+ )
532
+
533
+ new_span_data = span.SpanData(
534
+ trace_id=current_trace_data.id,
535
+ parent_span_id=None,
536
+ name=run_dict["name"],
537
+ input=run_dict["inputs"],
538
+ metadata=_get_run_metadata(run_dict),
539
+ tags=self._trace_default_tags,
540
+ project_name=project_name,
541
+ type=_get_span_type(run_dict),
542
+ )
543
+
544
+ if not self._is_opik_trace_created_by_this_tracer(current_trace_data.id):
545
+ self._externally_created_traces_ids.add(current_trace_data.id)
546
+ else:
547
+ LOGGER.warning(
548
+ f"Cannot find trace data or distributed headers for LangGraph child run '{run_id}'"
549
+ )
550
+ return
551
+
552
+ new_span_data.update(metadata={"created_from": "langchain"})
553
+ self._save_span_trace_data_to_local_maps(
554
+ run_id=run_id,
555
+ span_data=new_span_data,
556
+ trace_data=None,
557
+ )
558
+
559
+ if not self._opik_context_read_only_mode:
560
+ self._opik_context_storage.add_span_data(new_span_data)
561
+
562
+ if (
563
+ self._opik_client.config.log_start_trace_span
564
+ and tracing_runtime_config.is_tracing_active()
565
+ ):
566
+ self._opik_client.span(**new_span_data.as_start_parameters)
567
+
568
+ def _process_end_span(self, run: Run) -> None:
569
+ span_data = None
380
570
  try:
381
- run_dict: Dict[str, Any] = run.dict()
571
+ # Skip processing if this is a skipped LangGraph root run
572
+ if run.id in self._skipped_langgraph_root_run_ids:
573
+ return
574
+
575
+ if run.id not in self._span_data_map:
576
+ LOGGER.warning(
577
+ f"Span data for run '{run.id}' not found in the span data map. Skipping processing of end span."
578
+ )
579
+ return
382
580
  span_data = self._span_data_map[run.id]
581
+ run_dict: Dict[str, Any] = run.dict()
383
582
 
384
583
  usage_info = provider_usage_extractors.try_extract_provider_usage_data(
385
584
  run_dict
@@ -391,8 +590,15 @@ class OpikTracer(BaseTracer):
391
590
  if span_data.input == {"input": ""}:
392
591
  span_data.input = run_dict["inputs"]
393
592
 
593
+ filtered_output, additional_metadata = (
594
+ langchain_helpers.split_big_langgraph_outputs(run_dict["outputs"])
595
+ )
596
+
597
+ if additional_metadata:
598
+ span_data.update(metadata=additional_metadata)
599
+
394
600
  span_data.init_end_time().update(
395
- output=run_dict["outputs"],
601
+ output=filtered_output,
396
602
  usage=(
397
603
  usage_info.usage.provider_usage.model_dump()
398
604
  if isinstance(usage_info.usage, llm_usage.OpikUsage)
@@ -407,42 +613,78 @@ class OpikTracer(BaseTracer):
407
613
  except Exception as e:
408
614
  LOGGER.error(f"Failed during _process_end_span: {e}", exc_info=True)
409
615
  finally:
410
- self._opik_context_storage.trim_span_data_stack_to_certain_span(
411
- span_id=span_data.id
616
+ if span_data is not None and not self._opik_context_read_only_mode:
617
+ self._opik_context_storage.trim_span_data_stack_to_certain_span(
618
+ span_id=span_data.id
619
+ )
620
+ self._opik_context_storage.pop_span_data(ensure_id=span_data.id)
621
+
622
+ def _should_skip_error(self, error_str: str) -> bool:
623
+ if self._skip_error_callback is None:
624
+ return False
625
+
626
+ return self._skip_error_callback(error_str)
627
+
628
+ def _process_end_span_with_error(self, run: Run) -> None:
629
+ # Skip processing if this is a skipped LangGraph root run
630
+ if run.id in self._skipped_langgraph_root_run_ids:
631
+ return
632
+
633
+ if run.id not in self._span_data_map:
634
+ LOGGER.warning(
635
+ f"Span data for run '{run.id}' not found in the span data map. Skipping processing of _process_end_span_with_error."
412
636
  )
413
- self._opik_context_storage.pop_span_data(ensure_id=span_data.id)
637
+ return
414
638
 
415
- def _process_end_span_with_error(self, run: "Run") -> None:
639
+ span_data = None
416
640
  try:
417
641
  run_dict: Dict[str, Any] = run.dict()
418
642
  span_data = self._span_data_map[run.id]
419
- error_info: ErrorInfoDict = {
420
- "exception_type": "Exception",
421
- "traceback": run_dict["error"],
422
- }
643
+ error_str = run_dict["error"]
644
+
645
+ if self._should_skip_error(error_str):
646
+ span_data.init_end_time().update(output=ERROR_SKIPPED_OUTPUTS)
647
+ else:
648
+ error_info = ErrorInfoDict(
649
+ exception_type="Exception",
650
+ traceback=error_str,
651
+ )
652
+ span_data.init_end_time().update(
653
+ output=None,
654
+ error_info=error_info,
655
+ )
423
656
 
424
- span_data.init_end_time().update(
425
- output=None,
426
- error_info=error_info,
427
- )
428
657
  if tracing_runtime_config.is_tracing_active():
429
658
  self._opik_client.span(**span_data.as_parameters)
430
659
  except Exception as e:
431
660
  LOGGER.debug(f"Failed during _process_end_span_with_error: {e}")
432
661
  finally:
433
- self._opik_context_storage.trim_span_data_stack_to_certain_span(
434
- span_id=span_data.id
435
- )
436
- self._opik_context_storage.pop_span_data(ensure_id=span_data.id)
662
+ if span_data is not None and not self._opik_context_read_only_mode:
663
+ self._opik_context_storage.trim_span_data_stack_to_certain_span(
664
+ span_id=span_data.id
665
+ )
666
+ self._opik_context_storage.pop_span_data(ensure_id=span_data.id)
437
667
 
438
668
  def _update_thread_id_from_metadata(self, run_dict: Dict[str, Any]) -> None:
439
669
  if not self._thread_id:
440
670
  # We want to default to any manually set thread_id, so only update if self._thread_id is not already set
441
- thread_id = run_dict["extra"].get("metadata", {}).get("thread_id")
671
+ thread_id = _get_run_metadata(run_dict).get("thread_id")
442
672
 
443
673
  if thread_id:
444
674
  self._thread_id = thread_id
445
675
 
676
+ def _save_span_trace_data_to_local_maps(
677
+ self,
678
+ run_id: UUID,
679
+ span_data: Optional[span.SpanData],
680
+ trace_data: Optional[trace.TraceData],
681
+ ) -> None:
682
+ if span_data is not None:
683
+ self._span_data_map[run_id] = span_data
684
+
685
+ if trace_data is not None:
686
+ self._created_traces_data_map[run_id] = trace_data
687
+
446
688
  def flush(self) -> None:
447
689
  """
448
690
  Flush to ensure all data is sent to the Opik server.
@@ -458,31 +700,34 @@ class OpikTracer(BaseTracer):
458
700
  """
459
701
  return self._created_traces
460
702
 
703
+ def get_current_span_data_for_run(self, run_id: UUID) -> Optional[span.SpanData]:
704
+ return self._span_data_map.get(run_id)
705
+
461
706
  def _skip_tracking(self) -> bool:
462
707
  if not tracing_runtime_config.is_tracing_active():
463
708
  return True
464
709
 
465
710
  return False
466
711
 
467
- def _on_llm_start(self, run: "Run") -> None:
712
+ def _on_llm_start(self, run: Run) -> None:
468
713
  """Process the LLM Run upon start."""
469
714
  if self._skip_tracking():
470
715
  return
471
716
 
472
- self._process_start_span(run)
717
+ self._process_start_span(run, allow_duplicating_root_span=True)
473
718
 
474
719
  def on_chat_model_start(
475
720
  self,
476
721
  serialized: Dict[str, Any],
477
722
  messages: List[List["BaseMessage"]],
478
723
  *,
479
- run_id: "UUID",
724
+ run_id: UUID,
480
725
  tags: Optional[List[str]] = None,
481
- parent_run_id: Optional["UUID"] = None,
726
+ parent_run_id: Optional[UUID] = None,
482
727
  metadata: Optional[Dict[str, Any]] = None,
483
728
  name: Optional[str] = None,
484
729
  **kwargs: Any,
485
- ) -> "Run":
730
+ ) -> Run:
486
731
  """Start a trace for an LLM run.
487
732
 
488
733
  Duplicated from Langchain tracer, it is disabled by default in all tracers, see https://github.com/langchain-ai/langchain/blob/fdda1aaea14b257845a19023e8af5e20140ec9fe/libs/core/langchain_core/callbacks/manager.py#L270-L289 and https://github.com/langchain-ai/langchain/blob/fdda1aaea14b257845a19023e8af5e20140ec9fe/libs/core/langchain_core/tracers/core.py#L168-L180
@@ -524,63 +769,63 @@ class OpikTracer(BaseTracer):
524
769
  self._on_chat_model_start(chat_model_run)
525
770
  return chat_model_run
526
771
 
527
- def _on_chat_model_start(self, run: "Run") -> None:
772
+ def _on_chat_model_start(self, run: Run) -> None:
528
773
  """Process the Chat Model Run upon start."""
529
774
  if self._skip_tracking():
530
775
  return
531
776
 
532
- self._process_start_span(run)
777
+ self._process_start_span(run, allow_duplicating_root_span=True)
533
778
 
534
- def _on_llm_end(self, run: "Run") -> None:
779
+ def _on_llm_end(self, run: Run) -> None:
535
780
  """Process the LLM Run."""
536
781
  if self._skip_tracking():
537
782
  return
538
783
 
539
784
  self._process_end_span(run)
540
785
 
541
- def _on_llm_error(self, run: "Run") -> None:
786
+ def _on_llm_error(self, run: Run) -> None:
542
787
  """Process the LLM Run upon error."""
543
788
  if self._skip_tracking():
544
789
  return
545
790
 
546
791
  self._process_end_span_with_error(run)
547
792
 
548
- def _on_chain_start(self, run: "Run") -> None:
793
+ def _on_chain_start(self, run: Run) -> None:
549
794
  """Process the Chain Run upon start."""
550
795
  if self._skip_tracking():
551
796
  return
552
797
 
553
- self._process_start_span(run)
798
+ self._process_start_span(run, allow_duplicating_root_span=False)
554
799
 
555
- def _on_chain_end(self, run: "Run") -> None:
800
+ def _on_chain_end(self, run: Run) -> None:
556
801
  """Process the Chain Run."""
557
802
  if self._skip_tracking():
558
803
  return
559
804
 
560
805
  self._process_end_span(run)
561
806
 
562
- def _on_chain_error(self, run: "Run") -> None:
807
+ def _on_chain_error(self, run: Run) -> None:
563
808
  """Process the Chain Run upon error."""
564
809
  if self._skip_tracking():
565
810
  return
566
811
 
567
812
  self._process_end_span_with_error(run)
568
813
 
569
- def _on_tool_start(self, run: "Run") -> None:
814
+ def _on_tool_start(self, run: Run) -> None:
570
815
  """Process the Tool Run upon start."""
571
816
  if self._skip_tracking():
572
817
  return
573
818
 
574
- self._process_start_span(run)
819
+ self._process_start_span(run, allow_duplicating_root_span=True)
575
820
 
576
- def _on_tool_end(self, run: "Run") -> None:
821
+ def _on_tool_end(self, run: Run) -> None:
577
822
  """Process the Tool Run."""
578
823
  if self._skip_tracking():
579
824
  return
580
825
 
581
826
  self._process_end_span(run)
582
827
 
583
- def _on_tool_error(self, run: "Run") -> None:
828
+ def _on_tool_error(self, run: Run) -> None:
584
829
  """Process the Tool Run upon error."""
585
830
  if self._skip_tracking():
586
831
  return