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.
- opik/__init__.py +19 -3
- opik/anonymizer/__init__.py +5 -0
- opik/anonymizer/anonymizer.py +12 -0
- opik/anonymizer/factory.py +80 -0
- opik/anonymizer/recursive_anonymizer.py +64 -0
- opik/anonymizer/rules.py +56 -0
- opik/anonymizer/rules_anonymizer.py +35 -0
- opik/api_objects/attachment/attachment_context.py +36 -0
- opik/api_objects/attachment/attachments_extractor.py +153 -0
- opik/api_objects/attachment/client.py +1 -0
- opik/api_objects/attachment/converters.py +2 -0
- opik/api_objects/attachment/decoder.py +18 -0
- opik/api_objects/attachment/decoder_base64.py +83 -0
- opik/api_objects/attachment/decoder_helpers.py +137 -0
- opik/api_objects/data_helpers.py +79 -0
- opik/api_objects/dataset/dataset.py +64 -4
- opik/api_objects/dataset/rest_operations.py +11 -2
- opik/api_objects/experiment/experiment.py +57 -57
- opik/api_objects/experiment/experiment_item.py +2 -1
- opik/api_objects/experiment/experiments_client.py +64 -0
- opik/api_objects/experiment/helpers.py +35 -11
- opik/api_objects/experiment/rest_operations.py +65 -5
- opik/api_objects/helpers.py +8 -5
- opik/api_objects/local_recording.py +81 -0
- opik/api_objects/opik_client.py +600 -108
- opik/api_objects/opik_query_language.py +39 -5
- opik/api_objects/prompt/__init__.py +12 -2
- opik/api_objects/prompt/base_prompt.py +69 -0
- opik/api_objects/prompt/base_prompt_template.py +29 -0
- opik/api_objects/prompt/chat/__init__.py +1 -0
- opik/api_objects/prompt/chat/chat_prompt.py +210 -0
- opik/api_objects/prompt/chat/chat_prompt_template.py +350 -0
- opik/api_objects/prompt/chat/content_renderer_registry.py +203 -0
- opik/api_objects/prompt/client.py +189 -47
- opik/api_objects/prompt/text/__init__.py +1 -0
- opik/api_objects/prompt/text/prompt.py +174 -0
- opik/api_objects/prompt/{prompt_template.py → text/prompt_template.py} +10 -6
- opik/api_objects/prompt/types.py +23 -0
- opik/api_objects/search_helpers.py +89 -0
- opik/api_objects/span/span_data.py +35 -25
- opik/api_objects/threads/threads_client.py +39 -5
- opik/api_objects/trace/trace_client.py +52 -2
- opik/api_objects/trace/trace_data.py +15 -24
- opik/api_objects/validation_helpers.py +3 -3
- opik/cli/__init__.py +5 -0
- opik/cli/__main__.py +6 -0
- opik/cli/configure.py +66 -0
- opik/cli/exports/__init__.py +131 -0
- opik/cli/exports/dataset.py +278 -0
- opik/cli/exports/experiment.py +784 -0
- opik/cli/exports/project.py +685 -0
- opik/cli/exports/prompt.py +578 -0
- opik/cli/exports/utils.py +406 -0
- opik/cli/harbor.py +39 -0
- opik/cli/healthcheck.py +21 -0
- opik/cli/imports/__init__.py +439 -0
- opik/cli/imports/dataset.py +143 -0
- opik/cli/imports/experiment.py +1192 -0
- opik/cli/imports/project.py +262 -0
- opik/cli/imports/prompt.py +177 -0
- opik/cli/imports/utils.py +280 -0
- opik/cli/main.py +49 -0
- opik/cli/proxy.py +93 -0
- opik/cli/usage_report/__init__.py +16 -0
- opik/cli/usage_report/charts.py +783 -0
- opik/cli/usage_report/cli.py +274 -0
- opik/cli/usage_report/constants.py +9 -0
- opik/cli/usage_report/extraction.py +749 -0
- opik/cli/usage_report/pdf.py +244 -0
- opik/cli/usage_report/statistics.py +78 -0
- opik/cli/usage_report/utils.py +235 -0
- opik/config.py +13 -7
- opik/configurator/configure.py +17 -0
- opik/datetime_helpers.py +12 -0
- opik/decorator/arguments_helpers.py +9 -1
- opik/decorator/base_track_decorator.py +205 -133
- opik/decorator/context_manager/span_context_manager.py +123 -0
- opik/decorator/context_manager/trace_context_manager.py +84 -0
- opik/decorator/opik_args/__init__.py +13 -0
- opik/decorator/opik_args/api_classes.py +71 -0
- opik/decorator/opik_args/helpers.py +120 -0
- opik/decorator/span_creation_handler.py +25 -6
- opik/dict_utils.py +3 -3
- opik/evaluation/__init__.py +13 -2
- opik/evaluation/engine/engine.py +272 -75
- opik/evaluation/engine/evaluation_tasks_executor.py +6 -3
- opik/evaluation/engine/helpers.py +31 -6
- opik/evaluation/engine/metrics_evaluator.py +237 -0
- opik/evaluation/evaluation_result.py +168 -2
- opik/evaluation/evaluator.py +533 -62
- opik/evaluation/metrics/__init__.py +103 -4
- opik/evaluation/metrics/aggregated_metric.py +35 -6
- opik/evaluation/metrics/base_metric.py +1 -1
- opik/evaluation/metrics/conversation/__init__.py +48 -0
- opik/evaluation/metrics/conversation/conversation_thread_metric.py +56 -2
- opik/evaluation/metrics/conversation/g_eval_wrappers.py +19 -0
- opik/evaluation/metrics/conversation/helpers.py +14 -15
- opik/evaluation/metrics/conversation/heuristics/__init__.py +14 -0
- opik/evaluation/metrics/conversation/heuristics/degeneration/__init__.py +3 -0
- opik/evaluation/metrics/conversation/heuristics/degeneration/metric.py +189 -0
- opik/evaluation/metrics/conversation/heuristics/degeneration/phrases.py +12 -0
- opik/evaluation/metrics/conversation/heuristics/knowledge_retention/__init__.py +3 -0
- opik/evaluation/metrics/conversation/heuristics/knowledge_retention/metric.py +172 -0
- opik/evaluation/metrics/conversation/llm_judges/__init__.py +32 -0
- opik/evaluation/metrics/conversation/{conversational_coherence → llm_judges/conversational_coherence}/metric.py +22 -17
- opik/evaluation/metrics/conversation/{conversational_coherence → llm_judges/conversational_coherence}/templates.py +1 -1
- opik/evaluation/metrics/conversation/llm_judges/g_eval_wrappers.py +442 -0
- opik/evaluation/metrics/conversation/{session_completeness → llm_judges/session_completeness}/metric.py +13 -7
- opik/evaluation/metrics/conversation/{session_completeness → llm_judges/session_completeness}/templates.py +1 -1
- opik/evaluation/metrics/conversation/llm_judges/user_frustration/__init__.py +0 -0
- opik/evaluation/metrics/conversation/{user_frustration → llm_judges/user_frustration}/metric.py +21 -14
- opik/evaluation/metrics/conversation/{user_frustration → llm_judges/user_frustration}/templates.py +1 -1
- opik/evaluation/metrics/conversation/types.py +4 -5
- opik/evaluation/metrics/conversation_types.py +9 -0
- opik/evaluation/metrics/heuristics/bertscore.py +107 -0
- opik/evaluation/metrics/heuristics/bleu.py +35 -15
- opik/evaluation/metrics/heuristics/chrf.py +127 -0
- opik/evaluation/metrics/heuristics/contains.py +47 -11
- opik/evaluation/metrics/heuristics/distribution_metrics.py +331 -0
- opik/evaluation/metrics/heuristics/gleu.py +113 -0
- opik/evaluation/metrics/heuristics/language_adherence.py +123 -0
- opik/evaluation/metrics/heuristics/meteor.py +119 -0
- opik/evaluation/metrics/heuristics/prompt_injection.py +150 -0
- opik/evaluation/metrics/heuristics/readability.py +129 -0
- opik/evaluation/metrics/heuristics/rouge.py +26 -9
- opik/evaluation/metrics/heuristics/spearman.py +88 -0
- opik/evaluation/metrics/heuristics/tone.py +155 -0
- opik/evaluation/metrics/heuristics/vader_sentiment.py +77 -0
- opik/evaluation/metrics/llm_judges/answer_relevance/metric.py +20 -5
- opik/evaluation/metrics/llm_judges/context_precision/metric.py +20 -6
- opik/evaluation/metrics/llm_judges/context_recall/metric.py +20 -6
- opik/evaluation/metrics/llm_judges/g_eval/__init__.py +5 -0
- opik/evaluation/metrics/llm_judges/g_eval/metric.py +219 -68
- opik/evaluation/metrics/llm_judges/g_eval/parser.py +102 -52
- opik/evaluation/metrics/llm_judges/g_eval/presets.py +209 -0
- opik/evaluation/metrics/llm_judges/g_eval_presets/__init__.py +36 -0
- opik/evaluation/metrics/llm_judges/g_eval_presets/agent_assessment.py +77 -0
- opik/evaluation/metrics/llm_judges/g_eval_presets/bias_classifier.py +181 -0
- opik/evaluation/metrics/llm_judges/g_eval_presets/compliance_risk.py +41 -0
- opik/evaluation/metrics/llm_judges/g_eval_presets/prompt_uncertainty.py +41 -0
- opik/evaluation/metrics/llm_judges/g_eval_presets/qa_suite.py +146 -0
- opik/evaluation/metrics/llm_judges/hallucination/metric.py +16 -3
- opik/evaluation/metrics/llm_judges/llm_juries/__init__.py +3 -0
- opik/evaluation/metrics/llm_judges/llm_juries/metric.py +76 -0
- opik/evaluation/metrics/llm_judges/moderation/metric.py +16 -4
- opik/evaluation/metrics/llm_judges/structure_output_compliance/__init__.py +0 -0
- opik/evaluation/metrics/llm_judges/structure_output_compliance/metric.py +144 -0
- opik/evaluation/metrics/llm_judges/structure_output_compliance/parser.py +79 -0
- opik/evaluation/metrics/llm_judges/structure_output_compliance/schema.py +15 -0
- opik/evaluation/metrics/llm_judges/structure_output_compliance/template.py +50 -0
- opik/evaluation/metrics/llm_judges/syc_eval/__init__.py +0 -0
- opik/evaluation/metrics/llm_judges/syc_eval/metric.py +252 -0
- opik/evaluation/metrics/llm_judges/syc_eval/parser.py +82 -0
- opik/evaluation/metrics/llm_judges/syc_eval/template.py +155 -0
- opik/evaluation/metrics/llm_judges/trajectory_accuracy/metric.py +20 -5
- opik/evaluation/metrics/llm_judges/usefulness/metric.py +16 -4
- opik/evaluation/metrics/ragas_metric.py +43 -23
- opik/evaluation/models/__init__.py +8 -0
- opik/evaluation/models/base_model.py +107 -1
- opik/evaluation/models/langchain/langchain_chat_model.py +15 -7
- opik/evaluation/models/langchain/message_converters.py +97 -15
- opik/evaluation/models/litellm/litellm_chat_model.py +156 -29
- opik/evaluation/models/litellm/util.py +125 -0
- opik/evaluation/models/litellm/warning_filters.py +16 -4
- opik/evaluation/models/model_capabilities.py +187 -0
- opik/evaluation/models/models_factory.py +25 -3
- opik/evaluation/preprocessing.py +92 -0
- opik/evaluation/report.py +70 -12
- opik/evaluation/rest_operations.py +49 -45
- opik/evaluation/samplers/__init__.py +4 -0
- opik/evaluation/samplers/base_dataset_sampler.py +40 -0
- opik/evaluation/samplers/random_dataset_sampler.py +48 -0
- opik/evaluation/score_statistics.py +66 -0
- opik/evaluation/scorers/__init__.py +4 -0
- opik/evaluation/scorers/scorer_function.py +55 -0
- opik/evaluation/scorers/scorer_wrapper_metric.py +130 -0
- opik/evaluation/test_case.py +3 -2
- opik/evaluation/test_result.py +1 -0
- opik/evaluation/threads/evaluator.py +31 -3
- opik/evaluation/threads/helpers.py +3 -2
- opik/evaluation/types.py +9 -1
- opik/exceptions.py +33 -0
- opik/file_upload/file_uploader.py +13 -0
- opik/file_upload/upload_options.py +2 -0
- opik/hooks/__init__.py +23 -0
- opik/hooks/anonymizer_hook.py +36 -0
- opik/hooks/httpx_client_hook.py +112 -0
- opik/httpx_client.py +12 -9
- opik/id_helpers.py +18 -0
- opik/integrations/adk/graph/subgraph_edges_builders.py +1 -2
- opik/integrations/adk/helpers.py +16 -7
- opik/integrations/adk/legacy_opik_tracer.py +7 -4
- opik/integrations/adk/opik_tracer.py +14 -1
- opik/integrations/adk/patchers/adk_otel_tracer/opik_adk_otel_tracer.py +7 -3
- opik/integrations/adk/recursive_callback_injector.py +4 -7
- opik/integrations/bedrock/converse/__init__.py +0 -0
- opik/integrations/bedrock/converse/chunks_aggregator.py +188 -0
- opik/integrations/bedrock/{converse_decorator.py → converse/converse_decorator.py} +4 -3
- opik/integrations/bedrock/invoke_agent_decorator.py +5 -4
- opik/integrations/bedrock/invoke_model/__init__.py +0 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/__init__.py +78 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/api.py +45 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/base.py +23 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/claude.py +121 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/format_detector.py +107 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/llama.py +108 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/mistral.py +118 -0
- opik/integrations/bedrock/invoke_model/chunks_aggregator/nova.py +99 -0
- opik/integrations/bedrock/invoke_model/invoke_model_decorator.py +178 -0
- opik/integrations/bedrock/invoke_model/response_types.py +34 -0
- opik/integrations/bedrock/invoke_model/stream_wrappers.py +122 -0
- opik/integrations/bedrock/invoke_model/usage_converters.py +87 -0
- opik/integrations/bedrock/invoke_model/usage_extraction.py +108 -0
- opik/integrations/bedrock/opik_tracker.py +42 -4
- opik/integrations/bedrock/types.py +19 -0
- opik/integrations/crewai/crewai_decorator.py +8 -51
- opik/integrations/crewai/opik_tracker.py +31 -10
- opik/integrations/crewai/patchers/__init__.py +5 -0
- opik/integrations/crewai/patchers/flow.py +118 -0
- opik/integrations/crewai/patchers/litellm_completion.py +30 -0
- opik/integrations/crewai/patchers/llm_client.py +207 -0
- opik/integrations/dspy/callback.py +80 -17
- opik/integrations/dspy/parsers.py +168 -0
- opik/integrations/harbor/__init__.py +17 -0
- opik/integrations/harbor/experiment_service.py +269 -0
- opik/integrations/harbor/opik_tracker.py +528 -0
- opik/integrations/haystack/opik_connector.py +2 -2
- opik/integrations/haystack/opik_tracer.py +3 -7
- opik/integrations/langchain/__init__.py +3 -1
- opik/integrations/langchain/helpers.py +96 -0
- opik/integrations/langchain/langgraph_async_context_bridge.py +131 -0
- opik/integrations/langchain/langgraph_tracer_injector.py +88 -0
- opik/integrations/langchain/opik_encoder_extension.py +1 -1
- opik/integrations/langchain/opik_tracer.py +474 -229
- opik/integrations/litellm/__init__.py +5 -0
- opik/integrations/litellm/completion_chunks_aggregator.py +115 -0
- opik/integrations/litellm/litellm_completion_decorator.py +242 -0
- opik/integrations/litellm/opik_tracker.py +43 -0
- opik/integrations/litellm/stream_patchers.py +151 -0
- opik/integrations/llama_index/callback.py +146 -107
- opik/integrations/openai/agents/opik_tracing_processor.py +1 -2
- opik/integrations/openai/openai_chat_completions_decorator.py +2 -16
- opik/integrations/openai/opik_tracker.py +1 -1
- opik/integrations/sagemaker/auth.py +5 -1
- opik/llm_usage/google_usage.py +3 -1
- opik/llm_usage/opik_usage.py +7 -8
- opik/llm_usage/opik_usage_factory.py +4 -2
- opik/logging_messages.py +6 -0
- opik/message_processing/batching/base_batcher.py +14 -21
- opik/message_processing/batching/batch_manager.py +22 -10
- opik/message_processing/batching/batch_manager_constuctors.py +10 -0
- opik/message_processing/batching/batchers.py +59 -27
- opik/message_processing/batching/flushing_thread.py +0 -3
- opik/message_processing/emulation/__init__.py +0 -0
- opik/message_processing/emulation/emulator_message_processor.py +578 -0
- opik/message_processing/emulation/local_emulator_message_processor.py +140 -0
- opik/message_processing/emulation/models.py +162 -0
- opik/message_processing/encoder_helpers.py +79 -0
- opik/message_processing/messages.py +56 -1
- opik/message_processing/preprocessing/__init__.py +0 -0
- opik/message_processing/preprocessing/attachments_preprocessor.py +70 -0
- opik/message_processing/preprocessing/batching_preprocessor.py +53 -0
- opik/message_processing/preprocessing/constants.py +1 -0
- opik/message_processing/preprocessing/file_upload_preprocessor.py +38 -0
- opik/message_processing/preprocessing/preprocessor.py +36 -0
- opik/message_processing/processors/__init__.py +0 -0
- opik/message_processing/processors/attachments_extraction_processor.py +146 -0
- opik/message_processing/processors/message_processors.py +92 -0
- opik/message_processing/processors/message_processors_chain.py +96 -0
- opik/message_processing/{message_processors.py → processors/online_message_processor.py} +85 -29
- opik/message_processing/queue_consumer.py +9 -3
- opik/message_processing/streamer.py +71 -33
- opik/message_processing/streamer_constructors.py +43 -10
- opik/opik_context.py +16 -4
- opik/plugins/pytest/hooks.py +5 -3
- opik/rest_api/__init__.py +346 -15
- opik/rest_api/alerts/__init__.py +7 -0
- opik/rest_api/alerts/client.py +667 -0
- opik/rest_api/alerts/raw_client.py +1015 -0
- opik/rest_api/alerts/types/__init__.py +7 -0
- opik/rest_api/alerts/types/get_webhook_examples_request_alert_type.py +5 -0
- opik/rest_api/annotation_queues/__init__.py +4 -0
- opik/rest_api/annotation_queues/client.py +668 -0
- opik/rest_api/annotation_queues/raw_client.py +1019 -0
- opik/rest_api/automation_rule_evaluators/client.py +34 -2
- opik/rest_api/automation_rule_evaluators/raw_client.py +24 -0
- opik/rest_api/client.py +15 -0
- opik/rest_api/dashboards/__init__.py +4 -0
- opik/rest_api/dashboards/client.py +462 -0
- opik/rest_api/dashboards/raw_client.py +648 -0
- opik/rest_api/datasets/client.py +1310 -44
- opik/rest_api/datasets/raw_client.py +2269 -358
- opik/rest_api/experiments/__init__.py +2 -2
- opik/rest_api/experiments/client.py +191 -5
- opik/rest_api/experiments/raw_client.py +301 -7
- opik/rest_api/experiments/types/__init__.py +4 -1
- opik/rest_api/experiments/types/experiment_update_status.py +5 -0
- opik/rest_api/experiments/types/experiment_update_type.py +5 -0
- opik/rest_api/experiments/types/experiment_write_status.py +5 -0
- opik/rest_api/feedback_definitions/types/find_feedback_definitions_request_type.py +1 -1
- opik/rest_api/llm_provider_key/client.py +20 -0
- opik/rest_api/llm_provider_key/raw_client.py +20 -0
- opik/rest_api/llm_provider_key/types/provider_api_key_write_provider.py +1 -1
- opik/rest_api/manual_evaluation/__init__.py +4 -0
- opik/rest_api/manual_evaluation/client.py +347 -0
- opik/rest_api/manual_evaluation/raw_client.py +543 -0
- opik/rest_api/optimizations/client.py +145 -9
- opik/rest_api/optimizations/raw_client.py +237 -13
- opik/rest_api/optimizations/types/optimization_update_status.py +3 -1
- opik/rest_api/prompts/__init__.py +2 -2
- opik/rest_api/prompts/client.py +227 -6
- opik/rest_api/prompts/raw_client.py +331 -2
- opik/rest_api/prompts/types/__init__.py +3 -1
- opik/rest_api/prompts/types/create_prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/prompts/types/prompt_write_template_structure.py +5 -0
- opik/rest_api/spans/__init__.py +0 -2
- opik/rest_api/spans/client.py +238 -76
- opik/rest_api/spans/raw_client.py +307 -95
- opik/rest_api/spans/types/__init__.py +0 -2
- opik/rest_api/traces/client.py +572 -161
- opik/rest_api/traces/raw_client.py +736 -229
- opik/rest_api/types/__init__.py +352 -17
- opik/rest_api/types/aggregation_data.py +1 -0
- opik/rest_api/types/alert.py +33 -0
- opik/rest_api/types/alert_alert_type.py +5 -0
- opik/rest_api/types/alert_page_public.py +24 -0
- opik/rest_api/types/alert_public.py +33 -0
- opik/rest_api/types/alert_public_alert_type.py +5 -0
- opik/rest_api/types/alert_trigger.py +27 -0
- opik/rest_api/types/alert_trigger_config.py +28 -0
- opik/rest_api/types/alert_trigger_config_public.py +28 -0
- opik/rest_api/types/alert_trigger_config_public_type.py +10 -0
- opik/rest_api/types/alert_trigger_config_type.py +10 -0
- opik/rest_api/types/alert_trigger_config_write.py +22 -0
- opik/rest_api/types/alert_trigger_config_write_type.py +10 -0
- opik/rest_api/types/alert_trigger_event_type.py +19 -0
- opik/rest_api/types/alert_trigger_public.py +27 -0
- opik/rest_api/types/alert_trigger_public_event_type.py +19 -0
- opik/rest_api/types/alert_trigger_write.py +23 -0
- opik/rest_api/types/alert_trigger_write_event_type.py +19 -0
- opik/rest_api/types/alert_write.py +28 -0
- opik/rest_api/types/alert_write_alert_type.py +5 -0
- opik/rest_api/types/annotation_queue.py +42 -0
- opik/rest_api/types/annotation_queue_batch.py +27 -0
- opik/rest_api/types/annotation_queue_item_ids.py +19 -0
- opik/rest_api/types/annotation_queue_page_public.py +28 -0
- opik/rest_api/types/annotation_queue_public.py +38 -0
- opik/rest_api/types/annotation_queue_public_scope.py +5 -0
- opik/rest_api/types/annotation_queue_reviewer.py +20 -0
- opik/rest_api/types/annotation_queue_reviewer_public.py +20 -0
- opik/rest_api/types/annotation_queue_scope.py +5 -0
- opik/rest_api/types/annotation_queue_write.py +31 -0
- opik/rest_api/types/annotation_queue_write_scope.py +5 -0
- opik/rest_api/types/audio_url.py +19 -0
- opik/rest_api/types/audio_url_public.py +19 -0
- opik/rest_api/types/audio_url_write.py +19 -0
- opik/rest_api/types/automation_rule_evaluator.py +62 -2
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_llm_as_judge_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_object_object_public.py +155 -0
- opik/rest_api/types/automation_rule_evaluator_page_public.py +3 -2
- opik/rest_api/types/automation_rule_evaluator_public.py +57 -2
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_public.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_llm_as_judge_write.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_public.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_span_user_defined_metric_python_write.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_llm_as_judge_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_trace_thread_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update.py +51 -1
- opik/rest_api/types/automation_rule_evaluator_update_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_span_llm_as_judge.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_update_span_user_defined_metric_python.py +22 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_llm_as_judge.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_trace_thread_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_update_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_public.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_user_defined_metric_python_write.py +2 -0
- opik/rest_api/types/automation_rule_evaluator_write.py +51 -1
- opik/rest_api/types/boolean_feedback_definition.py +25 -0
- opik/rest_api/types/boolean_feedback_definition_create.py +20 -0
- opik/rest_api/types/boolean_feedback_definition_public.py +25 -0
- opik/rest_api/types/boolean_feedback_definition_update.py +20 -0
- opik/rest_api/types/boolean_feedback_detail.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_create.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_public.py +29 -0
- opik/rest_api/types/boolean_feedback_detail_update.py +29 -0
- opik/rest_api/types/dashboard_page_public.py +24 -0
- opik/rest_api/types/dashboard_public.py +30 -0
- opik/rest_api/types/dataset.py +4 -0
- opik/rest_api/types/dataset_expansion.py +42 -0
- opik/rest_api/types/dataset_expansion_response.py +39 -0
- opik/rest_api/types/dataset_item.py +2 -0
- opik/rest_api/types/dataset_item_changes_public.py +5 -0
- opik/rest_api/types/dataset_item_compare.py +2 -0
- opik/rest_api/types/dataset_item_filter.py +27 -0
- opik/rest_api/types/dataset_item_filter_operator.py +21 -0
- opik/rest_api/types/dataset_item_page_compare.py +5 -0
- opik/rest_api/types/dataset_item_page_public.py +5 -0
- opik/rest_api/types/dataset_item_public.py +2 -0
- opik/rest_api/types/dataset_item_update.py +39 -0
- opik/rest_api/types/dataset_item_write.py +1 -0
- opik/rest_api/types/dataset_public.py +4 -0
- opik/rest_api/types/dataset_public_status.py +5 -0
- opik/rest_api/types/dataset_status.py +5 -0
- opik/rest_api/types/dataset_version_diff.py +22 -0
- opik/rest_api/types/dataset_version_diff_stats.py +24 -0
- opik/rest_api/types/dataset_version_page_public.py +23 -0
- opik/rest_api/types/dataset_version_public.py +59 -0
- opik/rest_api/types/dataset_version_summary.py +46 -0
- opik/rest_api/types/dataset_version_summary_public.py +46 -0
- opik/rest_api/types/experiment.py +7 -2
- opik/rest_api/types/experiment_group_response.py +2 -0
- opik/rest_api/types/experiment_public.py +7 -2
- opik/rest_api/types/experiment_public_status.py +5 -0
- opik/rest_api/types/experiment_score.py +20 -0
- opik/rest_api/types/experiment_score_public.py +20 -0
- opik/rest_api/types/experiment_score_write.py +20 -0
- opik/rest_api/types/experiment_status.py +5 -0
- opik/rest_api/types/feedback.py +25 -1
- opik/rest_api/types/feedback_create.py +20 -1
- opik/rest_api/types/feedback_object_public.py +27 -1
- opik/rest_api/types/feedback_public.py +25 -1
- opik/rest_api/types/feedback_score_batch_item.py +2 -1
- opik/rest_api/types/feedback_score_batch_item_thread.py +2 -1
- opik/rest_api/types/feedback_score_public.py +4 -0
- opik/rest_api/types/feedback_update.py +20 -1
- opik/rest_api/types/group_content_with_aggregations.py +1 -0
- opik/rest_api/types/group_detail.py +19 -0
- opik/rest_api/types/group_details.py +20 -0
- opik/rest_api/types/guardrail.py +1 -0
- opik/rest_api/types/guardrail_write.py +1 -0
- opik/rest_api/types/ids_holder.py +19 -0
- opik/rest_api/types/image_url.py +20 -0
- opik/rest_api/types/image_url_public.py +20 -0
- opik/rest_api/types/image_url_write.py +20 -0
- opik/rest_api/types/llm_as_judge_message.py +5 -1
- opik/rest_api/types/llm_as_judge_message_content.py +26 -0
- opik/rest_api/types/llm_as_judge_message_content_public.py +26 -0
- opik/rest_api/types/llm_as_judge_message_content_write.py +26 -0
- opik/rest_api/types/llm_as_judge_message_public.py +5 -1
- opik/rest_api/types/llm_as_judge_message_write.py +5 -1
- opik/rest_api/types/llm_as_judge_model_parameters.py +3 -0
- opik/rest_api/types/llm_as_judge_model_parameters_public.py +3 -0
- opik/rest_api/types/llm_as_judge_model_parameters_write.py +3 -0
- opik/rest_api/types/manual_evaluation_request.py +38 -0
- opik/rest_api/types/manual_evaluation_request_entity_type.py +5 -0
- opik/rest_api/types/manual_evaluation_response.py +27 -0
- opik/rest_api/types/optimization.py +4 -2
- opik/rest_api/types/optimization_public.py +4 -2
- opik/rest_api/types/optimization_public_status.py +3 -1
- opik/rest_api/types/optimization_status.py +3 -1
- opik/rest_api/types/optimization_studio_config.py +27 -0
- opik/rest_api/types/optimization_studio_config_public.py +27 -0
- opik/rest_api/types/optimization_studio_config_write.py +27 -0
- opik/rest_api/types/optimization_studio_log.py +22 -0
- opik/rest_api/types/optimization_write.py +4 -2
- opik/rest_api/types/optimization_write_status.py +3 -1
- opik/rest_api/types/project.py +1 -0
- opik/rest_api/types/project_detailed.py +1 -0
- opik/rest_api/types/project_reference.py +31 -0
- opik/rest_api/types/project_reference_public.py +31 -0
- opik/rest_api/types/project_stats_summary_item.py +1 -0
- opik/rest_api/types/prompt.py +6 -0
- opik/rest_api/types/prompt_detail.py +6 -0
- opik/rest_api/types/prompt_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_public.py +6 -0
- opik/rest_api/types/prompt_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_template_structure.py +5 -0
- opik/rest_api/types/prompt_version.py +3 -0
- opik/rest_api/types/prompt_version_detail.py +3 -0
- opik/rest_api/types/prompt_version_detail_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_link.py +1 -0
- opik/rest_api/types/prompt_version_link_public.py +1 -0
- opik/rest_api/types/prompt_version_page_public.py +5 -0
- opik/rest_api/types/prompt_version_public.py +3 -0
- opik/rest_api/types/prompt_version_public_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_template_structure.py +5 -0
- opik/rest_api/types/prompt_version_update.py +33 -0
- opik/rest_api/types/provider_api_key.py +9 -0
- opik/rest_api/types/provider_api_key_provider.py +1 -1
- opik/rest_api/types/provider_api_key_public.py +9 -0
- opik/rest_api/types/provider_api_key_public_provider.py +1 -1
- opik/rest_api/types/score_name.py +1 -0
- opik/rest_api/types/service_toggles_config.py +18 -0
- opik/rest_api/types/span.py +1 -2
- opik/rest_api/types/span_enrichment_options.py +31 -0
- opik/rest_api/types/span_experiment_item_bulk_write_view.py +1 -2
- opik/rest_api/types/span_filter.py +23 -0
- opik/rest_api/types/span_filter_operator.py +21 -0
- opik/rest_api/types/span_filter_write.py +23 -0
- opik/rest_api/types/span_filter_write_operator.py +21 -0
- opik/rest_api/types/span_llm_as_judge_code.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_public.py +27 -0
- opik/rest_api/types/span_llm_as_judge_code_write.py +27 -0
- opik/rest_api/types/span_public.py +1 -2
- opik/rest_api/types/span_update.py +46 -0
- opik/rest_api/types/span_user_defined_metric_python_code.py +20 -0
- opik/rest_api/types/span_user_defined_metric_python_code_public.py +20 -0
- opik/rest_api/types/span_user_defined_metric_python_code_write.py +20 -0
- opik/rest_api/types/span_write.py +1 -2
- opik/rest_api/types/studio_evaluation.py +20 -0
- opik/rest_api/types/studio_evaluation_public.py +20 -0
- opik/rest_api/types/studio_evaluation_write.py +20 -0
- opik/rest_api/types/studio_llm_model.py +21 -0
- opik/rest_api/types/studio_llm_model_public.py +21 -0
- opik/rest_api/types/studio_llm_model_write.py +21 -0
- opik/rest_api/types/studio_message.py +20 -0
- opik/rest_api/types/studio_message_public.py +20 -0
- opik/rest_api/types/studio_message_write.py +20 -0
- opik/rest_api/types/studio_metric.py +21 -0
- opik/rest_api/types/studio_metric_public.py +21 -0
- opik/rest_api/types/studio_metric_write.py +21 -0
- opik/rest_api/types/studio_optimizer.py +21 -0
- opik/rest_api/types/studio_optimizer_public.py +21 -0
- opik/rest_api/types/studio_optimizer_write.py +21 -0
- opik/rest_api/types/studio_prompt.py +20 -0
- opik/rest_api/types/studio_prompt_public.py +20 -0
- opik/rest_api/types/studio_prompt_write.py +20 -0
- opik/rest_api/types/trace.py +11 -2
- opik/rest_api/types/trace_enrichment_options.py +32 -0
- opik/rest_api/types/trace_experiment_item_bulk_write_view.py +1 -2
- opik/rest_api/types/trace_filter.py +23 -0
- opik/rest_api/types/trace_filter_operator.py +21 -0
- opik/rest_api/types/trace_filter_write.py +23 -0
- opik/rest_api/types/trace_filter_write_operator.py +21 -0
- opik/rest_api/types/trace_public.py +11 -2
- opik/rest_api/types/trace_thread_filter_write.py +23 -0
- opik/rest_api/types/trace_thread_filter_write_operator.py +21 -0
- opik/rest_api/types/trace_thread_identifier.py +1 -0
- opik/rest_api/types/trace_update.py +39 -0
- opik/rest_api/types/trace_write.py +1 -2
- opik/rest_api/types/value_entry.py +2 -0
- opik/rest_api/types/value_entry_compare.py +2 -0
- opik/rest_api/types/value_entry_experiment_item_bulk_write_view.py +2 -0
- opik/rest_api/types/value_entry_public.py +2 -0
- opik/rest_api/types/video_url.py +19 -0
- opik/rest_api/types/video_url_public.py +19 -0
- opik/rest_api/types/video_url_write.py +19 -0
- opik/rest_api/types/webhook.py +28 -0
- opik/rest_api/types/webhook_examples.py +19 -0
- opik/rest_api/types/webhook_public.py +28 -0
- opik/rest_api/types/webhook_test_result.py +23 -0
- opik/rest_api/types/webhook_test_result_status.py +5 -0
- opik/rest_api/types/webhook_write.py +23 -0
- opik/rest_api/types/welcome_wizard_tracking.py +22 -0
- opik/rest_api/types/workspace_configuration.py +5 -0
- opik/rest_api/welcome_wizard/__init__.py +4 -0
- opik/rest_api/welcome_wizard/client.py +195 -0
- opik/rest_api/welcome_wizard/raw_client.py +208 -0
- opik/rest_api/workspaces/client.py +14 -2
- opik/rest_api/workspaces/raw_client.py +10 -0
- opik/s3_httpx_client.py +14 -1
- opik/simulation/__init__.py +6 -0
- opik/simulation/simulated_user.py +99 -0
- opik/simulation/simulator.py +108 -0
- opik/synchronization.py +5 -6
- opik/{decorator/tracing_runtime_config.py → tracing_runtime_config.py} +6 -7
- opik/types.py +36 -0
- opik/validation/chat_prompt_messages.py +241 -0
- opik/validation/feedback_score.py +3 -3
- opik/validation/validator.py +28 -0
- opik-1.9.71.dist-info/METADATA +370 -0
- opik-1.9.71.dist-info/RECORD +1110 -0
- opik/api_objects/prompt/prompt.py +0 -112
- opik/cli.py +0 -193
- opik/hooks.py +0 -13
- opik/integrations/bedrock/chunks_aggregator.py +0 -55
- opik/integrations/bedrock/helpers.py +0 -8
- opik/rest_api/types/automation_rule_evaluator_object_public.py +0 -100
- opik/rest_api/types/json_node_experiment_item_bulk_write_view.py +0 -5
- opik-1.8.39.dist-info/METADATA +0 -339
- opik-1.8.39.dist-info/RECORD +0 -790
- /opik/{evaluation/metrics/conversation/conversational_coherence → decorator/context_manager}/__init__.py +0 -0
- /opik/evaluation/metrics/conversation/{session_completeness → llm_judges/conversational_coherence}/__init__.py +0 -0
- /opik/evaluation/metrics/conversation/{conversational_coherence → llm_judges/conversational_coherence}/schema.py +0 -0
- /opik/evaluation/metrics/conversation/{user_frustration → llm_judges/session_completeness}/__init__.py +0 -0
- /opik/evaluation/metrics/conversation/{session_completeness → llm_judges/session_completeness}/schema.py +0 -0
- /opik/evaluation/metrics/conversation/{user_frustration → llm_judges/user_frustration}/schema.py +0 -0
- /opik/integrations/bedrock/{stream_wrappers.py → converse/stream_wrappers.py} +0 -0
- /opik/rest_api/{spans/types → types}/span_update_type.py +0 -0
- {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/WHEEL +0 -0
- {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/entry_points.txt +0 -0
- {opik-1.8.39.dist-info → opik-1.9.71.dist-info}/licenses/LICENSE +0 -0
- {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
|