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