opik 1.9.5__py3-none-any.whl → 1.9.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. opik/__init__.py +10 -3
  2. opik/anonymizer/__init__.py +5 -0
  3. opik/anonymizer/anonymizer.py +12 -0
  4. opik/anonymizer/factory.py +80 -0
  5. opik/anonymizer/recursive_anonymizer.py +64 -0
  6. opik/anonymizer/rules.py +56 -0
  7. opik/anonymizer/rules_anonymizer.py +35 -0
  8. opik/api_objects/dataset/rest_operations.py +5 -0
  9. opik/api_objects/experiment/experiment.py +46 -49
  10. opik/api_objects/experiment/helpers.py +34 -10
  11. opik/api_objects/local_recording.py +8 -3
  12. opik/api_objects/opik_client.py +230 -48
  13. opik/api_objects/opik_query_language.py +9 -0
  14. opik/api_objects/prompt/__init__.py +11 -3
  15. opik/api_objects/prompt/base_prompt.py +69 -0
  16. opik/api_objects/prompt/base_prompt_template.py +29 -0
  17. opik/api_objects/prompt/chat/__init__.py +1 -0
  18. opik/api_objects/prompt/chat/chat_prompt.py +193 -0
  19. opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
  20. opik/api_objects/prompt/{chat_content_renderer_registry.py → chat/content_renderer_registry.py} +37 -35
  21. opik/api_objects/prompt/client.py +101 -30
  22. opik/api_objects/prompt/text/__init__.py +1 -0
  23. opik/api_objects/prompt/text/prompt.py +174 -0
  24. opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
  25. opik/api_objects/prompt/types.py +1 -1
  26. opik/cli/export.py +6 -2
  27. opik/cli/usage_report/charts.py +39 -10
  28. opik/cli/usage_report/cli.py +164 -45
  29. opik/cli/usage_report/pdf.py +14 -1
  30. opik/config.py +0 -5
  31. opik/decorator/base_track_decorator.py +37 -40
  32. opik/decorator/context_manager/span_context_manager.py +9 -0
  33. opik/decorator/context_manager/trace_context_manager.py +5 -0
  34. opik/dict_utils.py +3 -3
  35. opik/evaluation/__init__.py +13 -2
  36. opik/evaluation/engine/engine.py +195 -223
  37. opik/evaluation/engine/helpers.py +8 -7
  38. opik/evaluation/engine/metrics_evaluator.py +237 -0
  39. opik/evaluation/evaluation_result.py +35 -1
  40. opik/evaluation/evaluator.py +318 -30
  41. opik/evaluation/models/litellm/util.py +78 -6
  42. opik/evaluation/models/model_capabilities.py +33 -0
  43. opik/evaluation/report.py +14 -2
  44. opik/evaluation/rest_operations.py +36 -33
  45. opik/evaluation/test_case.py +2 -2
  46. opik/evaluation/types.py +9 -1
  47. opik/exceptions.py +17 -0
  48. opik/hooks/__init__.py +17 -1
  49. opik/hooks/anonymizer_hook.py +36 -0
  50. opik/id_helpers.py +18 -0
  51. opik/integrations/adk/helpers.py +16 -7
  52. opik/integrations/adk/legacy_opik_tracer.py +7 -4
  53. opik/integrations/adk/opik_tracer.py +3 -1
  54. opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
  55. opik/integrations/adk/recursive_callback_injector.py +1 -6
  56. opik/integrations/dspy/callback.py +1 -4
  57. opik/integrations/haystack/opik_connector.py +2 -2
  58. opik/integrations/haystack/opik_tracer.py +2 -4
  59. opik/integrations/langchain/opik_tracer.py +273 -82
  60. opik/integrations/llama_index/callback.py +110 -108
  61. opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
  62. opik/integrations/openai/opik_tracker.py +1 -1
  63. opik/message_processing/batching/batchers.py +11 -7
  64. opik/message_processing/encoder_helpers.py +79 -0
  65. opik/message_processing/messages.py +25 -1
  66. opik/message_processing/online_message_processor.py +23 -8
  67. opik/opik_context.py +7 -7
  68. opik/rest_api/__init__.py +188 -12
  69. opik/rest_api/client.py +3 -0
  70. opik/rest_api/dashboards/__init__.py +4 -0
  71. opik/rest_api/dashboards/client.py +462 -0
  72. opik/rest_api/dashboards/raw_client.py +648 -0
  73. opik/rest_api/datasets/client.py +893 -89
  74. opik/rest_api/datasets/raw_client.py +1328 -87
  75. opik/rest_api/experiments/client.py +30 -2
  76. opik/rest_api/experiments/raw_client.py +26 -0
  77. opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
  78. opik/rest_api/optimizations/client.py +302 -0
  79. opik/rest_api/optimizations/raw_client.py +463 -0
  80. opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
  81. opik/rest_api/prompts/__init__.py +2 -2
  82. opik/rest_api/prompts/client.py +34 -4
  83. opik/rest_api/prompts/raw_client.py +32 -2
  84. opik/rest_api/prompts/types/__init__.py +3 -1
  85. opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
  86. opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
  87. opik/rest_api/spans/__init__.py +0 -2
  88. opik/rest_api/spans/client.py +148 -64
  89. opik/rest_api/spans/raw_client.py +210 -83
  90. opik/rest_api/spans/types/__init__.py +0 -2
  91. opik/rest_api/traces/client.py +241 -73
  92. opik/rest_api/traces/raw_client.py +344 -90
  93. opik/rest_api/types/__init__.py +200 -15
  94. opik/rest_api/types/aggregation_data.py +1 -0
  95. opik/rest_api/types/alert_trigger_config_public_type.py +6 -1
  96. opik/rest_api/types/alert_trigger_config_type.py +6 -1
  97. opik/rest_api/types/alert_trigger_config_write_type.py +6 -1
  98. opik/rest_api/types/automation_rule_evaluator.py +23 -1
  99. opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
  100. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
  101. opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
  102. opik/rest_api/types/{automation_rule_evaluator_object_public.py → automation_rule_evaluator_object_object_public.py} +32 -10
  103. opik/rest_api/types/automation_rule_evaluator_page_public.py +2 -2
  104. opik/rest_api/types/automation_rule_evaluator_public.py +23 -1
  105. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
  106. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
  107. opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
  108. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
  109. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
  110. opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
  111. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
  112. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
  113. opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
  114. opik/rest_api/types/automation_rule_evaluator_update.py +23 -1
  115. opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
  116. opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
  117. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
  118. opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
  119. opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
  120. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
  121. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
  122. opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
  123. opik/rest_api/types/automation_rule_evaluator_write.py +23 -1
  124. opik/rest_api/types/boolean_feedback_definition.py +25 -0
  125. opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
  126. opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
  127. opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
  128. opik/rest_api/types/boolean_feedback_detail.py +29 -0
  129. opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
  130. opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
  131. opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
  132. opik/rest_api/types/dashboard_page_public.py +24 -0
  133. opik/rest_api/types/dashboard_public.py +30 -0
  134. opik/rest_api/types/dataset.py +2 -0
  135. opik/rest_api/types/dataset_item.py +2 -0
  136. opik/rest_api/types/dataset_item_compare.py +2 -0
  137. opik/rest_api/types/dataset_item_filter.py +23 -0
  138. opik/rest_api/types/dataset_item_filter_operator.py +21 -0
  139. opik/rest_api/types/dataset_item_page_compare.py +1 -0
  140. opik/rest_api/types/dataset_item_page_public.py +1 -0
  141. opik/rest_api/types/dataset_item_public.py +2 -0
  142. opik/rest_api/types/dataset_item_update.py +39 -0
  143. opik/rest_api/types/dataset_item_write.py +1 -0
  144. opik/rest_api/types/dataset_public.py +2 -0
  145. opik/rest_api/types/dataset_public_status.py +5 -0
  146. opik/rest_api/types/dataset_status.py +5 -0
  147. opik/rest_api/types/dataset_version_diff.py +22 -0
  148. opik/rest_api/types/dataset_version_diff_stats.py +24 -0
  149. opik/rest_api/types/dataset_version_page_public.py +23 -0
  150. opik/rest_api/types/dataset_version_public.py +49 -0
  151. opik/rest_api/types/experiment.py +2 -0
  152. opik/rest_api/types/experiment_public.py +2 -0
  153. opik/rest_api/types/experiment_score.py +20 -0
  154. opik/rest_api/types/experiment_score_public.py +20 -0
  155. opik/rest_api/types/experiment_score_write.py +20 -0
  156. opik/rest_api/types/feedback.py +20 -1
  157. opik/rest_api/types/feedback_create.py +16 -1
  158. opik/rest_api/types/feedback_object_public.py +22 -1
  159. opik/rest_api/types/feedback_public.py +20 -1
  160. opik/rest_api/types/feedback_score_public.py +4 -0
  161. opik/rest_api/types/feedback_update.py +16 -1
  162. opik/rest_api/types/image_url.py +20 -0
  163. opik/rest_api/types/image_url_public.py +20 -0
  164. opik/rest_api/types/image_url_write.py +20 -0
  165. opik/rest_api/types/llm_as_judge_message.py +5 -1
  166. opik/rest_api/types/llm_as_judge_message_content.py +24 -0
  167. opik/rest_api/types/llm_as_judge_message_content_public.py +24 -0
  168. opik/rest_api/types/llm_as_judge_message_content_write.py +24 -0
  169. opik/rest_api/types/llm_as_judge_message_public.py +5 -1
  170. opik/rest_api/types/llm_as_judge_message_write.py +5 -1
  171. opik/rest_api/types/llm_as_judge_model_parameters.py +2 -0
  172. opik/rest_api/types/llm_as_judge_model_parameters_public.py +2 -0
  173. opik/rest_api/types/llm_as_judge_model_parameters_write.py +2 -0
  174. opik/rest_api/types/optimization.py +2 -0
  175. opik/rest_api/types/optimization_public.py +2 -0
  176. opik/rest_api/types/optimization_public_status.py +3 -1
  177. opik/rest_api/types/optimization_status.py +3 -1
  178. opik/rest_api/types/optimization_studio_config.py +27 -0
  179. opik/rest_api/types/optimization_studio_config_public.py +27 -0
  180. opik/rest_api/types/optimization_studio_config_write.py +27 -0
  181. opik/rest_api/types/optimization_studio_log.py +22 -0
  182. opik/rest_api/types/optimization_write.py +2 -0
  183. opik/rest_api/types/optimization_write_status.py +3 -1
  184. opik/rest_api/types/prompt.py +6 -0
  185. opik/rest_api/types/prompt_detail.py +6 -0
  186. opik/rest_api/types/prompt_detail_template_structure.py +5 -0
  187. opik/rest_api/types/prompt_public.py +6 -0
  188. opik/rest_api/types/prompt_public_template_structure.py +5 -0
  189. opik/rest_api/types/prompt_template_structure.py +5 -0
  190. opik/rest_api/types/prompt_version.py +2 -0
  191. opik/rest_api/types/prompt_version_detail.py +2 -0
  192. opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
  193. opik/rest_api/types/prompt_version_public.py +2 -0
  194. opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
  195. opik/rest_api/types/prompt_version_template_structure.py +5 -0
  196. opik/rest_api/types/score_name.py +1 -0
  197. opik/rest_api/types/service_toggles_config.py +6 -0
  198. opik/rest_api/types/span_enrichment_options.py +31 -0
  199. opik/rest_api/types/span_filter.py +23 -0
  200. opik/rest_api/types/span_filter_operator.py +21 -0
  201. opik/rest_api/types/span_filter_write.py +23 -0
  202. opik/rest_api/types/span_filter_write_operator.py +21 -0
  203. opik/rest_api/types/span_llm_as_judge_code.py +27 -0
  204. opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
  205. opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
  206. opik/rest_api/types/span_update.py +46 -0
  207. opik/rest_api/types/studio_evaluation.py +20 -0
  208. opik/rest_api/types/studio_evaluation_public.py +20 -0
  209. opik/rest_api/types/studio_evaluation_write.py +20 -0
  210. opik/rest_api/types/studio_llm_model.py +21 -0
  211. opik/rest_api/types/studio_llm_model_public.py +21 -0
  212. opik/rest_api/types/studio_llm_model_write.py +21 -0
  213. opik/rest_api/types/studio_message.py +20 -0
  214. opik/rest_api/types/studio_message_public.py +20 -0
  215. opik/rest_api/types/studio_message_write.py +20 -0
  216. opik/rest_api/types/studio_metric.py +21 -0
  217. opik/rest_api/types/studio_metric_public.py +21 -0
  218. opik/rest_api/types/studio_metric_write.py +21 -0
  219. opik/rest_api/types/studio_optimizer.py +21 -0
  220. opik/rest_api/types/studio_optimizer_public.py +21 -0
  221. opik/rest_api/types/studio_optimizer_write.py +21 -0
  222. opik/rest_api/types/studio_prompt.py +20 -0
  223. opik/rest_api/types/studio_prompt_public.py +20 -0
  224. opik/rest_api/types/studio_prompt_write.py +20 -0
  225. opik/rest_api/types/trace.py +6 -0
  226. opik/rest_api/types/trace_public.py +6 -0
  227. opik/rest_api/types/trace_thread_filter_write.py +23 -0
  228. opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
  229. opik/rest_api/types/trace_thread_update.py +19 -0
  230. opik/rest_api/types/trace_update.py +39 -0
  231. opik/rest_api/types/value_entry.py +2 -0
  232. opik/rest_api/types/value_entry_compare.py +2 -0
  233. opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
  234. opik/rest_api/types/value_entry_public.py +2 -0
  235. opik/rest_api/types/video_url.py +19 -0
  236. opik/rest_api/types/video_url_public.py +19 -0
  237. opik/rest_api/types/video_url_write.py +19 -0
  238. opik/synchronization.py +5 -6
  239. opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
  240. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/METADATA +5 -4
  241. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/RECORD +246 -151
  242. opik/api_objects/prompt/chat_prompt_template.py +0 -164
  243. opik/api_objects/prompt/prompt.py +0 -131
  244. /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
  245. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/WHEEL +0 -0
  246. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/entry_points.txt +0 -0
  247. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/licenses/LICENSE +0 -0
  248. {opik-1.9.5.dist-info → opik-1.9.39.dist-info}/top_level.txt +0 -0
@@ -7,11 +7,15 @@ import sys
7
7
  import traceback
8
8
  import webbrowser
9
9
  from pathlib import Path
10
- from typing import Optional
10
+ from typing import List, Optional
11
11
 
12
12
  import click
13
13
  from rich.console import Console
14
14
 
15
+ import opik.config as config
16
+ import opik.url_helpers as url_helpers
17
+ import opik.httpx_client as httpx_client
18
+
15
19
  from .extraction import extract_project_data
16
20
  from .pdf import create_pdf_report
17
21
  from .statistics import calculate_statistics
@@ -20,7 +24,7 @@ console = Console()
20
24
 
21
25
 
22
26
  @click.command(name="usage-report")
23
- @click.argument("workspace", type=str)
27
+ @click.argument("workspaces", nargs=-1, type=str)
24
28
  @click.option(
25
29
  "--start-date",
26
30
  type=str,
@@ -50,35 +54,52 @@ console = Console()
50
54
  is_flag=True,
51
55
  help="Automatically open the generated PDF report in the default viewer.",
52
56
  )
57
+ @click.option(
58
+ "--from-json",
59
+ "from_json",
60
+ is_flag=True,
61
+ help="Load data from existing JSON files instead of extracting from API. "
62
+ "JSON files should match the output pattern (e.g., opik_usage_report.json or opik_usage_report_{workspace}.json).",
63
+ )
53
64
  @click.pass_context
54
65
  def usage_report(
55
66
  ctx: click.Context,
56
- workspace: str,
67
+ workspaces: tuple,
57
68
  start_date: Optional[str],
58
69
  end_date: Optional[str],
59
70
  unit: str,
60
71
  output: str,
61
72
  open_pdf: bool,
73
+ from_json: bool,
62
74
  ) -> None:
63
75
  """
64
- Extract Opik usage data for a workspace.
76
+ Extract Opik usage data for one or more workspaces.
65
77
 
66
- This command extracts project-level metrics from Opik for a specific workspace:
67
- - Loops through all projects in the workspace
78
+ This command extracts project-level metrics from Opik for specified workspace(s):
79
+ - Loops through all projects in each workspace
68
80
  - Gets trace count, cost, and token count
69
81
  - Gets experiment and dataset counts (workspace-level)
70
82
  - Aggregates data by the specified time unit (month, week, day, or hour)
71
83
  - Saves data to a JSON file
72
84
  - Generates a PDF report with charts and statistics
73
85
 
74
- WORKSPACE: Workspace name to extract data from.
86
+ WORKSPACES: Zero or more workspace names to extract data from.
87
+ If no workspaces are provided, all workspaces will be processed.
75
88
 
76
89
  Examples:
77
90
 
78
- Extract data with auto-detected date range, aggregated by month (default):
91
+ Extract data for a single workspace with auto-detected date range:
79
92
 
80
93
  opik usage-report my-workspace
81
94
 
95
+ Extract data for multiple workspaces:
96
+
97
+ opik usage-report workspace1 workspace2 workspace3
98
+
99
+ Extract data for all workspaces (no workspace specified):
100
+
101
+ opik usage-report
102
+
82
103
  Extract data aggregated by week:
83
104
 
84
105
  opik usage-report my-workspace --unit week
@@ -90,6 +111,10 @@ def usage_report(
90
111
  Extract data and automatically open the PDF report:
91
112
 
92
113
  opik usage-report my-workspace --open
114
+
115
+ Generate PDF from existing JSON file (skip data extraction):
116
+
117
+ opik usage-report my-workspace --from-json
93
118
  """
94
119
  try:
95
120
  # Get API key from context (set by main CLI)
@@ -104,52 +129,146 @@ def usage_report(
104
129
  if end_date:
105
130
  end_date_obj = datetime.datetime.strptime(end_date, "%Y-%m-%d")
106
131
 
107
- console.print("[green]Starting Opik data extraction...[/green]\n")
132
+ # Determine which workspaces to process
133
+ workspaces_list: List[str] = list(workspaces) if workspaces else []
108
134
 
109
- data = extract_project_data(
110
- workspace, api_key, start_date_obj, end_date_obj, unit
111
- )
135
+ # If no workspaces provided, fetch all workspaces
136
+ if not workspaces_list:
137
+ console.print(
138
+ "[blue]No workspaces specified. Fetching all workspaces...[/blue]"
139
+ )
140
+ cfg = config.OpikConfig()
141
+ # Use API key from context if available, otherwise from config
142
+ api_key_to_use = api_key or cfg.api_key
143
+ with httpx_client.get(
144
+ workspace=None, # No workspace needed when fetching workspace list
145
+ api_key=api_key_to_use,
146
+ check_tls_certificate=cfg.check_tls_certificate,
147
+ compress_json_requests=cfg.enable_json_request_compression,
148
+ ) as client:
149
+ base_url = url_helpers.get_base_url(cfg.url_override)
150
+ workspace_list_url = url_helpers.get_workspace_list_url(base_url)
151
+ response = client.get(workspace_list_url)
152
+ workspaces_list = response.json().get("workspaceNames", [])
112
153
 
113
- # Calculate and add summary statistics to the data
114
- console.print("[blue]Calculating summary statistics...[/blue]")
115
- stats = calculate_statistics(data)
116
- data["statistics"] = stats
154
+ if not workspaces_list:
155
+ console.print("[yellow]No workspaces found.[/yellow]")
156
+ return
117
157
 
118
- # Save to JSON file
119
- console.print(f"\n[cyan]{'='*60}[/cyan]")
120
- console.print(f"[blue]Saving data to {output}...[/blue]")
121
- with open(output, "w") as f:
122
- json.dump(data, f, indent=2, default=str)
158
+ console.print(f"[green]Found {len(workspaces_list)} workspace(s)[/green]\n")
123
159
 
124
- console.print(
125
- f"[green]Data extraction complete! Results saved to {output}[/green]"
126
- )
160
+ # Process each workspace
161
+ for idx, workspace in enumerate(workspaces_list, 1):
162
+ console.print(f"\n[cyan]{'='*60}[/cyan]")
163
+ console.print(
164
+ f"[blue]Processing workspace {idx}/{len(workspaces_list)}: {workspace}[/blue]"
165
+ )
166
+ console.print(f"[cyan]{'='*60}[/cyan]\n")
127
167
 
128
- # Generate PDF report
129
- console.print(f"\n[cyan]{'='*60}[/cyan]")
130
- console.print("[blue]Generating PDF report...[/blue]")
131
- try:
132
- output_path = Path(output)
133
- output_dir = output_path.parent if output_path.parent != Path(".") else "."
134
- pdf_filename = create_pdf_report(data, output_dir=str(output_dir))
135
- console.print(f"[green]PDF report saved to {pdf_filename}[/green]")
136
-
137
- # Open PDF if --open flag is set
138
- if open_pdf:
139
- pdf_path = os.path.abspath(pdf_filename)
140
- if os.path.exists(pdf_path):
141
- webbrowser.open(f"file://{pdf_path}")
142
- console.print("[green]Opened PDF in default viewer[/green]")
168
+ try:
169
+ # Generate output filename for this workspace
170
+ if len(workspaces_list) == 1:
171
+ # Single workspace: use the provided output filename
172
+ workspace_output = output
143
173
  else:
174
+ # Multiple workspaces: append workspace name to output filename
175
+ output_path = Path(output)
176
+ workspace_output = str(
177
+ output_path.parent
178
+ / f"{output_path.stem}_{workspace}{output_path.suffix}"
179
+ )
180
+
181
+ # Load from JSON or extract from API
182
+ if from_json:
144
183
  console.print(
145
- f"[yellow]Warning: PDF file not found: {pdf_path}[/yellow]"
184
+ f"[green]Loading data from {workspace_output}...[/green]\n"
146
185
  )
147
- except Exception as e:
148
- console.print(
149
- f"[yellow]Warning: Could not generate PDF report: {e}[/yellow]"
150
- )
151
- traceback.print_exc()
186
+ if not os.path.exists(workspace_output):
187
+ console.print(
188
+ f"[red]Error: JSON file not found: {workspace_output}[/red]"
189
+ )
190
+ console.print(
191
+ f"[yellow]Expected file: {workspace_output}[/yellow]"
192
+ )
193
+ continue
194
+
195
+ with open(workspace_output, "r") as f:
196
+ data = json.load(f)
197
+
198
+ # Verify workspace matches
199
+ if data.get("workspace") != workspace:
200
+ console.print(
201
+ f"[yellow]Warning: JSON file workspace '{data.get('workspace')}' "
202
+ f"does not match expected workspace '{workspace}'[/yellow]"
203
+ )
204
+
205
+ # Calculate statistics if not present in JSON
206
+ if "statistics" not in data:
207
+ console.print("[blue]Calculating summary statistics...[/blue]")
208
+ stats = calculate_statistics(data)
209
+ data["statistics"] = stats
210
+
211
+ console.print(f"[green]Loaded data from {workspace_output}[/green]")
212
+ else:
213
+ console.print("[green]Starting Opik data extraction...[/green]\n")
214
+
215
+ data = extract_project_data(
216
+ workspace, api_key, start_date_obj, end_date_obj, unit
217
+ )
218
+
219
+ # Calculate and add summary statistics to the data
220
+ console.print("[blue]Calculating summary statistics...[/blue]")
221
+ stats = calculate_statistics(data)
222
+ data["statistics"] = stats
223
+
224
+ # Save to JSON file
225
+ console.print(f"\n[cyan]{'='*60}[/cyan]")
226
+ console.print(f"[blue]Saving data to {workspace_output}...[/blue]")
227
+ with open(workspace_output, "w") as f:
228
+ json.dump(data, f, indent=2, default=str)
229
+
230
+ console.print(
231
+ f"[green]Data extraction complete! Results saved to {workspace_output}[/green]"
232
+ )
233
+
234
+ # Generate PDF report
235
+ console.print(f"\n[cyan]{'='*60}[/cyan]")
236
+ console.print("[blue]Generating PDF report...[/blue]")
237
+ try:
238
+ output_path = Path(workspace_output)
239
+ output_dir = (
240
+ output_path.parent if output_path.parent != Path(".") else "."
241
+ )
242
+ pdf_filename = create_pdf_report(data, output_dir=str(output_dir))
243
+ console.print(f"[green]PDF report saved to {pdf_filename}[/green]")
244
+
245
+ # Open PDF if --open flag is set (only for the last workspace)
246
+ if open_pdf and idx == len(workspaces_list):
247
+ pdf_path = os.path.abspath(pdf_filename)
248
+ if os.path.exists(pdf_path):
249
+ webbrowser.open(f"file://{pdf_path}")
250
+ console.print("[green]Opened PDF in default viewer[/green]")
251
+ else:
252
+ console.print(
253
+ f"[yellow]Warning: PDF file not found: {pdf_path}[/yellow]"
254
+ )
255
+ except Exception as e:
256
+ console.print(
257
+ f"[yellow]Warning: Could not generate PDF report: {e}[/yellow]"
258
+ )
259
+ traceback.print_exc()
260
+
261
+ except Exception as e:
262
+ console.print(f"[red]Error processing workspace {workspace}: {e}[/red]")
263
+ traceback.print_exc()
264
+ # Continue with next workspace instead of exiting
265
+ continue
266
+
267
+ console.print(
268
+ f"\n[green]Completed processing {len(workspaces_list)} workspace(s)[/green]"
269
+ )
152
270
 
153
271
  except Exception as e:
154
272
  console.print(f"[red]Error: {e}[/red]")
273
+ traceback.print_exc()
155
274
  sys.exit(1)
@@ -184,7 +184,20 @@ def create_pdf_report(data: Dict[str, Any], output_dir: str = ".") -> str:
184
184
  )
185
185
  continue
186
186
 
187
- img = Image(chart_path, width=7 * inch, height=4.5 * inch)
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
+ )
188
201
  story.append(img)
189
202
  story.append(Spacer(1, 0.1 * inch))
190
203
  story.append(PageBreak())
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
@@ -257,10 +256,6 @@ class OpikConfig(pydantic_settings.BaseSettings):
257
256
  def guardrails_backend_host(self) -> str:
258
257
  return url_helpers.get_base_url(self.url_override) + "guardrails/"
259
258
 
260
- @property
261
- def runtime(self) -> tracing_runtime_config.TracingRuntimeConfig:
262
- return tracing_runtime_config.runtime_cfg
263
-
264
259
  @pydantic.model_validator(mode="after")
265
260
  def _set_url_override_from_api_key(self) -> "OpikConfig":
266
261
  url_was_not_provided = (
@@ -14,7 +14,7 @@ from typing import (
14
14
  NamedTuple,
15
15
  )
16
16
 
17
- from .. import context_storage, logging_messages
17
+ from .. import context_storage, logging_messages, tracing_runtime_config
18
18
  from ..api_objects import opik_client, span, trace
19
19
  from ..types import DistributedTraceHeadersDict, ErrorInfoDict, SpanType
20
20
  from . import (
@@ -24,7 +24,6 @@ from . import (
24
24
  inspect_helpers,
25
25
  opik_args,
26
26
  span_creation_handler,
27
- tracing_runtime_config,
28
27
  )
29
28
 
30
29
  LOGGER = logging.getLogger(__name__)
@@ -337,25 +336,24 @@ class BaseTrackDecorator(abc.ABC):
337
336
  )
338
337
  error_info = error_info_collector.collect(exception)
339
338
  func_exception = exception
340
- finally:
341
- stream_or_stream_manager = self._streams_handler(
342
- result,
343
- track_options.capture_output,
344
- track_options.generations_aggregator,
345
- )
346
- if stream_or_stream_manager is not None:
347
- return stream_or_stream_manager
348
-
349
- self._after_call(
350
- output=result,
351
- error_info=error_info,
352
- capture_output=track_options.capture_output,
353
- flush=track_options.flush,
354
- )
355
- if func_exception is not None:
356
- raise func_exception
357
- else:
358
- return result
339
+
340
+ stream_or_stream_manager = self._streams_handler(
341
+ result,
342
+ track_options.capture_output,
343
+ track_options.generations_aggregator,
344
+ )
345
+ if stream_or_stream_manager is not None:
346
+ return stream_or_stream_manager
347
+
348
+ self._after_call(
349
+ output=result,
350
+ error_info=error_info,
351
+ capture_output=track_options.capture_output,
352
+ flush=track_options.flush,
353
+ )
354
+ if func_exception is not None:
355
+ raise func_exception
356
+ return result
359
357
 
360
358
  wrapper.opik_tracked = True # type: ignore
361
359
 
@@ -391,25 +389,24 @@ class BaseTrackDecorator(abc.ABC):
391
389
  )
392
390
  error_info = error_info_collector.collect(exception)
393
391
  func_exception = exception
394
- finally:
395
- stream_or_stream_manager = self._streams_handler(
396
- result,
397
- track_options.capture_output,
398
- track_options.generations_aggregator,
399
- )
400
- if stream_or_stream_manager is not None:
401
- return stream_or_stream_manager
402
-
403
- self._after_call(
404
- output=result,
405
- error_info=error_info,
406
- capture_output=track_options.capture_output,
407
- flush=track_options.flush,
408
- )
409
- if func_exception is not None:
410
- raise func_exception
411
- else:
412
- return result
392
+
393
+ stream_or_stream_manager = self._streams_handler(
394
+ result,
395
+ track_options.capture_output,
396
+ track_options.generations_aggregator,
397
+ )
398
+ if stream_or_stream_manager is not None:
399
+ return stream_or_stream_manager
400
+
401
+ self._after_call(
402
+ output=result,
403
+ error_info=error_info,
404
+ capture_output=track_options.capture_output,
405
+ flush=track_options.flush,
406
+ )
407
+ if func_exception is not None:
408
+ raise func_exception
409
+ return result
413
410
 
414
411
  wrapper.opik_tracked = True # type: ignore
415
412
  return wrapper
@@ -4,6 +4,7 @@ from typing import Optional, Dict, Any, List, Generator
4
4
 
5
5
  from opik.api_objects import span, opik_client
6
6
  from opik.types import SpanType
7
+ from opik import context_storage
7
8
  from .. import arguments_helpers, base_track_decorator, error_info_collector
8
9
 
9
10
  LOGGER = logging.getLogger(__name__)
@@ -110,5 +111,13 @@ def start_as_current_span(
110
111
  )
111
112
  client.trace(**span_creation_result.trace_data.as_parameters)
112
113
 
114
+ # Clean up span and trace from context
115
+ opik_context_storage = context_storage.get_current_context_instance()
116
+ opik_context_storage.pop_span_data(ensure_id=span_creation_result.span_data.id)
117
+ if span_creation_result.trace_data is not None:
118
+ opik_context_storage.pop_trace_data(
119
+ ensure_id=span_creation_result.trace_data.id
120
+ )
121
+
113
122
  if flush:
114
123
  client.flush()
@@ -4,6 +4,7 @@ from typing import Any, Generator, Optional, Dict, List
4
4
 
5
5
  from opik import datetime_helpers
6
6
  from opik.api_objects import trace, opik_client, helpers
7
+ from opik import context_storage
7
8
  from .. import base_track_decorator, error_info_collector
8
9
 
9
10
  LOGGER = logging.getLogger(__name__)
@@ -75,5 +76,9 @@ def start_as_current_trace(
75
76
  client = opik_client.get_client_cached()
76
77
  client.trace(**trace_data.init_end_time().as_parameters)
77
78
 
79
+ # Clean up trace from context
80
+ opik_context_storage = context_storage.get_current_context_instance()
81
+ opik_context_storage.pop_trace_data(ensure_id=trace_data.id)
82
+
78
83
  if flush:
79
84
  client.flush()
opik/dict_utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import copy
2
2
  import logging
3
- from typing import Any, Dict, Mapping, Optional, List, Tuple, TypeVar, Type
3
+ from typing import Any, Dict, Optional, List, Tuple, TypeVar, Type
4
4
 
5
5
  from . import logging_messages
6
6
 
@@ -11,7 +11,7 @@ def flatten_dict(
11
11
  d: Dict[str, Any], parent_key: str, delim: str = "."
12
12
  ) -> Dict[str, Any]:
13
13
  """
14
- Current implementation does not have max depth restrictions or cyclic references handling!
14
+ The current implementation does not have max depth restrictions or cyclic references handling!
15
15
  """
16
16
  items = [] # type: ignore
17
17
 
@@ -60,7 +60,7 @@ def deepmerge(
60
60
  return merged
61
61
 
62
62
 
63
- def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Mapping[str, Any]:
63
+ def remove_none_from_dict(original: Dict[str, Optional[Any]]) -> Dict[str, Any]:
64
64
  new: Dict[str, Any] = {}
65
65
 
66
66
  for key, value in original.items():
@@ -1,4 +1,15 @@
1
- from .evaluator import evaluate, evaluate_prompt, evaluate_experiment
1
+ from .evaluator import (
2
+ evaluate,
3
+ evaluate_prompt,
4
+ evaluate_experiment,
5
+ evaluate_on_dict_items,
6
+ )
2
7
  from .threads.evaluator import evaluate_threads
3
8
 
4
- __all__ = ["evaluate", "evaluate_prompt", "evaluate_experiment", "evaluate_threads"]
9
+ __all__ = [
10
+ "evaluate",
11
+ "evaluate_prompt",
12
+ "evaluate_experiment",
13
+ "evaluate_on_dict_items",
14
+ "evaluate_threads",
15
+ ]