attune-ai 2.0.0__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.
- attune/__init__.py +358 -0
- attune/adaptive/__init__.py +13 -0
- attune/adaptive/task_complexity.py +127 -0
- attune/agent_monitoring.py +414 -0
- attune/cache/__init__.py +117 -0
- attune/cache/base.py +166 -0
- attune/cache/dependency_manager.py +256 -0
- attune/cache/hash_only.py +251 -0
- attune/cache/hybrid.py +457 -0
- attune/cache/storage.py +285 -0
- attune/cache_monitor.py +356 -0
- attune/cache_stats.py +298 -0
- attune/cli/__init__.py +152 -0
- attune/cli/__main__.py +12 -0
- attune/cli/commands/__init__.py +1 -0
- attune/cli/commands/batch.py +264 -0
- attune/cli/commands/cache.py +248 -0
- attune/cli/commands/help.py +331 -0
- attune/cli/commands/info.py +140 -0
- attune/cli/commands/inspect.py +436 -0
- attune/cli/commands/inspection.py +57 -0
- attune/cli/commands/memory.py +48 -0
- attune/cli/commands/metrics.py +92 -0
- attune/cli/commands/orchestrate.py +184 -0
- attune/cli/commands/patterns.py +207 -0
- attune/cli/commands/profiling.py +202 -0
- attune/cli/commands/provider.py +98 -0
- attune/cli/commands/routing.py +285 -0
- attune/cli/commands/setup.py +96 -0
- attune/cli/commands/status.py +235 -0
- attune/cli/commands/sync.py +166 -0
- attune/cli/commands/tier.py +121 -0
- attune/cli/commands/utilities.py +114 -0
- attune/cli/commands/workflow.py +579 -0
- attune/cli/core.py +32 -0
- attune/cli/parsers/__init__.py +68 -0
- attune/cli/parsers/batch.py +118 -0
- attune/cli/parsers/cache.py +65 -0
- attune/cli/parsers/help.py +41 -0
- attune/cli/parsers/info.py +26 -0
- attune/cli/parsers/inspect.py +66 -0
- attune/cli/parsers/metrics.py +42 -0
- attune/cli/parsers/orchestrate.py +61 -0
- attune/cli/parsers/patterns.py +54 -0
- attune/cli/parsers/provider.py +40 -0
- attune/cli/parsers/routing.py +110 -0
- attune/cli/parsers/setup.py +42 -0
- attune/cli/parsers/status.py +47 -0
- attune/cli/parsers/sync.py +31 -0
- attune/cli/parsers/tier.py +33 -0
- attune/cli/parsers/workflow.py +77 -0
- attune/cli/utils/__init__.py +1 -0
- attune/cli/utils/data.py +242 -0
- attune/cli/utils/helpers.py +68 -0
- attune/cli_legacy.py +3957 -0
- attune/cli_minimal.py +1159 -0
- attune/cli_router.py +437 -0
- attune/cli_unified.py +814 -0
- attune/config/__init__.py +66 -0
- attune/config/xml_config.py +286 -0
- attune/config.py +545 -0
- attune/coordination.py +870 -0
- attune/core.py +1511 -0
- attune/core_modules/__init__.py +15 -0
- attune/cost_tracker.py +626 -0
- attune/dashboard/__init__.py +41 -0
- attune/dashboard/app.py +512 -0
- attune/dashboard/simple_server.py +435 -0
- attune/dashboard/standalone_server.py +547 -0
- attune/discovery.py +306 -0
- attune/emergence.py +306 -0
- attune/exceptions.py +123 -0
- attune/feedback_loops.py +373 -0
- attune/hot_reload/README.md +473 -0
- attune/hot_reload/__init__.py +62 -0
- attune/hot_reload/config.py +83 -0
- attune/hot_reload/integration.py +229 -0
- attune/hot_reload/reloader.py +298 -0
- attune/hot_reload/watcher.py +183 -0
- attune/hot_reload/websocket.py +177 -0
- attune/levels.py +577 -0
- attune/leverage_points.py +441 -0
- attune/logging_config.py +261 -0
- attune/mcp/__init__.py +10 -0
- attune/mcp/server.py +506 -0
- attune/memory/__init__.py +237 -0
- attune/memory/claude_memory.py +469 -0
- attune/memory/config.py +224 -0
- attune/memory/control_panel.py +1290 -0
- attune/memory/control_panel_support.py +145 -0
- attune/memory/cross_session.py +845 -0
- attune/memory/edges.py +179 -0
- attune/memory/encryption.py +159 -0
- attune/memory/file_session.py +770 -0
- attune/memory/graph.py +570 -0
- attune/memory/long_term.py +913 -0
- attune/memory/long_term_types.py +99 -0
- attune/memory/mixins/__init__.py +25 -0
- attune/memory/mixins/backend_init_mixin.py +249 -0
- attune/memory/mixins/capabilities_mixin.py +208 -0
- attune/memory/mixins/handoff_mixin.py +208 -0
- attune/memory/mixins/lifecycle_mixin.py +49 -0
- attune/memory/mixins/long_term_mixin.py +352 -0
- attune/memory/mixins/promotion_mixin.py +109 -0
- attune/memory/mixins/short_term_mixin.py +182 -0
- attune/memory/nodes.py +179 -0
- attune/memory/redis_bootstrap.py +540 -0
- attune/memory/security/__init__.py +31 -0
- attune/memory/security/audit_logger.py +932 -0
- attune/memory/security/pii_scrubber.py +640 -0
- attune/memory/security/secrets_detector.py +678 -0
- attune/memory/short_term.py +2192 -0
- attune/memory/simple_storage.py +302 -0
- attune/memory/storage/__init__.py +15 -0
- attune/memory/storage_backend.py +167 -0
- attune/memory/summary_index.py +583 -0
- attune/memory/types.py +446 -0
- attune/memory/unified.py +182 -0
- attune/meta_workflows/__init__.py +74 -0
- attune/meta_workflows/agent_creator.py +248 -0
- attune/meta_workflows/builtin_templates.py +567 -0
- attune/meta_workflows/cli_commands/__init__.py +56 -0
- attune/meta_workflows/cli_commands/agent_commands.py +321 -0
- attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
- attune/meta_workflows/cli_commands/config_commands.py +232 -0
- attune/meta_workflows/cli_commands/memory_commands.py +182 -0
- attune/meta_workflows/cli_commands/template_commands.py +354 -0
- attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
- attune/meta_workflows/cli_meta_workflows.py +59 -0
- attune/meta_workflows/form_engine.py +292 -0
- attune/meta_workflows/intent_detector.py +409 -0
- attune/meta_workflows/models.py +569 -0
- attune/meta_workflows/pattern_learner.py +738 -0
- attune/meta_workflows/plan_generator.py +384 -0
- attune/meta_workflows/session_context.py +397 -0
- attune/meta_workflows/template_registry.py +229 -0
- attune/meta_workflows/workflow.py +984 -0
- attune/metrics/__init__.py +12 -0
- attune/metrics/collector.py +31 -0
- attune/metrics/prompt_metrics.py +194 -0
- attune/models/__init__.py +172 -0
- attune/models/__main__.py +13 -0
- attune/models/adaptive_routing.py +437 -0
- attune/models/auth_cli.py +444 -0
- attune/models/auth_strategy.py +450 -0
- attune/models/cli.py +655 -0
- attune/models/empathy_executor.py +354 -0
- attune/models/executor.py +257 -0
- attune/models/fallback.py +762 -0
- attune/models/provider_config.py +282 -0
- attune/models/registry.py +472 -0
- attune/models/tasks.py +359 -0
- attune/models/telemetry/__init__.py +71 -0
- attune/models/telemetry/analytics.py +594 -0
- attune/models/telemetry/backend.py +196 -0
- attune/models/telemetry/data_models.py +431 -0
- attune/models/telemetry/storage.py +489 -0
- attune/models/token_estimator.py +420 -0
- attune/models/validation.py +280 -0
- attune/monitoring/__init__.py +52 -0
- attune/monitoring/alerts.py +946 -0
- attune/monitoring/alerts_cli.py +448 -0
- attune/monitoring/multi_backend.py +271 -0
- attune/monitoring/otel_backend.py +362 -0
- attune/optimization/__init__.py +19 -0
- attune/optimization/context_optimizer.py +272 -0
- attune/orchestration/__init__.py +67 -0
- attune/orchestration/agent_templates.py +707 -0
- attune/orchestration/config_store.py +499 -0
- attune/orchestration/execution_strategies.py +2111 -0
- attune/orchestration/meta_orchestrator.py +1168 -0
- attune/orchestration/pattern_learner.py +696 -0
- attune/orchestration/real_tools.py +931 -0
- attune/pattern_cache.py +187 -0
- attune/pattern_library.py +542 -0
- attune/patterns/debugging/all_patterns.json +81 -0
- attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
- attune/patterns/refactoring_memory.json +89 -0
- attune/persistence.py +564 -0
- attune/platform_utils.py +265 -0
- attune/plugins/__init__.py +28 -0
- attune/plugins/base.py +361 -0
- attune/plugins/registry.py +268 -0
- attune/project_index/__init__.py +32 -0
- attune/project_index/cli.py +335 -0
- attune/project_index/index.py +667 -0
- attune/project_index/models.py +504 -0
- attune/project_index/reports.py +474 -0
- attune/project_index/scanner.py +777 -0
- attune/project_index/scanner_parallel.py +291 -0
- attune/prompts/__init__.py +61 -0
- attune/prompts/config.py +77 -0
- attune/prompts/context.py +177 -0
- attune/prompts/parser.py +285 -0
- attune/prompts/registry.py +313 -0
- attune/prompts/templates.py +208 -0
- attune/redis_config.py +302 -0
- attune/redis_memory.py +799 -0
- attune/resilience/__init__.py +56 -0
- attune/resilience/circuit_breaker.py +256 -0
- attune/resilience/fallback.py +179 -0
- attune/resilience/health.py +300 -0
- attune/resilience/retry.py +209 -0
- attune/resilience/timeout.py +135 -0
- attune/routing/__init__.py +43 -0
- attune/routing/chain_executor.py +433 -0
- attune/routing/classifier.py +217 -0
- attune/routing/smart_router.py +234 -0
- attune/routing/workflow_registry.py +343 -0
- attune/scaffolding/README.md +589 -0
- attune/scaffolding/__init__.py +35 -0
- attune/scaffolding/__main__.py +14 -0
- attune/scaffolding/cli.py +240 -0
- attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
- attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
- attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
- attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
- attune/socratic/__init__.py +256 -0
- attune/socratic/ab_testing.py +958 -0
- attune/socratic/blueprint.py +533 -0
- attune/socratic/cli.py +703 -0
- attune/socratic/collaboration.py +1114 -0
- attune/socratic/domain_templates.py +924 -0
- attune/socratic/embeddings.py +738 -0
- attune/socratic/engine.py +794 -0
- attune/socratic/explainer.py +682 -0
- attune/socratic/feedback.py +772 -0
- attune/socratic/forms.py +629 -0
- attune/socratic/generator.py +732 -0
- attune/socratic/llm_analyzer.py +637 -0
- attune/socratic/mcp_server.py +702 -0
- attune/socratic/session.py +312 -0
- attune/socratic/storage.py +667 -0
- attune/socratic/success.py +730 -0
- attune/socratic/visual_editor.py +860 -0
- attune/socratic/web_ui.py +958 -0
- attune/telemetry/__init__.py +39 -0
- attune/telemetry/agent_coordination.py +475 -0
- attune/telemetry/agent_tracking.py +367 -0
- attune/telemetry/approval_gates.py +545 -0
- attune/telemetry/cli.py +1231 -0
- attune/telemetry/commands/__init__.py +14 -0
- attune/telemetry/commands/dashboard_commands.py +696 -0
- attune/telemetry/event_streaming.py +409 -0
- attune/telemetry/feedback_loop.py +567 -0
- attune/telemetry/usage_tracker.py +591 -0
- attune/templates.py +754 -0
- attune/test_generator/__init__.py +38 -0
- attune/test_generator/__main__.py +14 -0
- attune/test_generator/cli.py +234 -0
- attune/test_generator/generator.py +355 -0
- attune/test_generator/risk_analyzer.py +216 -0
- attune/test_generator/templates/unit_test.py.jinja2 +272 -0
- attune/tier_recommender.py +384 -0
- attune/tools.py +183 -0
- attune/trust/__init__.py +28 -0
- attune/trust/circuit_breaker.py +579 -0
- attune/trust_building.py +527 -0
- attune/validation/__init__.py +19 -0
- attune/validation/xml_validator.py +281 -0
- attune/vscode_bridge.py +173 -0
- attune/workflow_commands.py +780 -0
- attune/workflow_patterns/__init__.py +33 -0
- attune/workflow_patterns/behavior.py +249 -0
- attune/workflow_patterns/core.py +76 -0
- attune/workflow_patterns/output.py +99 -0
- attune/workflow_patterns/registry.py +255 -0
- attune/workflow_patterns/structural.py +288 -0
- attune/workflows/__init__.py +539 -0
- attune/workflows/autonomous_test_gen.py +1268 -0
- attune/workflows/base.py +2667 -0
- attune/workflows/batch_processing.py +342 -0
- attune/workflows/bug_predict.py +1084 -0
- attune/workflows/builder.py +273 -0
- attune/workflows/caching.py +253 -0
- attune/workflows/code_review.py +1048 -0
- attune/workflows/code_review_adapters.py +312 -0
- attune/workflows/code_review_pipeline.py +722 -0
- attune/workflows/config.py +645 -0
- attune/workflows/dependency_check.py +644 -0
- attune/workflows/document_gen/__init__.py +25 -0
- attune/workflows/document_gen/config.py +30 -0
- attune/workflows/document_gen/report_formatter.py +162 -0
- attune/workflows/document_gen/workflow.py +1426 -0
- attune/workflows/document_manager.py +216 -0
- attune/workflows/document_manager_README.md +134 -0
- attune/workflows/documentation_orchestrator.py +1205 -0
- attune/workflows/history.py +510 -0
- attune/workflows/keyboard_shortcuts/__init__.py +39 -0
- attune/workflows/keyboard_shortcuts/generators.py +391 -0
- attune/workflows/keyboard_shortcuts/parsers.py +416 -0
- attune/workflows/keyboard_shortcuts/prompts.py +295 -0
- attune/workflows/keyboard_shortcuts/schema.py +193 -0
- attune/workflows/keyboard_shortcuts/workflow.py +509 -0
- attune/workflows/llm_base.py +363 -0
- attune/workflows/manage_docs.py +87 -0
- attune/workflows/manage_docs_README.md +134 -0
- attune/workflows/manage_documentation.py +821 -0
- attune/workflows/new_sample_workflow1.py +149 -0
- attune/workflows/new_sample_workflow1_README.md +150 -0
- attune/workflows/orchestrated_health_check.py +849 -0
- attune/workflows/orchestrated_release_prep.py +600 -0
- attune/workflows/output.py +413 -0
- attune/workflows/perf_audit.py +863 -0
- attune/workflows/pr_review.py +762 -0
- attune/workflows/progress.py +785 -0
- attune/workflows/progress_server.py +322 -0
- attune/workflows/progressive/README 2.md +454 -0
- attune/workflows/progressive/README.md +454 -0
- attune/workflows/progressive/__init__.py +82 -0
- attune/workflows/progressive/cli.py +219 -0
- attune/workflows/progressive/core.py +488 -0
- attune/workflows/progressive/orchestrator.py +723 -0
- attune/workflows/progressive/reports.py +520 -0
- attune/workflows/progressive/telemetry.py +274 -0
- attune/workflows/progressive/test_gen.py +495 -0
- attune/workflows/progressive/workflow.py +589 -0
- attune/workflows/refactor_plan.py +694 -0
- attune/workflows/release_prep.py +895 -0
- attune/workflows/release_prep_crew.py +969 -0
- attune/workflows/research_synthesis.py +404 -0
- attune/workflows/routing.py +168 -0
- attune/workflows/secure_release.py +593 -0
- attune/workflows/security_adapters.py +297 -0
- attune/workflows/security_audit.py +1329 -0
- attune/workflows/security_audit_phase3.py +355 -0
- attune/workflows/seo_optimization.py +633 -0
- attune/workflows/step_config.py +234 -0
- attune/workflows/telemetry_mixin.py +269 -0
- attune/workflows/test5.py +125 -0
- attune/workflows/test5_README.md +158 -0
- attune/workflows/test_coverage_boost_crew.py +849 -0
- attune/workflows/test_gen/__init__.py +52 -0
- attune/workflows/test_gen/ast_analyzer.py +249 -0
- attune/workflows/test_gen/config.py +88 -0
- attune/workflows/test_gen/data_models.py +38 -0
- attune/workflows/test_gen/report_formatter.py +289 -0
- attune/workflows/test_gen/test_templates.py +381 -0
- attune/workflows/test_gen/workflow.py +655 -0
- attune/workflows/test_gen.py +54 -0
- attune/workflows/test_gen_behavioral.py +477 -0
- attune/workflows/test_gen_parallel.py +341 -0
- attune/workflows/test_lifecycle.py +526 -0
- attune/workflows/test_maintenance.py +627 -0
- attune/workflows/test_maintenance_cli.py +590 -0
- attune/workflows/test_maintenance_crew.py +840 -0
- attune/workflows/test_runner.py +622 -0
- attune/workflows/tier_tracking.py +531 -0
- attune/workflows/xml_enhanced_crew.py +285 -0
- attune_ai-2.0.0.dist-info/METADATA +1026 -0
- attune_ai-2.0.0.dist-info/RECORD +457 -0
- attune_ai-2.0.0.dist-info/WHEEL +5 -0
- attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
- attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
- attune_ai-2.0.0.dist-info/top_level.txt +5 -0
- attune_healthcare/__init__.py +13 -0
- attune_healthcare/monitors/__init__.py +9 -0
- attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
- attune_healthcare/monitors/monitoring/__init__.py +44 -0
- attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
- attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
- attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
- attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
- attune_llm/README.md +553 -0
- attune_llm/__init__.py +28 -0
- attune_llm/agent_factory/__init__.py +53 -0
- attune_llm/agent_factory/adapters/__init__.py +85 -0
- attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
- attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
- attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
- attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
- attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
- attune_llm/agent_factory/adapters/native.py +228 -0
- attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
- attune_llm/agent_factory/base.py +305 -0
- attune_llm/agent_factory/crews/__init__.py +67 -0
- attune_llm/agent_factory/crews/code_review.py +1113 -0
- attune_llm/agent_factory/crews/health_check.py +1262 -0
- attune_llm/agent_factory/crews/refactoring.py +1128 -0
- attune_llm/agent_factory/crews/security_audit.py +1018 -0
- attune_llm/agent_factory/decorators.py +287 -0
- attune_llm/agent_factory/factory.py +558 -0
- attune_llm/agent_factory/framework.py +193 -0
- attune_llm/agent_factory/memory_integration.py +328 -0
- attune_llm/agent_factory/resilient.py +320 -0
- attune_llm/agents_md/__init__.py +22 -0
- attune_llm/agents_md/loader.py +218 -0
- attune_llm/agents_md/parser.py +271 -0
- attune_llm/agents_md/registry.py +307 -0
- attune_llm/claude_memory.py +466 -0
- attune_llm/cli/__init__.py +8 -0
- attune_llm/cli/sync_claude.py +487 -0
- attune_llm/code_health.py +1313 -0
- attune_llm/commands/__init__.py +51 -0
- attune_llm/commands/context.py +375 -0
- attune_llm/commands/loader.py +301 -0
- attune_llm/commands/models.py +231 -0
- attune_llm/commands/parser.py +371 -0
- attune_llm/commands/registry.py +429 -0
- attune_llm/config/__init__.py +29 -0
- attune_llm/config/unified.py +291 -0
- attune_llm/context/__init__.py +22 -0
- attune_llm/context/compaction.py +455 -0
- attune_llm/context/manager.py +434 -0
- attune_llm/contextual_patterns.py +361 -0
- attune_llm/core.py +907 -0
- attune_llm/git_pattern_extractor.py +435 -0
- attune_llm/hooks/__init__.py +24 -0
- attune_llm/hooks/config.py +306 -0
- attune_llm/hooks/executor.py +289 -0
- attune_llm/hooks/registry.py +302 -0
- attune_llm/hooks/scripts/__init__.py +39 -0
- attune_llm/hooks/scripts/evaluate_session.py +201 -0
- attune_llm/hooks/scripts/first_time_init.py +285 -0
- attune_llm/hooks/scripts/pre_compact.py +207 -0
- attune_llm/hooks/scripts/session_end.py +183 -0
- attune_llm/hooks/scripts/session_start.py +163 -0
- attune_llm/hooks/scripts/suggest_compact.py +225 -0
- attune_llm/learning/__init__.py +30 -0
- attune_llm/learning/evaluator.py +438 -0
- attune_llm/learning/extractor.py +514 -0
- attune_llm/learning/storage.py +560 -0
- attune_llm/levels.py +227 -0
- attune_llm/pattern_confidence.py +414 -0
- attune_llm/pattern_resolver.py +272 -0
- attune_llm/pattern_summary.py +350 -0
- attune_llm/providers.py +967 -0
- attune_llm/routing/__init__.py +32 -0
- attune_llm/routing/model_router.py +362 -0
- attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
- attune_llm/security/PHASE2_COMPLETE.md +384 -0
- attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
- attune_llm/security/QUICK_REFERENCE.md +316 -0
- attune_llm/security/README.md +262 -0
- attune_llm/security/__init__.py +62 -0
- attune_llm/security/audit_logger.py +929 -0
- attune_llm/security/audit_logger_example.py +152 -0
- attune_llm/security/pii_scrubber.py +640 -0
- attune_llm/security/secrets_detector.py +678 -0
- attune_llm/security/secrets_detector_example.py +304 -0
- attune_llm/security/secure_memdocs.py +1192 -0
- attune_llm/security/secure_memdocs_example.py +278 -0
- attune_llm/session_status.py +745 -0
- attune_llm/state.py +246 -0
- attune_llm/utils/__init__.py +5 -0
- attune_llm/utils/tokens.py +349 -0
- attune_software/SOFTWARE_PLUGIN_README.md +57 -0
- attune_software/__init__.py +13 -0
- attune_software/cli/__init__.py +120 -0
- attune_software/cli/inspect.py +362 -0
- attune_software/cli.py +574 -0
- attune_software/plugin.py +188 -0
- workflow_scaffolding/__init__.py +11 -0
- workflow_scaffolding/__main__.py +12 -0
- workflow_scaffolding/cli.py +206 -0
- workflow_scaffolding/generator.py +265 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"""Telemetry analytics and reporting.
|
|
2
|
+
|
|
3
|
+
Analytics functions for telemetry data analysis.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart-AI-Memory
|
|
6
|
+
Licensed under Fair Source License 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import heapq
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .storage import TelemetryStore
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TelemetryAnalytics:
|
|
17
|
+
"""Analytics helpers for telemetry data.
|
|
18
|
+
|
|
19
|
+
Provides insights into cost optimization, provider usage, and performance.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, store: TelemetryStore | None = None):
|
|
23
|
+
"""Initialize analytics.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
store: TelemetryStore to analyze (creates default if None)
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
self.store = store or TelemetryStore()
|
|
30
|
+
|
|
31
|
+
def top_expensive_workflows(
|
|
32
|
+
self,
|
|
33
|
+
n: int = 10,
|
|
34
|
+
since: datetime | None = None,
|
|
35
|
+
) -> list[dict[str, Any]]:
|
|
36
|
+
"""Get the most expensive workflows.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
n: Number of workflows to return
|
|
40
|
+
since: Only consider workflows after this time
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of dicts with workflow_name, total_cost, run_count
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
workflows = self.store.get_workflows(since=since, limit=10000)
|
|
47
|
+
|
|
48
|
+
# Aggregate by workflow name
|
|
49
|
+
costs: dict[str, dict[str, Any]] = {}
|
|
50
|
+
for wf in workflows:
|
|
51
|
+
if wf.workflow_name not in costs:
|
|
52
|
+
costs[wf.workflow_name] = {
|
|
53
|
+
"workflow_name": wf.workflow_name,
|
|
54
|
+
"total_cost": 0.0,
|
|
55
|
+
"run_count": 0,
|
|
56
|
+
"total_savings": 0.0,
|
|
57
|
+
"avg_duration_ms": 0,
|
|
58
|
+
}
|
|
59
|
+
costs[wf.workflow_name]["total_cost"] += wf.total_cost
|
|
60
|
+
costs[wf.workflow_name]["run_count"] += 1
|
|
61
|
+
costs[wf.workflow_name]["total_savings"] += wf.savings
|
|
62
|
+
|
|
63
|
+
# Calculate averages and sort
|
|
64
|
+
result = list(costs.values())
|
|
65
|
+
for item in result:
|
|
66
|
+
if item["run_count"] > 0:
|
|
67
|
+
item["avg_cost"] = item["total_cost"] / item["run_count"]
|
|
68
|
+
|
|
69
|
+
result.sort(key=lambda x: x["total_cost"], reverse=True)
|
|
70
|
+
return result[:n]
|
|
71
|
+
|
|
72
|
+
def provider_usage_summary(
|
|
73
|
+
self,
|
|
74
|
+
since: datetime | None = None,
|
|
75
|
+
) -> dict[str, dict[str, Any]]:
|
|
76
|
+
"""Get usage summary by provider.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
since: Only consider calls after this time
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dict mapping provider to usage stats
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
calls = self.store.get_calls(since=since, limit=100000)
|
|
86
|
+
|
|
87
|
+
summary: dict[str, dict[str, Any]] = {}
|
|
88
|
+
for call in calls:
|
|
89
|
+
if call.provider not in summary:
|
|
90
|
+
summary[call.provider] = {
|
|
91
|
+
"call_count": 0,
|
|
92
|
+
"total_tokens": 0,
|
|
93
|
+
"total_cost": 0.0,
|
|
94
|
+
"error_count": 0,
|
|
95
|
+
"avg_latency_ms": 0,
|
|
96
|
+
"by_tier": {"cheap": 0, "capable": 0, "premium": 0},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
s = summary[call.provider]
|
|
100
|
+
s["call_count"] += 1
|
|
101
|
+
s["total_tokens"] += call.input_tokens + call.output_tokens
|
|
102
|
+
s["total_cost"] += call.estimated_cost
|
|
103
|
+
if not call.success:
|
|
104
|
+
s["error_count"] += 1
|
|
105
|
+
if call.tier in s["by_tier"]:
|
|
106
|
+
s["by_tier"][call.tier] += 1
|
|
107
|
+
|
|
108
|
+
# Calculate averages
|
|
109
|
+
for _provider, stats in summary.items():
|
|
110
|
+
if stats["call_count"] > 0:
|
|
111
|
+
stats["avg_cost"] = stats["total_cost"] / stats["call_count"]
|
|
112
|
+
|
|
113
|
+
return summary
|
|
114
|
+
|
|
115
|
+
def tier_distribution(
|
|
116
|
+
self,
|
|
117
|
+
since: datetime | None = None,
|
|
118
|
+
) -> dict[str, dict[str, Any]]:
|
|
119
|
+
"""Get call distribution by tier.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
since: Only consider calls after this time
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dict mapping tier to stats
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
calls = self.store.get_calls(since=since, limit=100000)
|
|
129
|
+
|
|
130
|
+
dist: dict[str, dict[str, Any]] = {
|
|
131
|
+
"cheap": {"count": 0, "cost": 0.0, "tokens": 0},
|
|
132
|
+
"capable": {"count": 0, "cost": 0.0, "tokens": 0},
|
|
133
|
+
"premium": {"count": 0, "cost": 0.0, "tokens": 0},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for call in calls:
|
|
137
|
+
if call.tier in dist:
|
|
138
|
+
dist[call.tier]["count"] += 1
|
|
139
|
+
dist[call.tier]["cost"] += call.estimated_cost
|
|
140
|
+
dist[call.tier]["tokens"] += call.input_tokens + call.output_tokens
|
|
141
|
+
|
|
142
|
+
total_calls = sum(d["count"] for d in dist.values())
|
|
143
|
+
for _tier, stats in dist.items():
|
|
144
|
+
stats["percent"] = (stats["count"] / total_calls * 100) if total_calls > 0 else 0
|
|
145
|
+
|
|
146
|
+
return dist
|
|
147
|
+
|
|
148
|
+
def fallback_stats(
|
|
149
|
+
self,
|
|
150
|
+
since: datetime | None = None,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""Get fallback usage statistics.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
since: Only consider calls after this time
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Dict with fallback stats
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
calls = self.store.get_calls(since=since, limit=100000)
|
|
162
|
+
|
|
163
|
+
total = len(calls)
|
|
164
|
+
fallback_count = sum(1 for c in calls if c.fallback_used)
|
|
165
|
+
error_count = sum(1 for c in calls if not c.success)
|
|
166
|
+
|
|
167
|
+
# Count by original provider
|
|
168
|
+
by_provider: dict[str, int] = {}
|
|
169
|
+
for call in calls:
|
|
170
|
+
if call.fallback_used and call.original_provider:
|
|
171
|
+
by_provider[call.original_provider] = by_provider.get(call.original_provider, 0) + 1
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"total_calls": total,
|
|
175
|
+
"fallback_count": fallback_count,
|
|
176
|
+
"fallback_percent": (fallback_count / total * 100) if total > 0 else 0,
|
|
177
|
+
"error_count": error_count,
|
|
178
|
+
"error_rate": (error_count / total * 100) if total > 0 else 0,
|
|
179
|
+
"by_original_provider": by_provider,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def sonnet_opus_fallback_analysis(
|
|
183
|
+
self,
|
|
184
|
+
since: datetime | None = None,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
"""Analyze Sonnet 4.5 → Opus 4.5 fallback performance and cost savings.
|
|
187
|
+
|
|
188
|
+
Tracks:
|
|
189
|
+
- How often Sonnet 4.5 succeeds vs needs Opus fallback
|
|
190
|
+
- Cost savings from using Sonnet instead of always using Opus
|
|
191
|
+
- Success rates by model
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
since: Only consider calls after this time
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dict with fallback analysis and cost savings
|
|
198
|
+
"""
|
|
199
|
+
calls = self.store.get_calls(since=since, limit=100000)
|
|
200
|
+
|
|
201
|
+
# Filter for Anthropic calls (Sonnet/Opus)
|
|
202
|
+
anthropic_calls = [
|
|
203
|
+
c
|
|
204
|
+
for c in calls
|
|
205
|
+
if c.provider == "anthropic"
|
|
206
|
+
and c.model_id in ["claude-sonnet-4-5", "claude-opus-4-5-20251101"]
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
if not anthropic_calls:
|
|
210
|
+
return {
|
|
211
|
+
"total_calls": 0,
|
|
212
|
+
"sonnet_attempts": 0,
|
|
213
|
+
"sonnet_successes": 0,
|
|
214
|
+
"opus_fallbacks": 0,
|
|
215
|
+
"success_rate_sonnet": 0.0,
|
|
216
|
+
"fallback_rate": 0.0,
|
|
217
|
+
"actual_cost": 0.0,
|
|
218
|
+
"always_opus_cost": 0.0,
|
|
219
|
+
"savings": 0.0,
|
|
220
|
+
"savings_percent": 0.0,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
total = len(anthropic_calls)
|
|
224
|
+
|
|
225
|
+
# Count Sonnet attempts and successes
|
|
226
|
+
sonnet_calls = [c for c in anthropic_calls if c.model_id == "claude-sonnet-4-5"]
|
|
227
|
+
sonnet_successes = sum(1 for c in sonnet_calls if c.success)
|
|
228
|
+
|
|
229
|
+
# Count Opus fallbacks (calls with fallback_used and ended up on Opus)
|
|
230
|
+
opus_fallbacks = sum(
|
|
231
|
+
1
|
|
232
|
+
for c in anthropic_calls
|
|
233
|
+
if c.model_id == "claude-opus-4-5-20251101" and c.fallback_used
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Calculate costs
|
|
237
|
+
actual_cost = sum(c.estimated_cost for c in anthropic_calls)
|
|
238
|
+
|
|
239
|
+
# Calculate what it would cost if everything used Opus
|
|
240
|
+
opus_input_cost = 15.00 / 1_000_000 # per token
|
|
241
|
+
opus_output_cost = 75.00 / 1_000_000 # per token
|
|
242
|
+
always_opus_cost = sum(
|
|
243
|
+
(c.input_tokens * opus_input_cost) + (c.output_tokens * opus_output_cost)
|
|
244
|
+
for c in anthropic_calls
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
savings = always_opus_cost - actual_cost
|
|
248
|
+
savings_percent = (savings / always_opus_cost * 100) if always_opus_cost > 0 else 0
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"total_calls": total,
|
|
252
|
+
"sonnet_attempts": len(sonnet_calls),
|
|
253
|
+
"sonnet_successes": sonnet_successes,
|
|
254
|
+
"opus_fallbacks": opus_fallbacks,
|
|
255
|
+
"success_rate_sonnet": (
|
|
256
|
+
(sonnet_successes / len(sonnet_calls) * 100) if sonnet_calls else 0.0
|
|
257
|
+
),
|
|
258
|
+
"fallback_rate": (opus_fallbacks / total * 100) if total > 0 else 0.0,
|
|
259
|
+
"actual_cost": actual_cost,
|
|
260
|
+
"always_opus_cost": always_opus_cost,
|
|
261
|
+
"savings": savings,
|
|
262
|
+
"savings_percent": savings_percent,
|
|
263
|
+
"avg_cost_per_call": actual_cost / total if total > 0 else 0.0,
|
|
264
|
+
"avg_opus_cost_per_call": always_opus_cost / total if total > 0 else 0.0,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
def cost_savings_report(
|
|
268
|
+
self,
|
|
269
|
+
since: datetime | None = None,
|
|
270
|
+
) -> dict[str, Any]:
|
|
271
|
+
"""Generate cost savings report.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
since: Only consider workflows after this time
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dict with savings analysis
|
|
278
|
+
|
|
279
|
+
"""
|
|
280
|
+
workflows = self.store.get_workflows(since=since, limit=10000)
|
|
281
|
+
|
|
282
|
+
total_cost = sum(wf.total_cost for wf in workflows)
|
|
283
|
+
total_baseline = sum(wf.baseline_cost for wf in workflows)
|
|
284
|
+
total_savings = sum(wf.savings for wf in workflows)
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
"workflow_count": len(workflows),
|
|
288
|
+
"total_actual_cost": total_cost,
|
|
289
|
+
"total_baseline_cost": total_baseline,
|
|
290
|
+
"total_savings": total_savings,
|
|
291
|
+
"savings_percent": (
|
|
292
|
+
(total_savings / total_baseline * 100) if total_baseline > 0 else 0
|
|
293
|
+
),
|
|
294
|
+
"avg_cost_per_workflow": total_cost / len(workflows) if workflows else 0,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Tier 1 automation monitoring analytics
|
|
298
|
+
|
|
299
|
+
def task_routing_accuracy(
|
|
300
|
+
self,
|
|
301
|
+
since: datetime | None = None,
|
|
302
|
+
) -> dict[str, Any]:
|
|
303
|
+
"""Analyze task routing accuracy.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
since: Only consider routings after this time
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Dict with routing accuracy metrics by task type and strategy
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
routings = self.store.get_task_routings(since=since, limit=10000)
|
|
313
|
+
|
|
314
|
+
if not routings:
|
|
315
|
+
return {
|
|
316
|
+
"total_tasks": 0,
|
|
317
|
+
"successful_routing": 0,
|
|
318
|
+
"accuracy_rate": 0.0,
|
|
319
|
+
"avg_confidence": 0.0,
|
|
320
|
+
"by_task_type": {},
|
|
321
|
+
"by_strategy": {},
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
total = len(routings)
|
|
325
|
+
successful = sum(1 for r in routings if r.success)
|
|
326
|
+
total_confidence = sum(r.confidence_score for r in routings)
|
|
327
|
+
|
|
328
|
+
# Aggregate by task type
|
|
329
|
+
by_type: dict[str, dict[str, int | float]] = {}
|
|
330
|
+
for r in routings:
|
|
331
|
+
if r.task_type not in by_type:
|
|
332
|
+
by_type[r.task_type] = {"total": 0, "success": 0}
|
|
333
|
+
by_type[r.task_type]["total"] += 1
|
|
334
|
+
if r.success:
|
|
335
|
+
by_type[r.task_type]["success"] += 1
|
|
336
|
+
|
|
337
|
+
# Calculate rates
|
|
338
|
+
for _task_type, stats in by_type.items():
|
|
339
|
+
stats["rate"] = stats["success"] / stats["total"] if stats["total"] > 0 else 0.0
|
|
340
|
+
|
|
341
|
+
# Aggregate by strategy
|
|
342
|
+
by_strategy: dict[str, dict[str, int]] = {}
|
|
343
|
+
for r in routings:
|
|
344
|
+
if r.routing_strategy not in by_strategy:
|
|
345
|
+
by_strategy[r.routing_strategy] = {"total": 0, "success": 0}
|
|
346
|
+
by_strategy[r.routing_strategy]["total"] += 1
|
|
347
|
+
if r.success:
|
|
348
|
+
by_strategy[r.routing_strategy]["success"] += 1
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"total_tasks": total,
|
|
352
|
+
"successful_routing": successful,
|
|
353
|
+
"accuracy_rate": successful / total if total > 0 else 0.0,
|
|
354
|
+
"avg_confidence": total_confidence / total if total > 0 else 0.0,
|
|
355
|
+
"by_task_type": by_type,
|
|
356
|
+
"by_strategy": by_strategy,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
def test_execution_trends(
|
|
360
|
+
self,
|
|
361
|
+
since: datetime | None = None,
|
|
362
|
+
) -> dict[str, Any]:
|
|
363
|
+
"""Analyze test execution trends.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
since: Only consider executions after this time
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Dict with test execution metrics and trends
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
executions = self.store.get_test_executions(since=since, limit=1000)
|
|
373
|
+
|
|
374
|
+
if not executions:
|
|
375
|
+
return {
|
|
376
|
+
"total_executions": 0,
|
|
377
|
+
"success_rate": 0.0,
|
|
378
|
+
"avg_duration_seconds": 0.0,
|
|
379
|
+
"total_tests_run": 0,
|
|
380
|
+
"total_failures": 0,
|
|
381
|
+
"coverage_trend": "stable",
|
|
382
|
+
"most_failing_tests": [],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
total_execs = len(executions)
|
|
386
|
+
successful_execs = sum(1 for e in executions if e.success)
|
|
387
|
+
total_duration = sum(e.duration_seconds for e in executions)
|
|
388
|
+
total_tests = sum(e.total_tests for e in executions)
|
|
389
|
+
total_failures = sum(e.failed for e in executions)
|
|
390
|
+
|
|
391
|
+
# Find most failing tests
|
|
392
|
+
failure_counts: dict[str, int] = {}
|
|
393
|
+
for exec_rec in executions:
|
|
394
|
+
for test in exec_rec.failed_tests:
|
|
395
|
+
test_name = test.get("name", "unknown")
|
|
396
|
+
failure_counts[test_name] = failure_counts.get(test_name, 0) + 1
|
|
397
|
+
|
|
398
|
+
most_failing = [
|
|
399
|
+
{"name": name, "failures": count}
|
|
400
|
+
for name, count in heapq.nlargest(10, failure_counts.items(), key=lambda x: x[1])
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
"total_executions": total_execs,
|
|
405
|
+
"success_rate": successful_execs / total_execs if total_execs > 0 else 0.0,
|
|
406
|
+
"avg_duration_seconds": total_duration / total_execs if total_execs > 0 else 0.0,
|
|
407
|
+
"total_tests_run": total_tests,
|
|
408
|
+
"total_failures": total_failures,
|
|
409
|
+
"coverage_trend": "stable", # Will be computed from coverage_progress
|
|
410
|
+
"most_failing_tests": most_failing,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
def coverage_progress(
|
|
414
|
+
self,
|
|
415
|
+
since: datetime | None = None,
|
|
416
|
+
) -> dict[str, Any]:
|
|
417
|
+
"""Track coverage progress over time.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
since: Only consider coverage records after this time
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Dict with coverage metrics and trends
|
|
424
|
+
|
|
425
|
+
"""
|
|
426
|
+
records = self.store.get_coverage_history(since=since, limit=1000)
|
|
427
|
+
|
|
428
|
+
if not records:
|
|
429
|
+
return {
|
|
430
|
+
"current_coverage": 0.0,
|
|
431
|
+
"previous_coverage": 0.0,
|
|
432
|
+
"change": 0.0,
|
|
433
|
+
"trend": "no_data",
|
|
434
|
+
"coverage_history": [],
|
|
435
|
+
"files_improved": 0,
|
|
436
|
+
"files_declined": 0,
|
|
437
|
+
"critical_gaps_count": 0,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
# Latest and first records
|
|
441
|
+
latest = records[-1]
|
|
442
|
+
first = records[0]
|
|
443
|
+
current_coverage = latest.overall_percentage
|
|
444
|
+
|
|
445
|
+
# Calculate trend by comparing first to last
|
|
446
|
+
if len(records) == 1:
|
|
447
|
+
# Single record - no trend analysis possible
|
|
448
|
+
prev_coverage = 0.0
|
|
449
|
+
change = 0.0
|
|
450
|
+
trend = "stable"
|
|
451
|
+
else:
|
|
452
|
+
# Multiple records - compare first to last
|
|
453
|
+
prev_coverage = first.overall_percentage
|
|
454
|
+
change = current_coverage - prev_coverage
|
|
455
|
+
|
|
456
|
+
# Determine trend based on change
|
|
457
|
+
if change > 1.0:
|
|
458
|
+
trend = "improving"
|
|
459
|
+
elif change < -1.0:
|
|
460
|
+
trend = "declining"
|
|
461
|
+
else:
|
|
462
|
+
trend = "stable"
|
|
463
|
+
|
|
464
|
+
# Build coverage history from records
|
|
465
|
+
coverage_history = [
|
|
466
|
+
{
|
|
467
|
+
"timestamp": r.timestamp,
|
|
468
|
+
"coverage": r.overall_percentage,
|
|
469
|
+
"trend": r.trend,
|
|
470
|
+
}
|
|
471
|
+
for r in records
|
|
472
|
+
]
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
"current_coverage": current_coverage,
|
|
476
|
+
"previous_coverage": prev_coverage,
|
|
477
|
+
"change": change,
|
|
478
|
+
"trend": trend,
|
|
479
|
+
"coverage_history": coverage_history,
|
|
480
|
+
"files_improved": 0, # Would need file-level history
|
|
481
|
+
"files_declined": 0, # Would need file-level history
|
|
482
|
+
"critical_gaps_count": len(latest.critical_gaps),
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
def agent_performance(
|
|
486
|
+
self,
|
|
487
|
+
since: datetime | None = None,
|
|
488
|
+
) -> dict[str, Any]:
|
|
489
|
+
"""Analyze agent/workflow performance.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
since: Only consider assignments after this time
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Dict with agent performance metrics
|
|
496
|
+
|
|
497
|
+
"""
|
|
498
|
+
assignments = self.store.get_agent_assignments(
|
|
499
|
+
since=since, automated_only=False, limit=10000
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if not assignments:
|
|
503
|
+
return {
|
|
504
|
+
"total_assignments": 0,
|
|
505
|
+
"by_agent": {},
|
|
506
|
+
"automation_rate": 0.0,
|
|
507
|
+
"human_review_rate": 0.0,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# Aggregate by agent
|
|
511
|
+
by_agent: dict[str, dict[str, Any]] = {}
|
|
512
|
+
total_assignments = len(assignments)
|
|
513
|
+
total_automated = 0
|
|
514
|
+
total_human_review = 0
|
|
515
|
+
|
|
516
|
+
for assignment in assignments:
|
|
517
|
+
agent = assignment.assigned_agent
|
|
518
|
+
if agent not in by_agent:
|
|
519
|
+
by_agent[agent] = {
|
|
520
|
+
"assignments": 0,
|
|
521
|
+
"completed": 0,
|
|
522
|
+
"successful": 0,
|
|
523
|
+
"success_rate": 0.0,
|
|
524
|
+
"avg_duration_hours": 0.0,
|
|
525
|
+
"quality_score_avg": 0.0,
|
|
526
|
+
"total_duration": 0.0,
|
|
527
|
+
"quality_scores": [],
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
stats = by_agent[agent]
|
|
531
|
+
stats["assignments"] += 1
|
|
532
|
+
if assignment.status == "completed":
|
|
533
|
+
stats["completed"] += 1
|
|
534
|
+
if assignment.actual_duration_hours is not None:
|
|
535
|
+
stats["total_duration"] += assignment.actual_duration_hours
|
|
536
|
+
|
|
537
|
+
# Track successful assignments (not just completed)
|
|
538
|
+
if assignment.success:
|
|
539
|
+
stats["successful"] += 1
|
|
540
|
+
|
|
541
|
+
if assignment.automated_eligible:
|
|
542
|
+
total_automated += 1
|
|
543
|
+
if assignment.human_review_required:
|
|
544
|
+
total_human_review += 1
|
|
545
|
+
|
|
546
|
+
# Calculate averages
|
|
547
|
+
for _agent, stats in by_agent.items():
|
|
548
|
+
if stats["assignments"] > 0:
|
|
549
|
+
stats["success_rate"] = stats["successful"] / stats["assignments"]
|
|
550
|
+
if stats["completed"] > 0:
|
|
551
|
+
stats["avg_duration_hours"] = stats["total_duration"] / stats["completed"]
|
|
552
|
+
|
|
553
|
+
# Remove helper fields
|
|
554
|
+
del stats["total_duration"]
|
|
555
|
+
del stats["quality_scores"]
|
|
556
|
+
del stats["successful"] # Remove helper field, keep success_rate
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
"total_assignments": total_assignments,
|
|
560
|
+
"by_agent": by_agent,
|
|
561
|
+
"automation_rate": (
|
|
562
|
+
total_automated / total_assignments if total_assignments > 0 else 0.0
|
|
563
|
+
),
|
|
564
|
+
"human_review_rate": (
|
|
565
|
+
total_human_review / total_assignments if total_assignments > 0 else 0.0
|
|
566
|
+
),
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
def tier1_summary(
|
|
570
|
+
self,
|
|
571
|
+
since: datetime | None = None,
|
|
572
|
+
) -> dict[str, Any]:
|
|
573
|
+
"""Comprehensive Tier 1 automation summary.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
since: Only consider records after this time
|
|
577
|
+
|
|
578
|
+
Returns:
|
|
579
|
+
Dict combining all Tier 1 metrics
|
|
580
|
+
|
|
581
|
+
"""
|
|
582
|
+
return {
|
|
583
|
+
"task_routing": self.task_routing_accuracy(since),
|
|
584
|
+
"test_execution": self.test_execution_trends(since),
|
|
585
|
+
"coverage": self.coverage_progress(since),
|
|
586
|
+
"agent_performance": self.agent_performance(since),
|
|
587
|
+
"cost_savings": self.cost_savings_report(since),
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# Singleton for global telemetry
|
|
592
|
+
_telemetry_store: TelemetryStore | None = None
|
|
593
|
+
|
|
594
|
+
|