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
@@ -0,0 +1,137 @@
1
+ import mimetypes
2
+ import random
3
+ import time
4
+ from typing import Optional
5
+
6
+
7
+ # The attachment file name regex
8
+ ATTACHMENT_FILE_NAME_REGEX = r"(?:input|output|metadata)-attachment-\d+-\d+-sdk\.\w+"
9
+ ATTACHMENT_FILE_NAME_PLACEHOLDER_REGEX = (
10
+ r"\[((?:input|output|metadata)-attachment-\d+-\d+-sdk\.\w+)\]"
11
+ )
12
+
13
+
14
+ def get_file_extension(mime_type: str) -> str:
15
+ """Convert MIME type to file extension.
16
+
17
+ Mirrors the Java getFileExtension() method in AttachmentStripperService.
18
+
19
+ Args:
20
+ mime_type: The MIME type (e.g., "image/png", "application/pdf")
21
+
22
+ Returns:
23
+ File extension without a leading dot (e.g., "png", "pdf")
24
+ """
25
+ if not mime_type:
26
+ return "bin"
27
+
28
+ # Try to get extension from mimetypes module
29
+ extension = mimetypes.guess_extension(mime_type, strict=False)
30
+
31
+ if extension:
32
+ # Remove the leading dot
33
+ extension = extension.lstrip(".")
34
+ # Handle special cases where mimetypes returns less common extensions
35
+ if mime_type == "image/jpeg" and extension == "jpe":
36
+ return "jpg"
37
+ return extension
38
+
39
+ # Fallback: extract from the MIME type (e.g., "image/png" -> "png")
40
+ if "/" in mime_type:
41
+ subtype = mime_type.split("/")[1]
42
+ # Handle special cases like "svg+xml" -> "svg"
43
+ if "+" in subtype:
44
+ subtype = subtype.split("+")[0]
45
+ # Remove any parameters (e.g., "jpeg; charset=utf-8" -> "jpeg")
46
+ subtype = subtype.split(";")[0].strip()
47
+ return subtype
48
+
49
+ return "bin"
50
+
51
+
52
+ def detect_mime_type(data: bytes) -> Optional[str]:
53
+ """Detect MIME type from byte content using magic bytes.
54
+
55
+ This provides basic MIME type detection similar to Apache Tika in the Java implementation.
56
+ It checks common file format magic bytes.
57
+
58
+ Args:
59
+ data: The byte data to analyze
60
+
61
+ Returns:
62
+ Detected MIME type string, or "application/octet-stream" if unknown
63
+ """
64
+ if len(data) < 4:
65
+ return "application/octet-stream"
66
+
67
+ # Check common file format magic bytes
68
+ # PNG
69
+ if data[:8] == b"\x89PNG\r\n\x1a\n":
70
+ return "image/png"
71
+
72
+ # JPEG
73
+ if data[:2] == b"\xff\xd8" and data[-2:] == b"\xff\xd9":
74
+ return "image/jpeg"
75
+
76
+ # GIF
77
+ if data[:6] in (b"GIF87a", b"GIF89a"):
78
+ return "image/gif"
79
+
80
+ # PDF
81
+ if data[:4] == b"%PDF":
82
+ return "application/pdf"
83
+
84
+ # WebP
85
+ if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
86
+ return "image/webp"
87
+
88
+ # SVG (XML-based, check for SVG tag)
89
+ try:
90
+ text = data[:1024].decode("utf-8", errors="ignore")
91
+ if "<svg" in text.lower():
92
+ return "image/svg+xml"
93
+ except Exception:
94
+ pass
95
+
96
+ # MP4
97
+ if len(data) >= 12 and data[4:8] == b"ftyp":
98
+ return "video/mp4"
99
+
100
+ # JSON
101
+ try:
102
+ text = data[:100].decode("utf-8", errors="strict").strip()
103
+ if text.startswith("{") or text.startswith("["):
104
+ return "application/json"
105
+ except Exception:
106
+ pass
107
+
108
+ # Default to octet-stream for unknown types
109
+ return "application/octet-stream"
110
+
111
+
112
+ def create_attachment_filename(context: str, extension: str) -> str:
113
+ """
114
+ Generates a unique attachment filename based on the provided context and file extension.
115
+
116
+ This function creates a filename by combining the given context, a randomly generated
117
+ prefix to ensure uniqueness, the current timestamp in milliseconds, and the provided
118
+ file extension. The generated filename aligns with the backend convention for naming
119
+ attachments, which includes specific formatting and structure.
120
+
121
+ Args:
122
+ context: The context to use as the base for the filename (e.g., "input",
123
+ "output", or "metadata").
124
+ extension: The file extension to use for the filename (e.g., "png",
125
+ "jpg", "txt").
126
+
127
+ Returns:
128
+ A generated filename string in the format
129
+ "{context}-attachment-{random_prefix}-{timestamp}.{extension}".
130
+ """
131
+ # The backend has the following naming convention: r"\\[((?:input|output|metadata)-attachment-\\d+-\\d+\\.\\w+)\\]"
132
+ # Example: [input-attachment-1-1704067200000.png]
133
+
134
+ timestamp = int(round(time.time() * 1000))
135
+ # we need to generate a large enough random prefix to avoid collisions
136
+ random_prefix = random.randint(1, 99999999)
137
+ return f"{context}-attachment-{random_prefix}-{timestamp}-sdk.{extension}"
@@ -0,0 +1,79 @@
1
+ from typing import Optional, Any, Dict, List, Union
2
+
3
+ import pydantic
4
+
5
+ from opik import dict_utils
6
+
7
+
8
+ def merge_tags(
9
+ existing_tags: Optional[List[str]], new_tags: Optional[List[str]]
10
+ ) -> Optional[List[str]]:
11
+ """Merge tag lists, preserving existing tags and adding new ones.
12
+
13
+ If both existing_tags and new_tags are None or empty, return None."""
14
+ if existing_tags is None and new_tags is None:
15
+ return None
16
+
17
+ result = list(existing_tags or [])
18
+ if new_tags:
19
+ for tag in new_tags:
20
+ if tag not in result:
21
+ result.append(tag)
22
+
23
+ return result if result else None
24
+
25
+
26
+ def merge_metadata(
27
+ existing_metadata: Optional[Dict[str, Any]],
28
+ new_metadata: Optional[Union[Dict[str, Any], pydantic.BaseModel]],
29
+ prompts: Optional[List[Dict[str, Any]]] = None,
30
+ ) -> Optional[Dict[str, Any]]:
31
+ """Merge the existing metadata dictionary with new data, with new values taking precedence.
32
+
33
+ If both existing_metadata and new_metadata are None or empty, return None.
34
+ """
35
+ if prompts is not None:
36
+ new_metadata = new_metadata or {}
37
+ new_metadata["opik_prompts"] = prompts
38
+
39
+ return _merge_dictionary_with_data(existing_metadata, new_data=new_metadata)
40
+
41
+
42
+ def merge_inputs(
43
+ existing_inputs: Optional[Dict[str, Any]],
44
+ new_inputs: Optional[Union[Dict[str, Any], pydantic.BaseModel]],
45
+ ) -> Optional[Dict[str, Any]]:
46
+ """Merge the existing input dictionary with new data, with new values taking precedence.
47
+
48
+ If both existing_inputs and new_inputs are None or empty, return None."""
49
+ return _merge_dictionary_with_data(existing_inputs, new_data=new_inputs)
50
+
51
+
52
+ def merge_outputs(
53
+ existing_outputs: Optional[Dict[str, Any]],
54
+ new_outputs: Optional[Union[Dict[str, Any], pydantic.BaseModel]],
55
+ ) -> Optional[Dict[str, Any]]:
56
+ """Merge the existing output dictionary with new data, with new values taking precedence.
57
+
58
+ If both existing_outputs and new_outputs are None or empty, return None."""
59
+ return _merge_dictionary_with_data(existing_outputs, new_data=new_outputs)
60
+
61
+
62
+ def _merge_dictionary_with_data(
63
+ existing_dict: Optional[Dict[str, Any]],
64
+ new_data: Optional[Union[Dict[str, Any], pydantic.BaseModel]],
65
+ ) -> Optional[Dict[str, Any]]:
66
+ """Merge the dictionary with new data, with new values taking precedence.
67
+
68
+ If both existing_dict and new_data are None or empty, return None."""
69
+ if existing_dict is None and new_data is None:
70
+ return None
71
+
72
+ if isinstance(new_data, pydantic.BaseModel):
73
+ new_data = new_data.model_dump()
74
+
75
+ result = dict(existing_dict or {})
76
+ if new_data:
77
+ result = dict_utils.deepmerge(result, new_data)
78
+
79
+ return result if result else None
@@ -1,11 +1,14 @@
1
1
  import logging
2
2
  import functools
3
- from typing import Optional, Any, List, Dict, Sequence, Set, TYPE_CHECKING
3
+ import time
4
+ from typing import Optional, Any, List, Dict, Sequence, Set, TYPE_CHECKING, Callable
4
5
 
5
6
  from opik.api_objects import rest_stream_parser
6
7
  from opik.rest_api import client as rest_api_client
7
8
  from opik.rest_api.types import dataset_item_write as rest_dataset_item
9
+ from opik.rest_api.core.api_error import ApiError
8
10
  from opik.message_processing.batching import sequence_splitter
11
+ from opik.rate_limit import rate_limit
9
12
  import opik.exceptions as exceptions
10
13
  import opik.config as config
11
14
  from opik.rest_client_configurator import retry_decorator
@@ -18,6 +21,54 @@ if TYPE_CHECKING:
18
21
  LOGGER = logging.getLogger(__name__)
19
22
 
20
23
 
24
+ def _ensure_rest_api_call_respecting_rate_limit(
25
+ rest_callable: Callable[[], Any],
26
+ ) -> Any:
27
+ """
28
+ Execute a REST API call with automatic retry on rate limit (429) errors.
29
+
30
+ This function handles HTTP 429 rate limit errors by waiting for the duration
31
+ specified in the response headers and retrying the request. Regular retries
32
+ for other errors are handled by the underlying rest client.
33
+
34
+ Args:
35
+ rest_callable: A callable that performs the REST API call.
36
+
37
+ Returns:
38
+ The result of the successful REST API call.
39
+
40
+ Raises:
41
+ ApiError: If the error is not a 429 rate limit error.
42
+ """
43
+ while True:
44
+ try:
45
+ result = rest_callable()
46
+ return result
47
+ except ApiError as exception:
48
+ if exception.status_code == 429:
49
+ # Parse rate limit headers to get retry delay
50
+ if exception.headers is not None:
51
+ rate_limiter = rate_limit.parse_rate_limit(exception.headers)
52
+ if rate_limiter is not None:
53
+ retry_after = rate_limiter.retry_after()
54
+ LOGGER.info(
55
+ "Rate limited (HTTP 429), retrying in %s seconds",
56
+ retry_after,
57
+ )
58
+ time.sleep(retry_after)
59
+ continue
60
+
61
+ # Fallback: wait 1 second if no header available
62
+ LOGGER.info(
63
+ "Rate limited (HTTP 429) with no retry-after header, retrying in 1 second"
64
+ )
65
+ time.sleep(1)
66
+ continue
67
+
68
+ # Re-raise if not a 429 error
69
+ raise
70
+
71
+
21
72
  class Dataset:
22
73
  def __init__(
23
74
  self,
@@ -52,6 +103,17 @@ class Dataset:
52
103
  """The description of the dataset."""
53
104
  return self._description
54
105
 
106
+ def _insert_batch_with_retry(
107
+ self, batch: List[rest_dataset_item.DatasetItemWrite]
108
+ ) -> None:
109
+ """Insert a batch of dataset items with automatic retry on rate limit errors."""
110
+ _ensure_rest_api_call_respecting_rate_limit(
111
+ lambda: self._rest_client.datasets.create_or_update_dataset_items(
112
+ dataset_name=self._name, items=batch
113
+ )
114
+ )
115
+ LOGGER.debug("Successfully sent dataset items batch of size %d", len(batch))
116
+
55
117
  def __internal_api__insert_items_as_dataclasses__(
56
118
  self, items: List[dataset_item.DatasetItem]
57
119
  ) -> None:
@@ -90,9 +152,7 @@ class Dataset:
90
152
 
91
153
  for batch in batches:
92
154
  LOGGER.debug("Sending dataset items batch of size %d", len(batch))
93
- self._rest_client.datasets.create_or_update_dataset_items(
94
- dataset_name=self._name, items=batch
95
- )
155
+ self._insert_batch_with_retry(batch)
96
156
 
97
157
  def insert(self, items: Sequence[Dict[str, Any]]) -> None:
98
158
  """
@@ -1,8 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import List
2
4
  from opik.rest_api import OpikApi
3
5
  import opik.exceptions as exceptions
6
+ from opik.message_processing import streamer
4
7
  from . import dataset
5
8
  from .. import experiment
9
+ from ..experiment import experiments_client
6
10
  from ...rest_api.core.api_error import ApiError
7
11
 
8
12
 
@@ -55,7 +59,11 @@ def get_dataset_id(rest_client: OpikApi, dataset_name: str) -> str:
55
59
 
56
60
 
57
61
  def get_dataset_experiments(
58
- rest_client: OpikApi, dataset_id: str, max_results: int = 1000
62
+ rest_client: OpikApi,
63
+ dataset_id: str,
64
+ max_results: int,
65
+ streamer: streamer.Streamer,
66
+ experiments_client: experiments_client.ExperimentsClient,
59
67
  ) -> List[experiment.Experiment]:
60
68
  page_size = 100
61
69
  experiments: List[experiment.Experiment] = []
@@ -78,7 +86,8 @@ def get_dataset_experiments(
78
86
  name=experiment_.name,
79
87
  dataset_name=experiment_.dataset_name,
80
88
  rest_client=rest_client,
81
- # TODO: add prompt if exists
89
+ streamer=streamer,
90
+ experiments_client=experiments_client,
82
91
  )
83
92
  )
84
93
 
@@ -1,15 +1,17 @@
1
1
  import functools
2
2
  import logging
3
- from typing import List, Optional
3
+ from typing import List, Optional, TYPE_CHECKING
4
4
 
5
- import opik.rest_api
6
5
  from opik.message_processing.batching import sequence_splitter
6
+ from opik.message_processing import messages, streamer
7
7
  from opik.rest_api import client as rest_api_client
8
- from opik.rest_api.types import experiment_item as rest_experiment_item
9
- from opik.rest_api.types import experiment_public
10
- from . import experiment_item
11
- from .. import constants, helpers, rest_stream_parser
12
- from ...api_objects.prompt import Prompt
8
+ from opik.rest_api import types as rest_api_types
9
+ from . import experiment_item, experiments_client
10
+ from .. import constants, helpers
11
+ from ...api_objects.prompt import base_prompt
12
+
13
+ if TYPE_CHECKING:
14
+ from opik.evaluation.metrics import score_result
13
15
 
14
16
  LOGGER = logging.getLogger(__name__)
15
17
 
@@ -21,13 +23,17 @@ class Experiment:
21
23
  name: Optional[str],
22
24
  dataset_name: str,
23
25
  rest_client: rest_api_client.OpikApi,
24
- prompts: Optional[List[Prompt]] = None,
26
+ streamer: streamer.Streamer,
27
+ experiments_client: experiments_client.ExperimentsClient,
28
+ prompts: Optional[List[base_prompt.BasePrompt]] = None,
25
29
  ) -> None:
26
30
  self._id = id
27
31
  self._name = name
28
32
  self._dataset_name = dataset_name
29
33
  self._rest_client = rest_client
30
34
  self._prompts = prompts
35
+ self._streamer = streamer
36
+ self._experiments_client = experiments_client
31
37
 
32
38
  @property
33
39
  def id(self) -> str:
@@ -57,7 +63,7 @@ class Experiment:
57
63
  def experiments_rest_client(self) -> rest_api_client.ExperimentsClient:
58
64
  return self._rest_client.experiments
59
65
 
60
- def get_experiment_data(self) -> experiment_public.ExperimentPublic:
66
+ def get_experiment_data(self) -> rest_api_types.experiment_public.ExperimentPublic:
61
67
  return self._rest_client.experiments.get_experiment_by_id(id=self.id)
62
68
 
63
69
  def insert(
@@ -74,8 +80,8 @@ class Experiment:
74
80
  Returns:
75
81
  None
76
82
  """
77
- rest_experiment_items = [
78
- rest_experiment_item.ExperimentItem(
83
+ experiment_item_messages = [
84
+ messages.ExperimentItemMessage(
79
85
  id=helpers.generate_id(),
80
86
  experiment_id=self.id,
81
87
  dataset_item_id=item.dataset_item_id,
@@ -84,68 +90,62 @@ class Experiment:
84
90
  for item in experiment_items_references
85
91
  ]
86
92
 
93
+ # Split into batches for the streamer
87
94
  batches = sequence_splitter.split_into_batches(
88
- rest_experiment_items, max_length=constants.EXPERIMENT_ITEMS_MAX_BATCH_SIZE
95
+ experiment_item_messages,
96
+ max_length=constants.FEEDBACK_SCORES_MAX_BATCH_SIZE,
89
97
  )
90
98
 
91
99
  for batch in batches:
92
- LOGGER.debug("Sending experiment items batch: %s", batch)
93
- self._rest_client.experiments.create_experiment_items(
94
- experiment_items=batch,
100
+ create_experiment_items_batch_message = (
101
+ messages.CreateExperimentItemsBatchMessage(batch=batch)
95
102
  )
96
- LOGGER.debug("Sent experiment items batch of size %d", len(batch))
103
+ self._streamer.put(create_experiment_items_batch_message)
97
104
 
98
105
  def get_items(
99
106
  self,
100
- max_results: Optional[int] = None,
107
+ max_results: Optional[int] = 10000,
101
108
  truncate: bool = False,
102
109
  ) -> List[experiment_item.ExperimentItemContent]:
103
110
  """
104
- Retrieves and returns a list of experiment items by streaming from the backend in batches, with an option to
105
- truncate the results for each batch.
106
-
107
- This method streams experiment items from a backend service in chunks up to the specified `max_results`
108
- or until the available items are exhausted. It handles batch-wise retrieval and parsing, ensuring the client
109
- receives a list of `ExperimentItemContent` objects, while respecting the constraints on maximum retrieval size
110
- from the backend. If truncation is enabled, the backend may return truncated details for each item.
111
+ Retrieves and returns a list of experiment items for this experiment.
111
112
 
112
113
  Args:
113
- max_results: Maximum number of experiment items to retrieve.
114
- truncate: Whether to truncate the items returned by the backend.
114
+ max_results: Maximum number of experiment items to retrieve. Defaults to 10000 if not specified.
115
+ truncate: Whether to truncate the items returned by the backend. Defaults to False.
115
116
 
117
+ Returns:
118
+ List of ExperimentItemContent objects for this experiment.
116
119
  """
117
- result: List[experiment_item.ExperimentItemContent] = []
118
- max_endpoint_batch_size = rest_stream_parser.MAX_ENDPOINT_BATCH_SIZE
119
-
120
- while True:
121
- if max_results is None:
122
- current_batch_size = max_endpoint_batch_size
123
- else:
124
- current_batch_size = min(
125
- max_results - len(result), max_endpoint_batch_size
126
- )
127
-
128
- items_stream = self._rest_client.experiments.stream_experiment_items(
129
- experiment_name=self.name,
130
- limit=current_batch_size,
131
- last_retrieved_id=result[-1].id if len(result) > 0 else None,
132
- truncate=truncate,
133
- )
120
+ if max_results is None:
121
+ max_results = 10000 # TODO: remove this once we have a proper way to get all experiment items
122
+
123
+ return self._experiments_client.find_experiment_items_for_dataset(
124
+ dataset_name=self.dataset_name,
125
+ experiment_ids=[self.id],
126
+ truncate=truncate,
127
+ max_results=max_results,
128
+ )
134
129
 
135
- experiment_item_compare_current_batch = (
136
- rest_stream_parser.read_and_parse_stream(
137
- stream=items_stream,
138
- item_class=opik.rest_api.ExperimentItemCompare,
139
- )
140
- )
130
+ def log_experiment_scores(
131
+ self,
132
+ score_results: List["score_result.ScoreResult"],
133
+ ) -> None:
134
+ """Log experiment-level scores to the backend."""
135
+ experiment_scores: List[rest_api_types.ExperimentScore] = []
141
136
 
142
- for item in experiment_item_compare_current_batch:
143
- converted_item = experiment_item.ExperimentItemContent.from_rest_experiment_item_compare(
144
- value=item
145
- )
146
- result.append(converted_item)
137
+ for score_result_ in score_results:
138
+ if score_result_.scoring_failed:
139
+ continue
147
140
 
148
- if current_batch_size > len(experiment_item_compare_current_batch):
149
- break
141
+ experiment_score = rest_api_types.ExperimentScore(
142
+ name=score_result_.name,
143
+ value=score_result_.value,
144
+ )
145
+ experiment_scores.append(experiment_score)
150
146
 
151
- return result
147
+ if experiment_scores:
148
+ self._rest_client.experiments.update_experiment(
149
+ id=self.id,
150
+ experiment_scores=experiment_scores,
151
+ )
@@ -24,6 +24,7 @@ class ExperimentItemContent:
24
24
  def from_rest_experiment_item_compare(
25
25
  cls,
26
26
  value: experiment_item_compare.ExperimentItemCompare,
27
+ dataset_item_data: Optional[Dict[str, Any]] = None,
27
28
  ) -> "ExperimentItemContent":
28
29
  if value.feedback_scores is None:
29
30
  feedback_scores: List[FeedbackScoreDict] = []
@@ -42,7 +43,7 @@ class ExperimentItemContent:
42
43
  id=value.id,
43
44
  trace_id=value.trace_id,
44
45
  dataset_item_id=value.dataset_item_id,
45
- dataset_item_data=value.input,
46
+ dataset_item_data=dataset_item_data if dataset_item_data else value.input,
46
47
  evaluation_task_output=value.output,
47
48
  feedback_scores=feedback_scores,
48
49
  )
@@ -0,0 +1,64 @@
1
+ import json
2
+ from typing import List, Optional
3
+
4
+ from . import rest_operations, experiment_item
5
+ from .. import opik_query_language
6
+ from ...rest_api import client as rest_api_client
7
+
8
+
9
+ class ExperimentsClient:
10
+ """Client for managing and querying experiments in bulk."""
11
+
12
+ def __init__(self, rest_client: rest_api_client.OpikApi):
13
+ self._rest_client = rest_client
14
+
15
+ def find_experiment_items_for_dataset(
16
+ self,
17
+ dataset_name: str,
18
+ experiment_ids: List[str],
19
+ truncate: bool = True,
20
+ max_results: int = 1000,
21
+ filter_string: Optional[str] = None,
22
+ ) -> List[experiment_item.ExperimentItemContent]:
23
+ """
24
+ Find experiment items associated with a specific dataset among a list of experiments.
25
+
26
+ This method queries the dataset for experiment items matching the
27
+ criteria provided by the input parameters. It leverages the
28
+ ExperimentsClient to perform the underlying operation.
29
+
30
+ Args:
31
+ dataset_name: Name of the dataset to query for experiment items.
32
+ experiment_ids: List of experiment IDs to filter the results.
33
+ filter_string: Optional filter string to refine the
34
+ query based on additional criteria (dataset fields, feedback scores, etc.).
35
+ truncate: Whether to truncate image data stored in input, output,
36
+ or metadata. Defaults to True.
37
+ max_results: Maximum number of results to return. Defaults to 1000.
38
+
39
+ Returns:
40
+ A list of experiment item content objects that match the criteria.
41
+ """
42
+ # prepare filter expression
43
+ if filter_string is not None:
44
+ filter_expression = json.dumps(
45
+ opik_query_language.OpikQueryLanguage(
46
+ filter_string
47
+ ).get_filter_expressions()
48
+ )
49
+ else:
50
+ filter_expression = None
51
+
52
+ # get dataset id
53
+ dataset_id = self._rest_client.datasets.get_dataset_by_identifier(
54
+ dataset_name=dataset_name
55
+ ).id
56
+
57
+ return rest_operations.find_experiment_items_for_dataset(
58
+ dataset_id=dataset_id,
59
+ experiment_ids=experiment_ids,
60
+ rest_client=self._rest_client,
61
+ max_results=max_results,
62
+ truncate=truncate,
63
+ filter_expression=filter_expression,
64
+ )
@@ -1,8 +1,12 @@
1
+ import copy
1
2
  import logging
2
- import opik.jsonable_encoder as jsonable_encoder
3
3
  from typing import Any, Dict, List, Mapping, Optional, Tuple
4
- import copy
5
- from .. import prompt
4
+
5
+ from opik import id_helpers
6
+ import opik.jsonable_encoder as jsonable_encoder
7
+
8
+ from ..prompt import base_prompt
9
+
6
10
 
7
11
  LOGGER = logging.getLogger(__name__)
8
12
 
@@ -11,7 +15,7 @@ PromptVersion = Dict[str, str]
11
15
 
12
16
  def build_metadata_and_prompt_versions(
13
17
  experiment_config: Optional[Dict[str, Any]],
14
- prompts: Optional[List[prompt.Prompt]],
18
+ prompts: Optional[List[base_prompt.BasePrompt]],
15
19
  ) -> Tuple[Optional[Dict[str, Any]], Optional[List[PromptVersion]]]:
16
20
  prompt_versions: Optional[List[PromptVersion]] = None
17
21
 
@@ -35,11 +39,22 @@ def build_metadata_and_prompt_versions(
35
39
 
36
40
  if prompts is not None and len(prompts) > 0:
37
41
  prompt_versions = []
38
- experiment_config["prompts"] = []
42
+ experiment_config["prompts"] = {}
39
43
 
40
- for prompt in prompts:
41
- prompt_versions.append({"id": prompt.__internal_api__version_id__})
42
- experiment_config["prompts"].append(prompt.prompt)
44
+ for prompt_obj in prompts:
45
+ prompt_versions.append({"id": prompt_obj.__internal_api__version_id__})
46
+ # Use __internal_api__to_info_dict__() to get the prompt content in a consistent way
47
+ prompt_info = prompt_obj.__internal_api__to_info_dict__()
48
+ # Extract the template/messages from the version dict
49
+ if "version" in prompt_info:
50
+ if "template" in prompt_info["version"]:
51
+ experiment_config["prompts"][prompt_obj.name] = prompt_info[
52
+ "version"
53
+ ]["template"]
54
+ elif "messages" in prompt_info["version"]:
55
+ experiment_config["prompts"][prompt_obj.name] = prompt_info[
56
+ "version"
57
+ ]["messages"]
43
58
 
44
59
  if experiment_config == {}:
45
60
  return None, None
@@ -50,9 +65,9 @@ def build_metadata_and_prompt_versions(
50
65
 
51
66
 
52
67
  def handle_prompt_args(
53
- prompt: Optional[prompt.Prompt] = None,
54
- prompts: Optional[List[prompt.Prompt]] = None,
55
- ) -> Optional[List[prompt.Prompt]]:
68
+ prompt: Optional[base_prompt.BasePrompt] = None,
69
+ prompts: Optional[List[base_prompt.BasePrompt]] = None,
70
+ ) -> Optional[List[base_prompt.BasePrompt]]:
56
71
  if prompts is not None and len(prompts) > 0 and prompt is not None:
57
72
  LOGGER.warning(
58
73
  "Arguments `prompt` and `prompts` are mutually exclusive, `prompts` will be used`."
@@ -63,3 +78,12 @@ def handle_prompt_args(
63
78
  prompts = None
64
79
 
65
80
  return prompts
81
+
82
+
83
+ def generate_unique_experiment_name(experiment_name_prefix: Optional[str]) -> str:
84
+ if experiment_name_prefix is None:
85
+ return id_helpers.generate_random_alphanumeric_string(12)
86
+
87
+ return (
88
+ f"{experiment_name_prefix}-{id_helpers.generate_random_alphanumeric_string(6)}"
89
+ )