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,749 @@
1
+ """Data extraction functions for usage report module."""
2
+
3
+ import datetime
4
+ import json
5
+ import os
6
+ import traceback
7
+ from collections import defaultdict
8
+ from datetime import timezone
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import opik
12
+ from rich.console import Console
13
+ from tqdm import tqdm
14
+
15
+ from .constants import MAX_PAGINATION_PAGES, MAX_TRACE_RESULTS
16
+ from .utils import (
17
+ aggregate_by_unit,
18
+ format_datetime_key,
19
+ normalize_timezone_for_comparison,
20
+ process_experiment_for_stats,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ def extract_project_data(
27
+ workspace: str,
28
+ api_key: Optional[str] = None,
29
+ start_date: Optional[datetime.datetime] = None,
30
+ end_date: Optional[datetime.datetime] = None,
31
+ unit: str = "month",
32
+ ) -> Dict[str, Any]:
33
+ """
34
+ Extract project data from Opik for a specific workspace.
35
+
36
+ Args:
37
+ workspace: Workspace name
38
+ api_key: Opik API key (optional, will use environment/config if not provided)
39
+ start_date: Start date for data extraction (None to auto-detect from data)
40
+ end_date: End date for data extraction (None to auto-detect from data)
41
+ unit: Time unit for aggregation - "month", "week", "day", or "hour". Defaults to "month".
42
+
43
+ Returns:
44
+ Dictionary containing all extracted data
45
+ """
46
+ # If dates are None, we'll collect all data and determine the range afterwards
47
+ auto_detect_start = start_date is None
48
+ auto_detect_end = end_date is None
49
+
50
+ # Use wide date ranges to capture all data when auto-detecting
51
+ query_start_date = start_date
52
+ if query_start_date is None:
53
+ # Use environment variable OPIK_DEFAULT_START_DATE if set, else use start of current year
54
+ env_start_date = os.environ.get("OPIK_DEFAULT_START_DATE")
55
+ if env_start_date:
56
+ try:
57
+ query_start_date = datetime.datetime.strptime(
58
+ env_start_date, "%Y-%m-%d"
59
+ )
60
+ except ValueError:
61
+ console.print(
62
+ "[yellow]Warning: Invalid OPIK_DEFAULT_START_DATE format. Using start of current year.[/yellow]"
63
+ )
64
+ query_start_date = datetime.datetime(datetime.datetime.now().year, 1, 1)
65
+ else:
66
+ query_start_date = datetime.datetime(datetime.datetime.now().year, 1, 1)
67
+
68
+ query_end_date = end_date
69
+ if query_end_date is None:
70
+ # Use a future date to ensure we get all data
71
+ query_end_date = datetime.datetime.now() + datetime.timedelta(days=1)
72
+
73
+ console.print(f"[blue]Workspace: {workspace}[/blue]")
74
+ if auto_detect_start or auto_detect_end:
75
+ date_msg = "Date range will be auto-detected from collected data"
76
+ if auto_detect_start and not auto_detect_end and end_date:
77
+ date_msg += f" (end date: {end_date.strftime('%Y-%m-%d')})"
78
+ elif not auto_detect_start and auto_detect_end and start_date:
79
+ date_msg += f" (start date: {start_date.strftime('%Y-%m-%d')})"
80
+ console.print(f"[blue]{date_msg}[/blue]")
81
+ else:
82
+ if start_date and end_date:
83
+ console.print(
84
+ f"[blue]Extracting data from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}[/blue]"
85
+ )
86
+ console.print(f"[blue]Aggregating by: {unit}[/blue]\n")
87
+
88
+ # Initialize client for the workspace
89
+ if api_key:
90
+ client = opik.Opik(api_key=api_key, workspace=workspace)
91
+ else:
92
+ client = opik.Opik(workspace=workspace)
93
+
94
+ # Get projects for this workspace
95
+ console.print("[blue]Getting projects...[/blue]")
96
+ with tqdm(total=1, desc="Fetching projects", unit="page", leave=False) as pbar:
97
+ projects_page = client.rest_client.projects.find_projects(size=1000)
98
+ projects = projects_page.content or []
99
+ pbar.update(1)
100
+ console.print(f"[blue]Found {len(projects)} project(s)[/blue]\n")
101
+
102
+ # Track all dates collected for auto-detection
103
+ all_dates: List[datetime.datetime] = []
104
+
105
+ all_data: Dict[str, Any] = {
106
+ "workspace": workspace,
107
+ "extraction_date": datetime.datetime.now().isoformat(),
108
+ "date_range": {"start": None, "end": None},
109
+ "unit": unit,
110
+ "experiments_by_unit": {},
111
+ "datasets_by_unit": {},
112
+ "total_datasets": 0,
113
+ "projects": [],
114
+ }
115
+
116
+ # Get experiment counts by unit (workspace-level)
117
+ experiment_by_unit: Dict[str, int] = defaultdict(int)
118
+ total_experiments_processed = 0
119
+ total_experiments_in_range = 0
120
+ experiments_without_date = 0
121
+ experiments_outside_range = 0
122
+
123
+ # Get dataset counts (workspace-level)
124
+ dataset_by_unit: Dict[str, int] = defaultdict(int)
125
+ total_datasets_processed = 0
126
+ total_datasets_in_range = 0
127
+ datasets_without_date = 0
128
+ datasets_outside_range = 0
129
+
130
+ try:
131
+ page = 1 # API uses 1-indexed pagination
132
+ total_datasets = None
133
+
134
+ # First, get total count to set up progress bar
135
+ datasets_page = client.rest_client.datasets.find_datasets(page=1, size=1000)
136
+ total_datasets = datasets_page.total or 0
137
+
138
+ # Reset page to 1 for the main loop
139
+ page = 1
140
+
141
+ with tqdm(
142
+ total=total_datasets,
143
+ desc="Processing datasets",
144
+ unit="dataset",
145
+ leave=False,
146
+ ) as pbar:
147
+ while True:
148
+ datasets_page = client.rest_client.datasets.find_datasets(
149
+ page=page, size=1000
150
+ )
151
+
152
+ datasets_list = datasets_page.content or []
153
+
154
+ if not datasets_list or len(datasets_list) == 0:
155
+ break
156
+
157
+ # Count datasets by month based on created_at
158
+ for dataset in datasets_list:
159
+ total_datasets_processed += 1
160
+
161
+ if dataset.created_at:
162
+ dataset_date = dataset.created_at
163
+
164
+ # Normalize timezones for comparison
165
+ dataset_date, start_date_aware, end_date_aware = (
166
+ normalize_timezone_for_comparison(
167
+ dataset_date, query_start_date, query_end_date
168
+ )
169
+ )
170
+
171
+ # Check if within date range
172
+ if dataset_date.tzinfo is not None:
173
+ date_check = (
174
+ start_date_aware <= dataset_date <= end_date_aware
175
+ )
176
+ else:
177
+ date_check = (
178
+ query_start_date <= dataset_date <= query_end_date
179
+ )
180
+
181
+ if date_check:
182
+ total_datasets_in_range += 1
183
+ all_dates.append(dataset_date)
184
+ unit_key = format_datetime_key(dataset_date, unit)
185
+ dataset_by_unit[unit_key] += 1
186
+ else:
187
+ datasets_outside_range += 1
188
+ else:
189
+ datasets_without_date += 1
190
+
191
+ # Update progress bar
192
+ pbar.update(1)
193
+
194
+ # Check if there are more pages
195
+ if total_datasets and page * 1000 >= total_datasets:
196
+ break
197
+ if len(datasets_list) == 0:
198
+ break
199
+
200
+ page += 1
201
+
202
+ # Safety check to avoid infinite loops
203
+ if page > MAX_PAGINATION_PAGES:
204
+ console.print(
205
+ f"[yellow] Warning: Stopped pagination after {MAX_PAGINATION_PAGES} pages to avoid infinite loop[/yellow]"
206
+ )
207
+ break
208
+
209
+ except Exception as e:
210
+ console.print(f"[yellow]Warning: Could not get dataset counts: {e}[/yellow]")
211
+ traceback.print_exc()
212
+
213
+ all_data["datasets_by_unit"] = dict(dataset_by_unit)
214
+ all_data["total_datasets"] = total_datasets_processed
215
+
216
+ # Get all existing (non-deleted) dataset names for filtering experiments
217
+ # The UI only shows experiments whose datasets still exist
218
+ console.print("[blue]Getting existing datasets for filtering...[/blue]")
219
+ existing_dataset_names = set()
220
+ try:
221
+ datasets_page = client.rest_client.datasets.find_datasets(page=1, size=1000)
222
+ existing_dataset_names = {ds.name for ds in (datasets_page.content or [])}
223
+ page = 2
224
+ while datasets_page.content and len(datasets_page.content) > 0:
225
+ datasets_page = client.rest_client.datasets.find_datasets(
226
+ page=page, size=1000
227
+ )
228
+ if datasets_page.content:
229
+ existing_dataset_names.update({ds.name for ds in datasets_page.content})
230
+ if not datasets_page.content or len(datasets_page.content) < 1000:
231
+ break
232
+ page += 1
233
+ console.print(
234
+ f"[blue]Found {len(existing_dataset_names)} existing dataset(s)[/blue]\n"
235
+ )
236
+ except Exception as e:
237
+ console.print(
238
+ f"[yellow]Warning: Could not get datasets for filtering: {e}[/yellow]"
239
+ )
240
+ console.print(
241
+ "[yellow]Will count all experiments (may include those with deleted datasets)[/yellow]\n"
242
+ )
243
+
244
+ # Get experiment counts by unit (workspace-level)
245
+ try:
246
+ # Use REST client method (handles parameters correctly)
247
+ # Filter by type="regular" to match UI behavior (UI only shows regular experiments)
248
+ # Note: types parameter needs to be JSON-encoded array string
249
+ try:
250
+ test_page = client.rest_client.experiments.find_experiments(
251
+ page=1,
252
+ size=1000,
253
+ types=json.dumps(
254
+ ["regular"]
255
+ ), # Filter to only regular experiments (matches UI)
256
+ dataset_deleted=False, # Filter out experiments with deleted datasets
257
+ )
258
+ total_experiments = test_page.total or 0
259
+ except Exception as api_error:
260
+ # Handle Pydantic validation errors from malformed API responses
261
+ error_str = str(api_error)
262
+ if "dataset_name" in error_str and (
263
+ "Field required" in error_str or "missing" in error_str.lower()
264
+ ):
265
+ # Try to get raw response to get total count
266
+ try:
267
+ httpx_client = client.rest_client._client_wrapper.httpx_client
268
+ response = httpx_client.request(
269
+ "v1/private/experiments",
270
+ method="GET",
271
+ params={
272
+ "page": 1,
273
+ "size": 1000,
274
+ "types": json.dumps(["regular"]),
275
+ "dataset_deleted": False,
276
+ },
277
+ )
278
+ if response.status_code >= 200 and response.status_code < 300:
279
+ response_data = response.json()
280
+ total_experiments = response_data.get("total", 0)
281
+ else:
282
+ total_experiments = 0
283
+ except Exception:
284
+ total_experiments = 0
285
+ else:
286
+ # Re-raise other errors
287
+ raise api_error
288
+
289
+ page = 1 # API uses 1-indexed pagination
290
+
291
+ # Note: total_experiments should now match UI count since we filter by type="regular"
292
+ # We also filter client-side for deleted datasets as a safety measure
293
+ with tqdm(
294
+ total=total_experiments,
295
+ desc="Processing experiments (regular type, filtering deleted datasets)",
296
+ unit="experiment",
297
+ leave=False,
298
+ ) as pbar:
299
+ while True:
300
+ # Use REST client method (handles parameters correctly)
301
+ try:
302
+ experiments_page = client.rest_client.experiments.find_experiments(
303
+ page=page,
304
+ size=1000,
305
+ types=json.dumps(
306
+ ["regular"]
307
+ ), # Filter to only regular experiments (matches UI)
308
+ dataset_deleted=False, # Filter out experiments with deleted datasets
309
+ )
310
+ experiments_list = experiments_page.content or []
311
+ except Exception as api_error:
312
+ # Handle Pydantic validation errors from malformed API responses
313
+ # Some experiments may be missing required fields like dataset_name
314
+ error_str = str(api_error)
315
+ if "dataset_name" in error_str and (
316
+ "Field required" in error_str or "missing" in error_str.lower()
317
+ ):
318
+ # Try to get raw response and manually filter out invalid experiments
319
+ try:
320
+ httpx_client = (
321
+ client.rest_client._client_wrapper.httpx_client
322
+ )
323
+ response = httpx_client.request(
324
+ "v1/private/experiments",
325
+ method="GET",
326
+ params={
327
+ "page": page,
328
+ "size": 1000,
329
+ "types": json.dumps(["regular"]),
330
+ "dataset_deleted": False,
331
+ },
332
+ )
333
+ if (
334
+ response.status_code >= 200
335
+ and response.status_code < 300
336
+ ):
337
+ response_data = response.json()
338
+ experiments_list = response_data.get("content", [])
339
+ # Note: We process experiments even if they're missing dataset_name
340
+ # since process_experiment_for_stats only needs created_at
341
+ else:
342
+ # If raw request also fails, try with smaller page size as fallback
343
+ console.print(
344
+ f"[yellow] Warning: Could not fetch page {page} (HTTP {response.status_code}). Trying smaller page size...[/yellow]"
345
+ )
346
+ try:
347
+ # Try with smaller page size to potentially avoid the problematic experiment
348
+ small_response = httpx_client.request(
349
+ "v1/private/experiments",
350
+ method="GET",
351
+ params={
352
+ "page": page,
353
+ "size": 100, # Smaller page size
354
+ "types": json.dumps(["regular"]),
355
+ "dataset_deleted": False,
356
+ },
357
+ )
358
+ if (
359
+ small_response.status_code >= 200
360
+ and small_response.status_code < 300
361
+ ):
362
+ small_response_data = small_response.json()
363
+ experiments_list = small_response_data.get(
364
+ "content", []
365
+ )
366
+ console.print(
367
+ f"[yellow] Successfully fetched page {page} with smaller page size. Got {len(experiments_list)} experiment(s).[/yellow]"
368
+ )
369
+ else:
370
+ # If smaller page size also fails, skip this page
371
+ console.print(
372
+ f"[yellow] Warning: Could not fetch page {page} even with smaller page size. Skipping page (may lose some experiments).[/yellow]"
373
+ )
374
+ experiments_list = []
375
+ page += 1
376
+ continue
377
+ except Exception:
378
+ # If smaller page size request fails, skip this page
379
+ console.print(
380
+ f"[yellow] Warning: Could not fetch page {page} even with smaller page size. Skipping page (may lose some experiments).[/yellow]"
381
+ )
382
+ experiments_list = []
383
+ page += 1
384
+ continue
385
+ except Exception as raw_error:
386
+ # If raw request fails, try smaller page size as last resort
387
+ console.print(
388
+ f"[yellow] Warning: Could not fetch page {page} due to error: {raw_error}. Trying smaller page size...[/yellow]"
389
+ )
390
+ try:
391
+ httpx_client = (
392
+ client.rest_client._client_wrapper.httpx_client
393
+ )
394
+ small_response = httpx_client.request(
395
+ "v1/private/experiments",
396
+ method="GET",
397
+ params={
398
+ "page": page,
399
+ "size": 100, # Smaller page size
400
+ "types": json.dumps(["regular"]),
401
+ "dataset_deleted": False,
402
+ },
403
+ )
404
+ if (
405
+ small_response.status_code >= 200
406
+ and small_response.status_code < 300
407
+ ):
408
+ small_response_data = small_response.json()
409
+ experiments_list = small_response_data.get(
410
+ "content", []
411
+ )
412
+ console.print(
413
+ f"[yellow] Successfully fetched page {page} with smaller page size. Got {len(experiments_list)} experiment(s).[/yellow]"
414
+ )
415
+ else:
416
+ # If smaller page size also fails, skip this page
417
+ console.print(
418
+ f"[yellow] Warning: Could not fetch page {page} even with smaller page size. Skipping page (may lose some experiments).[/yellow]"
419
+ )
420
+ experiments_list = []
421
+ page += 1
422
+ continue
423
+ except Exception:
424
+ # If smaller page size request also fails, skip this page
425
+ console.print(
426
+ f"[yellow] Warning: Could not fetch page {page} even with smaller page size. Skipping page (may lose some experiments).[/yellow]"
427
+ )
428
+ experiments_list = []
429
+ page += 1
430
+ continue
431
+ else:
432
+ # Re-raise other errors
433
+ raise api_error
434
+
435
+ # Convert to dict format for processing
436
+ experiments_dict_list = []
437
+ for exp in experiments_list:
438
+ try:
439
+ if hasattr(exp, "model_dump"):
440
+ # Use mode='python' to get native Python types and exclude_unset to avoid validation issues
441
+ exp_dict = exp.model_dump(mode="python", exclude_unset=True)
442
+ elif hasattr(exp, "dict"):
443
+ exp_dict = exp.dict(exclude_unset=True)
444
+ else:
445
+ # Already a dict
446
+ exp_dict = exp # type: ignore[assignment]
447
+ experiments_dict_list.append(exp_dict)
448
+ except Exception as e:
449
+ # Skip experiments that can't be converted (e.g., missing required fields)
450
+ console.print(
451
+ f"[yellow] Warning: Skipping experiment due to conversion error: {e}[/yellow]"
452
+ )
453
+ continue
454
+ experiments_list = experiments_dict_list
455
+
456
+ if not experiments_list or len(experiments_list) == 0:
457
+ break
458
+
459
+ # Filter experiments to only include those with existing (non-deleted) datasets
460
+ # This matches the UI behavior - UI only shows experiments whose datasets still exist
461
+ # Note: We still process experiments without dataset_name since process_experiment_for_stats
462
+ # only needs created_at, but we filter out experiments whose datasets don't exist
463
+ filtered_experiments = []
464
+ skipped_count = 0
465
+ for experiment_dict in experiments_list:
466
+ dataset_name = experiment_dict.get("dataset_name")
467
+ # Skip experiments that have a dataset_name but the dataset doesn't exist
468
+ # (experiments without dataset_name are still processed)
469
+ if (
470
+ dataset_name
471
+ and existing_dataset_names
472
+ and dataset_name not in existing_dataset_names
473
+ ):
474
+ # Dataset doesn't exist (was deleted)
475
+ skipped_count += 1
476
+ continue
477
+ filtered_experiments.append(experiment_dict)
478
+
479
+ # Count experiments by month based on created_at
480
+ # Process all experiments (including those without dataset_name)
481
+ for experiment_dict in filtered_experiments:
482
+ total_experiments_processed += 1
483
+ in_range, without_date, outside_range = (
484
+ process_experiment_for_stats(
485
+ experiment_dict,
486
+ experiment_by_unit,
487
+ all_dates,
488
+ query_start_date,
489
+ query_end_date,
490
+ unit,
491
+ start_date,
492
+ )
493
+ )
494
+ total_experiments_in_range += in_range
495
+ experiments_without_date += without_date
496
+ experiments_outside_range += outside_range
497
+ pbar.update(1)
498
+
499
+ # Check if there are more pages
500
+ # Note: page is 1-indexed, so page 1 = items 0-999, page 2 = items 1000-1999, etc.
501
+ if total_experiments and page * 1000 >= total_experiments:
502
+ break
503
+ if len(experiments_list) == 0:
504
+ break
505
+
506
+ page += 1
507
+
508
+ # Safety check to avoid infinite loops
509
+ if page > MAX_PAGINATION_PAGES:
510
+ console.print(
511
+ f"[yellow] Warning: Stopped pagination after {MAX_PAGINATION_PAGES} pages to avoid infinite loop[/yellow]"
512
+ )
513
+ break
514
+
515
+ except Exception as e:
516
+ console.print(f"[yellow]Warning: Could not get experiment counts: {e}[/yellow]")
517
+ traceback.print_exc()
518
+
519
+ all_data["experiments_by_unit"] = dict(experiment_by_unit)
520
+
521
+ # Process each project
522
+ with tqdm(total=len(projects), desc="Processing projects", unit="project") as pbar:
523
+ for project in projects:
524
+ project_id = project.id
525
+ project_name = project.name
526
+
527
+ # Pad project name to fixed width to prevent progress bar from jumping
528
+ # Truncate to 30 chars and pad to 30 chars for consistent width
529
+ display_name = (project_name[:30] + " " * 30)[:30]
530
+ pbar.set_description(f"Processing {display_name}")
531
+
532
+ project_data = {
533
+ "project_id": project_id,
534
+ "project_name": project_name,
535
+ "metrics_by_unit": {},
536
+ }
537
+
538
+ try:
539
+ # Get trace counts
540
+ trace_response = client.rest_client.projects.get_project_metrics(
541
+ id=project_id,
542
+ metric_type="TRACE_COUNT",
543
+ interval="DAILY",
544
+ interval_start=query_start_date,
545
+ interval_end=query_end_date,
546
+ )
547
+ trace_by_unit = aggregate_by_unit(trace_response, unit)
548
+ # Track dates from metrics
549
+ if trace_response.results:
550
+ for result in trace_response.results:
551
+ if result.data:
552
+ for data_point in result.data:
553
+ if data_point.value is not None:
554
+ all_dates.append(data_point.time)
555
+
556
+ # Get token counts
557
+ token_response = client.rest_client.projects.get_project_metrics(
558
+ id=project_id,
559
+ metric_type="TOKEN_USAGE",
560
+ interval="DAILY",
561
+ interval_start=query_start_date,
562
+ interval_end=query_end_date,
563
+ )
564
+ # Token usage has multiple result types (total_tokens, prompt_tokens, etc.)
565
+ # We'll aggregate all of them
566
+ token_by_unit: Dict[str, Dict[str, float]] = defaultdict(
567
+ lambda: defaultdict(float)
568
+ )
569
+ if token_response.results:
570
+ for result in token_response.results:
571
+ token_type = result.name or "unknown"
572
+ for data_point in result.data or []:
573
+ if data_point.value is not None:
574
+ all_dates.append(data_point.time)
575
+ unit_key = format_datetime_key(data_point.time, unit)
576
+ token_by_unit[unit_key][token_type] += data_point.value
577
+
578
+ # Get cost
579
+ cost_response = client.rest_client.projects.get_project_metrics(
580
+ id=project_id,
581
+ metric_type="COST",
582
+ interval="DAILY",
583
+ interval_start=query_start_date,
584
+ interval_end=query_end_date,
585
+ )
586
+ cost_by_unit = aggregate_by_unit(cost_response, unit)
587
+ # Track dates from metrics
588
+ if cost_response.results:
589
+ for result in cost_response.results:
590
+ if result.data:
591
+ for data_point in result.data:
592
+ if data_point.value is not None:
593
+ all_dates.append(data_point.time)
594
+
595
+ # Get span counts by getting all traces and using their span_count field
596
+ span_by_unit: Dict[str, int] = defaultdict(int)
597
+ try:
598
+ # Get all traces for this project within the date range
599
+ # Use a filter string to limit by date range
600
+ filter_string = None
601
+ if query_start_date and query_end_date:
602
+ # Format dates for filter (ISO 8601 format with timezone)
603
+ # API expects format like "2024-01-01T00:00:00Z"
604
+ def format_date_for_filter(dt: datetime.datetime) -> str:
605
+ """Format datetime for filter string with timezone."""
606
+ if dt.tzinfo is None:
607
+ # Naive datetime - assume UTC and add Z
608
+ return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
609
+ else:
610
+ # Timezone-aware - convert to UTC and format
611
+ utc_dt = dt.astimezone(timezone.utc)
612
+ return utc_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
613
+
614
+ start_str = format_date_for_filter(query_start_date)
615
+ end_str = format_date_for_filter(query_end_date)
616
+ filter_string = (
617
+ f'start_time >= "{start_str}" AND start_time <= "{end_str}"'
618
+ )
619
+
620
+ traces = client.search_traces(
621
+ project_name=project_name,
622
+ filter_string=filter_string,
623
+ max_results=MAX_TRACE_RESULTS,
624
+ )
625
+
626
+ # For each trace, get span count
627
+ for trace in tqdm(
628
+ traces,
629
+ desc=f" Getting span counts for {project_name[:20]}",
630
+ leave=False,
631
+ unit="trace",
632
+ ):
633
+ # Try to get span count from trace object first
634
+ span_count = trace.span_count
635
+
636
+ # If span_count is not available, count spans directly
637
+ if span_count is None:
638
+ try:
639
+ spans = client.search_spans(
640
+ trace_id=trace.id,
641
+ project_name=project_name,
642
+ max_results=10000,
643
+ )
644
+ span_count = len(spans)
645
+ except Exception:
646
+ # If counting fails, default to 0
647
+ span_count = 0
648
+
649
+ span_count = span_count or 0
650
+
651
+ # Aggregate by unit based on trace start_time
652
+ if trace.start_time:
653
+ trace_date = trace.start_time
654
+
655
+ # Normalize timezones for comparison
656
+ trace_date, start_date_aware, end_date_aware = (
657
+ normalize_timezone_for_comparison(
658
+ trace_date, query_start_date, query_end_date
659
+ )
660
+ )
661
+
662
+ # Check if within date range
663
+ if trace_date.tzinfo is not None:
664
+ date_check = (
665
+ start_date_aware <= trace_date <= end_date_aware
666
+ )
667
+ else:
668
+ date_check = (
669
+ query_start_date <= trace_date <= query_end_date
670
+ )
671
+
672
+ if date_check:
673
+ unit_key = format_datetime_key(trace_date, unit)
674
+ span_by_unit[unit_key] += span_count
675
+ all_dates.append(trace_date)
676
+ except Exception as e:
677
+ console.print(
678
+ f"[yellow] Warning: Could not get span counts for project {project_name}: {e}[/yellow]"
679
+ )
680
+
681
+ # Combine all metrics by unit
682
+ all_units = set(
683
+ list(trace_by_unit.keys())
684
+ + list(token_by_unit.keys())
685
+ + list(cost_by_unit.keys())
686
+ + list(span_by_unit.keys())
687
+ )
688
+
689
+ for unit_key in sorted(all_units):
690
+ project_data["metrics_by_unit"][unit_key] = {
691
+ "trace_count": trace_by_unit.get(unit_key, 0),
692
+ "token_count": dict(token_by_unit.get(unit_key, {})),
693
+ "cost": cost_by_unit.get(unit_key, 0.0),
694
+ "span_count": span_by_unit.get(unit_key, 0),
695
+ }
696
+
697
+ except Exception as e:
698
+ console.print(
699
+ f"[red] Error processing project {project_name}: {e}[/red]\n"
700
+ )
701
+ project_data["error"] = str(e)
702
+
703
+ all_data["projects"].append(project_data)
704
+ pbar.update(1)
705
+
706
+ # Determine actual date range from collected data if auto-detection was requested
707
+ if all_dates:
708
+ actual_start = min(all_dates)
709
+ actual_end = max(all_dates)
710
+
711
+ # Use provided dates where available, otherwise use detected dates
712
+ if auto_detect_start:
713
+ all_data["date_range"]["start"] = actual_start.isoformat()
714
+ else:
715
+ if start_date:
716
+ all_data["date_range"]["start"] = start_date.isoformat()
717
+
718
+ if auto_detect_end:
719
+ all_data["date_range"]["end"] = actual_end.isoformat()
720
+ else:
721
+ if end_date:
722
+ all_data["date_range"]["end"] = end_date.isoformat()
723
+
724
+ if auto_detect_start or auto_detect_end:
725
+ # Format dates nicely for display
726
+ start_str = all_data["date_range"]["start"]
727
+ end_str = all_data["date_range"]["end"]
728
+ try:
729
+ start_dt = datetime.datetime.fromisoformat(
730
+ start_str.replace("Z", "+00:00")
731
+ )
732
+ end_dt = datetime.datetime.fromisoformat(end_str.replace("Z", "+00:00"))
733
+ start_formatted = start_dt.strftime("%Y-%m-%d")
734
+ end_formatted = end_dt.strftime("%Y-%m-%d")
735
+ console.print(
736
+ f"[blue]Auto-detected date range: {start_formatted} to {end_formatted}[/blue]\n"
737
+ )
738
+ except (ValueError, AttributeError):
739
+ console.print(
740
+ f"[blue]Auto-detected date range: {start_str} to {end_str}[/blue]\n"
741
+ )
742
+ else:
743
+ # No data collected, use provided dates or None
744
+ if start_date:
745
+ all_data["date_range"]["start"] = start_date.isoformat()
746
+ if end_date:
747
+ all_data["date_range"]["end"] = end_date.isoformat()
748
+
749
+ return all_data