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,244 @@
1
+ """PDF report generation functions for usage report module."""
2
+
3
+ import os
4
+ import traceback
5
+ from typing import Any, Dict
6
+
7
+ from rich.console import Console
8
+
9
+ from .charts import create_individual_chart
10
+ from .statistics import calculate_statistics
11
+
12
+ console = Console()
13
+
14
+
15
+ def create_pdf_report(data: Dict[str, Any], output_dir: str = ".") -> str:
16
+ """
17
+ Create a PDF report with statistics page and individual chart pages.
18
+
19
+ Args:
20
+ data: The extracted data dictionary
21
+ output_dir: Directory to save PDF (default: current directory)
22
+
23
+ Returns:
24
+ Path to saved PDF file
25
+ """
26
+ try:
27
+ from reportlab.lib import colors
28
+ from reportlab.lib.pagesizes import letter
29
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
30
+ from reportlab.lib.units import inch
31
+ from reportlab.platypus import (
32
+ Image,
33
+ PageBreak,
34
+ Paragraph,
35
+ SimpleDocTemplate,
36
+ Spacer,
37
+ Table,
38
+ TableStyle,
39
+ )
40
+ except ImportError:
41
+ raise ImportError(
42
+ "reportlab is required for PDF report generation. "
43
+ "Please install it with: pip install reportlab"
44
+ )
45
+
46
+ # Calculate statistics
47
+ stats = calculate_statistics(data)
48
+
49
+ # Create PDF
50
+ pdf_filename = os.path.join(
51
+ output_dir, f"opik_usage_report_{data['workspace']}.pdf"
52
+ )
53
+ doc = SimpleDocTemplate(pdf_filename, pagesize=letter)
54
+ story = []
55
+
56
+ # Get styles
57
+ styles = getSampleStyleSheet()
58
+ title_style = ParagraphStyle(
59
+ "CustomTitle",
60
+ parent=styles["Heading1"],
61
+ fontSize=24,
62
+ textColor=colors.HexColor("#1a1a1a"),
63
+ spaceAfter=30,
64
+ alignment=1, # Center alignment
65
+ )
66
+ heading_style = ParagraphStyle(
67
+ "CustomHeading",
68
+ parent=styles["Heading2"],
69
+ fontSize=16,
70
+ textColor=colors.HexColor("#2c3e50"),
71
+ spaceAfter=12,
72
+ )
73
+
74
+ # Title page / First page with statistics
75
+ story.append(Paragraph("Opik Usage Report", title_style))
76
+ story.append(Spacer(1, 0.3 * inch))
77
+
78
+ # Statistics section
79
+ story.append(Paragraph("Summary Statistics", heading_style))
80
+ story.append(Spacer(1, 0.1 * inch))
81
+
82
+ # Format dates for display
83
+ extraction_date_str = "N/A"
84
+ if stats["extraction_date"]:
85
+ try:
86
+ extraction_date_str = stats["extraction_date"][:10]
87
+ except (TypeError, IndexError):
88
+ extraction_date_str = (
89
+ str(stats["extraction_date"])[:10]
90
+ if stats["extraction_date"]
91
+ else "N/A"
92
+ )
93
+
94
+ start_date_str = "N/A"
95
+ end_date_str = "N/A"
96
+ if stats["date_range"].get("start"):
97
+ try:
98
+ start_date_str = stats["date_range"]["start"][:10]
99
+ except (TypeError, IndexError):
100
+ start_date_str = (
101
+ str(stats["date_range"]["start"])[:10]
102
+ if stats["date_range"]["start"]
103
+ else "N/A"
104
+ )
105
+ if stats["date_range"].get("end"):
106
+ try:
107
+ end_date_str = stats["date_range"]["end"][:10]
108
+ except (TypeError, IndexError):
109
+ end_date_str = (
110
+ str(stats["date_range"]["end"])[:10]
111
+ if stats["date_range"]["end"]
112
+ else "N/A"
113
+ )
114
+
115
+ # Create statistics table
116
+ stats_data = [
117
+ ["Workspace", stats["workspace"]],
118
+ ["Extraction Date", extraction_date_str],
119
+ ["Date Range", f"{start_date_str} to {end_date_str}"],
120
+ ["Aggregation Unit", stats["unit"].capitalize()],
121
+ ["", ""], # Separator row
122
+ ["Total Projects", str(stats["total_projects"])],
123
+ ["Projects with Data", str(stats["projects_with_data"])],
124
+ ["Periods with Data", str(stats["periods_with_data"])],
125
+ ["", ""], # Separator row
126
+ ["Total Experiments", f"{stats['total_experiments']:,}"],
127
+ ["Total Datasets", f"{stats['total_datasets']:,}"],
128
+ ["Total Traces", f"{stats['total_traces']:,.0f}"],
129
+ ["Total Spans", f"{stats['total_spans']:,.0f}"],
130
+ ["Total Tokens", f"{stats['total_tokens']:,.0f}"],
131
+ ["Total Cost", f"${stats['total_cost']:,.2f}"],
132
+ ]
133
+
134
+ stats_table = Table(stats_data, colWidths=[2.5 * inch, 4 * inch])
135
+ stats_table.setStyle(
136
+ TableStyle(
137
+ [
138
+ ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#ecf0f1")),
139
+ ("TEXTCOLOR", (0, 0), (-1, -1), colors.HexColor("#2c3e50")),
140
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
141
+ ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
142
+ ("FONTNAME", (1, 0), (1, -1), "Helvetica"),
143
+ ("FONTSIZE", (0, 0), (-1, -1), 10),
144
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
145
+ ("TOPPADDING", (0, 0), (-1, -1), 8),
146
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#bdc3c7")),
147
+ ]
148
+ )
149
+ )
150
+
151
+ story.append(stats_table)
152
+ story.append(PageBreak())
153
+
154
+ # Create individual charts and add to PDF
155
+ chart_types = [
156
+ ("trace_count", "Trace Count"),
157
+ ("span_count", "Span Count"),
158
+ ("token_count", "Token Count"),
159
+ ("cost", "Cost"),
160
+ ("experiment_count", "Experiment Count"),
161
+ ("dataset_count", "Dataset Count"),
162
+ ]
163
+
164
+ chart_files_to_cleanup = [] # Keep track of files to delete after PDF is built
165
+
166
+ for chart_type, chart_title in chart_types:
167
+ try:
168
+ chart_path = create_individual_chart(data, chart_type, output_dir)
169
+ if chart_path:
170
+ # Ensure path is absolute
171
+ chart_path = os.path.abspath(chart_path)
172
+ # Double-check file exists and is readable
173
+ if os.path.exists(chart_path) and os.access(chart_path, os.R_OK):
174
+ # Add chart title
175
+ story.append(Paragraph(chart_title, heading_style))
176
+ story.append(Spacer(1, 0.1 * inch))
177
+
178
+ # Add chart image (legend is already included in the chart image below the chart)
179
+ try:
180
+ # Use absolute path and verify file is readable
181
+ if not os.path.exists(chart_path):
182
+ console.print(
183
+ f"[yellow]Warning: Chart file disappeared: {chart_path}[/yellow]"
184
+ )
185
+ continue
186
+
187
+ # All charts are exactly 14x8 inches (4200x2400 pixels at 300 DPI)
188
+ # Scale to fit page with margins
189
+ # Aspect ratio: 14/8 = 1.75 (always wider than tall)
190
+ max_width = 7.5 * inch # Leave margin
191
+ chart_aspect_ratio = 14.0 / 8.0 # 1.75
192
+
193
+ # Charts are always wider than tall, so always scale by width
194
+ display_width = max_width
195
+ display_height = max_width / chart_aspect_ratio
196
+
197
+ # All charts use the same dimensions, so use fixed scaling
198
+ img = Image(
199
+ chart_path, width=display_width, height=display_height
200
+ )
201
+ story.append(img)
202
+ story.append(Spacer(1, 0.1 * inch))
203
+ story.append(PageBreak())
204
+
205
+ # Track file for cleanup after PDF is built
206
+ chart_files_to_cleanup.append(chart_path)
207
+ except Exception as img_error:
208
+ console.print(
209
+ f"[yellow]Warning: Could not add chart image {chart_title}: {img_error}[/yellow]"
210
+ )
211
+ # Try to clean up the file if we couldn't use it
212
+ try:
213
+ if os.path.exists(chart_path):
214
+ os.remove(chart_path)
215
+ except Exception:
216
+ pass
217
+ else:
218
+ console.print(
219
+ f"[yellow]Warning: Chart file not found or not readable: {chart_path}[/yellow]"
220
+ )
221
+ else:
222
+ console.print(
223
+ f"[yellow]Warning: Could not create chart: {chart_title}[/yellow]"
224
+ )
225
+ except Exception as chart_error:
226
+ console.print(
227
+ f"[yellow]Warning: Error creating chart {chart_title}: {chart_error}[/yellow]"
228
+ )
229
+ traceback.print_exc()
230
+ continue # Skip this chart and continue with others
231
+
232
+ # Build PDF (this is when reportlab actually reads the image files)
233
+ try:
234
+ doc.build(story)
235
+ finally:
236
+ # Clean up temporary chart files after PDF is built
237
+ for chart_path in chart_files_to_cleanup:
238
+ try:
239
+ if os.path.exists(chart_path):
240
+ os.remove(chart_path)
241
+ except Exception:
242
+ pass # Ignore cleanup errors
243
+
244
+ return pdf_filename
@@ -0,0 +1,78 @@
1
+ """Statistics calculation functions for usage report module."""
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ def calculate_statistics(data: Dict[str, Any]) -> Dict[str, Any]:
7
+ """
8
+ Calculate summary statistics from the usage data.
9
+
10
+ Args:
11
+ data: The extracted data dictionary
12
+
13
+ Returns:
14
+ Dictionary containing calculated statistics
15
+ """
16
+ stats = {
17
+ "workspace": data.get("workspace", "Unknown"),
18
+ "extraction_date": data.get("extraction_date", ""),
19
+ "date_range": data.get("date_range", {}),
20
+ "unit": data.get("unit", "month"),
21
+ "total_projects": len(data.get("projects", [])),
22
+ "projects_with_data": 0,
23
+ "total_experiments": 0,
24
+ "total_datasets": data.get("total_datasets", 0),
25
+ "total_traces": 0.0,
26
+ "total_spans": 0.0,
27
+ "total_tokens": 0.0,
28
+ "total_cost": 0.0,
29
+ "periods_with_data": 0,
30
+ }
31
+
32
+ projects = data.get("projects", [])
33
+ all_periods_set = set()
34
+
35
+ for project in projects:
36
+ if "metrics_by_unit" in project and "error" not in project:
37
+ stats["projects_with_data"] += 1
38
+ all_periods_set.update(project["metrics_by_unit"].keys())
39
+
40
+ for period_metrics in project["metrics_by_unit"].values():
41
+ # Trace count
42
+ trace_count = period_metrics.get("trace_count", 0)
43
+ if isinstance(trace_count, dict):
44
+ trace_count = sum(trace_count.values()) if trace_count else 0
45
+ stats["total_traces"] += float(trace_count) if trace_count else 0.0
46
+
47
+ # Span count
48
+ span_count = period_metrics.get("span_count", 0)
49
+ if isinstance(span_count, dict):
50
+ span_count = sum(span_count.values()) if span_count else 0
51
+ stats["total_spans"] += float(span_count) if span_count else 0.0
52
+
53
+ # Token count
54
+ token_count = period_metrics.get("token_count", {})
55
+ if isinstance(token_count, dict):
56
+ if "total_tokens" in token_count:
57
+ stats["total_tokens"] += float(token_count["total_tokens"])
58
+ else:
59
+ stats["total_tokens"] += (
60
+ sum(float(v) for v in token_count.values())
61
+ if token_count
62
+ else 0.0
63
+ )
64
+ else:
65
+ stats["total_tokens"] += float(token_count) if token_count else 0.0
66
+
67
+ # Cost
68
+ cost = period_metrics.get("cost", 0)
69
+ if isinstance(cost, dict):
70
+ cost = sum(cost.values()) if cost else 0
71
+ stats["total_cost"] += float(cost) if cost else 0.0
72
+
73
+ # Experiment count
74
+ experiments_by_unit = data.get("experiments_by_unit", {})
75
+ stats["total_experiments"] = sum(experiments_by_unit.values())
76
+ stats["periods_with_data"] = len(all_periods_set)
77
+
78
+ return stats
@@ -0,0 +1,235 @@
1
+ """Utility functions for usage report module."""
2
+
3
+ import datetime
4
+ from collections import defaultdict
5
+ from typing import Any, Callable, Dict, List, Optional, Tuple
6
+
7
+
8
+ def aggregate_by_unit(metrics_response: Any, unit: str = "month") -> Dict[str, float]:
9
+ """
10
+ Aggregate metrics by specified time unit.
11
+
12
+ Args:
13
+ metrics_response: ProjectMetricResponsePublic object from get_project_metrics
14
+ unit: Time unit for aggregation - "month", "week", "day", or "hour"
15
+
16
+ Returns:
17
+ Dictionary mapping time period key to total value
18
+ """
19
+ unit_data: Dict[str, float] = defaultdict(float)
20
+
21
+ if metrics_response.results:
22
+ for result in metrics_response.results:
23
+ if result.data:
24
+ for data_point in result.data:
25
+ if data_point.value is not None:
26
+ # Generate key based on unit
27
+ if unit == "month":
28
+ key = data_point.time.strftime("%Y-%m")
29
+ elif unit == "week":
30
+ # ISO week format: YYYY-Www
31
+ year, week, _ = data_point.time.isocalendar()
32
+ key = f"{year}-W{week:02d}"
33
+ elif unit == "day":
34
+ key = data_point.time.strftime("%Y-%m-%d")
35
+ elif unit == "hour":
36
+ key = data_point.time.strftime("%Y-%m-%d-%H")
37
+ else:
38
+ raise ValueError(f"Unsupported unit: {unit}")
39
+ unit_data[key] += data_point.value
40
+
41
+ return dict(unit_data)
42
+
43
+
44
+ def format_datetime_key(dt: datetime.datetime, unit: str) -> str:
45
+ """
46
+ Format a datetime object to a key string based on the specified unit.
47
+
48
+ Args:
49
+ dt: Datetime object to format
50
+ unit: Time unit - "month", "week", "day", or "hour"
51
+
52
+ Returns:
53
+ Formatted key string
54
+ """
55
+ if unit == "month":
56
+ return dt.strftime("%Y-%m")
57
+ elif unit == "week":
58
+ year, week, _ = dt.isocalendar()
59
+ return f"{year}-W{week:02d}"
60
+ elif unit == "day":
61
+ return dt.strftime("%Y-%m-%d")
62
+ elif unit == "hour":
63
+ return dt.strftime("%Y-%m-%d-%H")
64
+ else:
65
+ raise ValueError(f"Unsupported unit: {unit}")
66
+
67
+
68
+ def parse_and_normalize_datetime(
69
+ dt_str: Any, reference_tz: Optional[datetime.tzinfo]
70
+ ) -> Optional[datetime.datetime]:
71
+ """
72
+ Parse a datetime string and normalize it with respect to a reference timezone.
73
+
74
+ Args:
75
+ dt_str: Datetime string or datetime object to parse
76
+ reference_tz: Reference timezone to use for naive datetimes
77
+
78
+ Returns:
79
+ Parsed datetime object, or None if parsing fails
80
+ """
81
+ if not dt_str:
82
+ return None
83
+
84
+ try:
85
+ # If already a datetime object, return it
86
+ if isinstance(dt_str, datetime.datetime):
87
+ return dt_str
88
+
89
+ # Parse ISO format datetime string
90
+ if isinstance(dt_str, str):
91
+ # Handle with or without timezone
92
+ if "T" in dt_str:
93
+ if dt_str.endswith("Z"):
94
+ dt = datetime.datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
95
+ elif "+" in dt_str or dt_str.count("-") > 2:
96
+ dt = datetime.datetime.fromisoformat(dt_str)
97
+ else:
98
+ # Naive datetime
99
+ dt = datetime.datetime.fromisoformat(dt_str)
100
+ if reference_tz is not None:
101
+ # Make naive date timezone-aware
102
+ dt = dt.replace(tzinfo=reference_tz)
103
+ return dt
104
+ else:
105
+ # Not a valid datetime string
106
+ return None
107
+ else:
108
+ return dt_str
109
+ except (ValueError, TypeError):
110
+ return None
111
+
112
+
113
+ def normalize_timezone_for_comparison(
114
+ dt: datetime.datetime,
115
+ query_start_date: datetime.datetime,
116
+ query_end_date: datetime.datetime,
117
+ ) -> Tuple[datetime.datetime, datetime.datetime, datetime.datetime]:
118
+ """
119
+ Normalize timezones for date comparison.
120
+
121
+ Args:
122
+ dt: Datetime to normalize
123
+ query_start_date: Start date for comparison
124
+ query_end_date: End date for comparison
125
+
126
+ Returns:
127
+ Tuple of (normalized_dt, normalized_start_date, normalized_end_date)
128
+ """
129
+ # Handle timezone differences
130
+ if dt.tzinfo is None and query_start_date.tzinfo is not None:
131
+ dt = dt.replace(tzinfo=query_start_date.tzinfo)
132
+ start_date_aware = query_start_date
133
+ end_date_aware = query_end_date
134
+ elif dt.tzinfo is not None and query_start_date.tzinfo is None:
135
+ start_date_aware = query_start_date.replace(tzinfo=dt.tzinfo)
136
+ end_date_aware = query_end_date.replace(tzinfo=dt.tzinfo)
137
+ else:
138
+ start_date_aware = query_start_date
139
+ end_date_aware = query_end_date
140
+
141
+ return dt, start_date_aware, end_date_aware
142
+
143
+
144
+ def extract_metric_data(
145
+ projects: List[Dict[str, Any]],
146
+ all_periods: List[str],
147
+ metric_key: str,
148
+ aggregation_fn: Optional[Callable[[Any], float]] = None,
149
+ ) -> List[List[float]]:
150
+ """
151
+ Extract metric data from projects for all periods.
152
+
153
+ Args:
154
+ projects: List of project dictionaries with metrics_by_unit
155
+ all_periods: List of period keys (e.g., "2024-01", "2024-02")
156
+ metric_key: Key to extract from period_metrics (e.g., "trace_count", "cost")
157
+ aggregation_fn: Optional function to aggregate metric values.
158
+ If None, uses default: sum dict values or use scalar value.
159
+
160
+ Returns:
161
+ List of lists, where each inner list contains metric values for one period
162
+ across all projects
163
+ """
164
+ metric_data = []
165
+ for period in all_periods:
166
+ period_values = []
167
+ for project in projects:
168
+ period_metrics = project["metrics_by_unit"].get(period, {})
169
+ metric_value = period_metrics.get(metric_key, 0)
170
+
171
+ if aggregation_fn:
172
+ metric_value = aggregation_fn(metric_value)
173
+ elif isinstance(metric_value, dict):
174
+ # Default: sum all dict values
175
+ metric_value = sum(metric_value.values()) if metric_value else 0
176
+
177
+ period_values.append(float(metric_value) if metric_value else 0.0)
178
+ metric_data.append(period_values)
179
+
180
+ return metric_data
181
+
182
+
183
+ def process_experiment_for_stats(
184
+ experiment_dict: Dict[str, Any],
185
+ experiment_by_unit: Dict[str, int],
186
+ all_dates: List[datetime.datetime],
187
+ query_start_date: datetime.datetime,
188
+ query_end_date: datetime.datetime,
189
+ unit: str,
190
+ start_date: Optional[datetime.datetime],
191
+ ) -> Tuple[int, int, int]:
192
+ """
193
+ Process a single experiment dictionary and update statistics.
194
+
195
+ Returns:
196
+ Tuple of (in_range_count, without_date_count, outside_range_count)
197
+ """
198
+ in_range = 0
199
+ without_date = 0
200
+ outside_range = 0
201
+
202
+ # Extract created_at from raw dict (handles missing fields gracefully)
203
+ created_at_str = experiment_dict.get("created_at")
204
+ if created_at_str:
205
+ # Parse datetime using helper function
206
+ reference_tz = start_date.tzinfo if start_date else None
207
+ exp_date = parse_and_normalize_datetime(created_at_str, reference_tz)
208
+
209
+ if exp_date is None:
210
+ without_date = 1
211
+ else:
212
+ # Normalize timezones for comparison
213
+ exp_date, start_date_aware, end_date_aware = (
214
+ normalize_timezone_for_comparison(
215
+ exp_date, query_start_date, query_end_date
216
+ )
217
+ )
218
+
219
+ # Check if within date range
220
+ if exp_date.tzinfo is not None:
221
+ date_check = start_date_aware <= exp_date <= end_date_aware
222
+ else:
223
+ date_check = query_start_date <= exp_date <= query_end_date
224
+
225
+ if date_check:
226
+ in_range = 1
227
+ all_dates.append(exp_date)
228
+ unit_key = format_datetime_key(exp_date, unit)
229
+ experiment_by_unit[unit_key] += 1
230
+ else:
231
+ outside_range = 1
232
+ else:
233
+ without_date = 1
234
+
235
+ return (in_range, without_date, outside_range)
opik/config.py CHANGED
@@ -7,7 +7,6 @@ import pathlib
7
7
  import urllib.parse
8
8
  from typing import Any, Dict, Final, List, Literal, Optional, Tuple, Type, Union
9
9
 
10
- import opik.decorator.tracing_runtime_config as tracing_runtime_config
11
10
  import pydantic
12
11
  import pydantic_settings
13
12
  from pydantic_settings import BaseSettings, InitSettingsSource
@@ -213,7 +212,7 @@ class OpikConfig(pydantic_settings.BaseSettings):
213
212
  Timeout for guardrail.validate calls in seconds. If response takes more than this, it will be considered failed and raises an Exception.
214
213
  """
215
214
 
216
- maximal_queue_size: int = 100_000
215
+ maximal_queue_size: int = 1_000_000
217
216
  """
218
217
  Specifies the maximum number of messages that can be queued for delivery when a connection error occurs or rate limiting is in effect.
219
218
  """
@@ -228,6 +227,17 @@ class OpikConfig(pydantic_settings.BaseSettings):
228
227
  For shorter traces/spans, it is recommended to keep this setting disabled to minimize data logging overhead.
229
228
  """
230
229
 
230
+ min_base64_embedded_attachment_size: int = 256_000
231
+ """
232
+ Minimum size of the attachment string in bytes that will be kept embedded in the base64 string. (250KB)
233
+ Attachments larger than this size will be extracted from inputs/outputs of spans/traces and uploaded to the Opik backend.
234
+ """
235
+
236
+ is_attachment_extraction_active: bool = False
237
+ """
238
+ If set to True, attachments larger than `min_base64_embedded_attachment_size` will be extracted from spans/traces and uploaded to the Opik backend.
239
+ """
240
+
231
241
  @property
232
242
  def config_file_fullpath(self) -> pathlib.Path:
233
243
  config_file_path = os.getenv("OPIK_CONFIG_PATH", CONFIG_FILE_PATH_DEFAULT)
@@ -257,12 +267,8 @@ class OpikConfig(pydantic_settings.BaseSettings):
257
267
  def guardrails_backend_host(self) -> str:
258
268
  return url_helpers.get_base_url(self.url_override) + "guardrails/"
259
269
 
260
- @property
261
- def runtime(self) -> tracing_runtime_config.TracingRuntimeConfig:
262
- return tracing_runtime_config.runtime_cfg
263
-
264
270
  @pydantic.model_validator(mode="after")
265
- def _set_url_override_from_api_key(self) -> "OpikConfig":
271
+ def _set_url_override_from_api_key(self) -> OpikConfig:
266
272
  url_was_not_provided = (
267
273
  "url_override" not in self.model_fields_set or self.url_override is None
268
274
  )
@@ -103,6 +103,8 @@ class OpikConfigurator:
103
103
  self.current_config.config_file_fullpath,
104
104
  )
105
105
 
106
+ self._log_project_configuration_message()
107
+
106
108
  def _configure_local(self) -> None:
107
109
  """
108
110
  Configure the local Opik instance by setting the local URL and workspace.
@@ -119,6 +121,7 @@ class OpikConfigurator:
119
121
  # Step 1: If the URL is provided and active, update the configuration
120
122
  if url_was_provided and opik_rest_helpers.is_instance_active(self.base_url):
121
123
  self._update_config(save_to_file=self.force)
124
+ self._log_project_configuration_message()
122
125
  return
123
126
 
124
127
  # Step 2: Check if the default local instance is active
@@ -130,6 +133,7 @@ class OpikConfigurator:
130
133
  LOGGER.info(
131
134
  f"Opik is already configured to local instance at {OPIK_BASE_URL_LOCAL}."
132
135
  )
136
+ self._log_project_configuration_message()
133
137
  return
134
138
 
135
139
  # Step 3: Ask user if they want to use the found local instance
@@ -149,6 +153,7 @@ class OpikConfigurator:
149
153
  if use_url:
150
154
  self.base_url = OPIK_BASE_URL_LOCAL
151
155
  self._update_config()
156
+ self._log_project_configuration_message()
152
157
  return
153
158
 
154
159
  # Step 4: Ask user for URL if no valid local instance is found or approved
@@ -158,6 +163,7 @@ class OpikConfigurator:
158
163
  )
159
164
  self._ask_for_url()
160
165
  self._update_config()
166
+ self._log_project_configuration_message()
161
167
 
162
168
  def _set_api_key(self) -> bool:
163
169
  """
@@ -405,6 +411,17 @@ class OpikConfigurator:
405
411
  LOGGER.error(f"Failed to update config: {str(e)}")
406
412
  raise ConfigurationError("Failed to update configuration.")
407
413
 
414
+ def _log_project_configuration_message(self) -> None:
415
+ """
416
+ Log an informative message about project configuration after successful setup.
417
+ """
418
+ project_name = self.current_config.project_name
419
+
420
+ LOGGER.info(
421
+ f"Configuration completed successfully. Traces will be logged to '{project_name}' project. "
422
+ "To change the destination project, see: https://www.comet.com/docs/opik/tracing/log_traces#configuring-the-project-name"
423
+ )
424
+
408
425
  def _ask_for_url(self) -> None:
409
426
  """
410
427
  Prompt the user for an Opik instance URL and check if it is accessible.
opik/datetime_helpers.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ from typing import Optional
2
3
 
3
4
 
4
5
  def local_timestamp() -> datetime.datetime:
@@ -8,3 +9,14 @@ def local_timestamp() -> datetime.datetime:
8
9
 
9
10
  def datetime_to_iso8601(value: datetime.datetime) -> str:
10
11
  return value.isoformat()
12
+
13
+
14
+ def parse_iso_timestamp(timestamp_str: Optional[str]) -> Optional[datetime.datetime]:
15
+ """Parse an ISO 8601 timestamp string to datetime."""
16
+ if timestamp_str is None:
17
+ return None
18
+ try:
19
+ timestamp_str = timestamp_str.replace("Z", "+00:00")
20
+ return datetime.datetime.fromisoformat(timestamp_str)
21
+ except (ValueError, TypeError):
22
+ return None