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,547 @@
|
|
|
1
|
+
"""Standalone Dashboard Server - Reads Directly from Redis.
|
|
2
|
+
|
|
3
|
+
This version bypasses the telemetry API layer and reads directly from Redis.
|
|
4
|
+
Works with data populated by scripts/populate_redis_direct.py.
|
|
5
|
+
|
|
6
|
+
Zero external dependencies (uses Python stdlib only).
|
|
7
|
+
|
|
8
|
+
Copyright 2025 Smart-AI-Memory
|
|
9
|
+
Licensed under Fair Source License 0.9
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from urllib.parse import parse_qs, urlparse
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import redis
|
|
23
|
+
|
|
24
|
+
REDIS_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
REDIS_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StandaloneDashboardHandler(BaseHTTPRequestHandler):
|
|
32
|
+
"""HTTP handler that reads directly from Redis."""
|
|
33
|
+
|
|
34
|
+
# Class variable for Redis connection (shared across requests)
|
|
35
|
+
_redis_client = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_redis(cls):
|
|
39
|
+
"""Get or create Redis connection."""
|
|
40
|
+
if not REDIS_AVAILABLE:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
if cls._redis_client is None:
|
|
44
|
+
try:
|
|
45
|
+
cls._redis_client = redis.Redis(host="localhost", port=6379, decode_responses=False)
|
|
46
|
+
cls._redis_client.ping() # Test connection
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Failed to connect to Redis: {e}")
|
|
49
|
+
cls._redis_client = None
|
|
50
|
+
|
|
51
|
+
return cls._redis_client
|
|
52
|
+
|
|
53
|
+
def do_GET(self):
|
|
54
|
+
"""Handle GET requests."""
|
|
55
|
+
parsed = urlparse(self.path)
|
|
56
|
+
path = parsed.path
|
|
57
|
+
query = parse_qs(parsed.query)
|
|
58
|
+
|
|
59
|
+
# Route requests
|
|
60
|
+
if path == "/" or path == "/index.html":
|
|
61
|
+
self.serve_file("index.html", "text/html")
|
|
62
|
+
elif path == "/static/style.css":
|
|
63
|
+
self.serve_file("style.css", "text/css")
|
|
64
|
+
elif path == "/static/app.js":
|
|
65
|
+
self.serve_file("app.js", "application/javascript")
|
|
66
|
+
elif path == "/api/health":
|
|
67
|
+
self.api_health()
|
|
68
|
+
elif path == "/api/agents":
|
|
69
|
+
self.api_agents()
|
|
70
|
+
elif path.startswith("/api/agents/"):
|
|
71
|
+
agent_id = path.split("/")[-1]
|
|
72
|
+
self.api_agent_detail(agent_id)
|
|
73
|
+
elif path == "/api/signals":
|
|
74
|
+
limit = int(query.get("limit", [50])[0])
|
|
75
|
+
self.api_signals(limit)
|
|
76
|
+
elif path == "/api/events":
|
|
77
|
+
event_type = query.get("event_type", [None])[0]
|
|
78
|
+
limit = int(query.get("limit", [100])[0])
|
|
79
|
+
self.api_events(event_type, limit)
|
|
80
|
+
elif path == "/api/approvals":
|
|
81
|
+
self.api_approvals()
|
|
82
|
+
elif path == "/api/feedback/workflows":
|
|
83
|
+
self.api_feedback_workflows()
|
|
84
|
+
elif path == "/api/feedback/underperforming":
|
|
85
|
+
threshold = float(query.get("threshold", [0.7])[0])
|
|
86
|
+
self.api_underperforming(threshold)
|
|
87
|
+
else:
|
|
88
|
+
self.send_error(404, "Not Found")
|
|
89
|
+
|
|
90
|
+
def do_POST(self):
|
|
91
|
+
"""Handle POST requests."""
|
|
92
|
+
parsed = urlparse(self.path)
|
|
93
|
+
path = parsed.path
|
|
94
|
+
|
|
95
|
+
# Get request body
|
|
96
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
97
|
+
body = self.rfile.read(content_length) if content_length > 0 else b"{}"
|
|
98
|
+
data = json.loads(body.decode("utf-8")) if body else {}
|
|
99
|
+
|
|
100
|
+
# Route requests
|
|
101
|
+
if "/approve" in path:
|
|
102
|
+
request_id = path.split("/")[-2]
|
|
103
|
+
self.api_approve(request_id, data.get("reason", "Approved via dashboard"))
|
|
104
|
+
elif "/reject" in path:
|
|
105
|
+
request_id = path.split("/")[-2]
|
|
106
|
+
self.api_reject(request_id, data.get("reason", "Rejected via dashboard"))
|
|
107
|
+
else:
|
|
108
|
+
self.send_error(404, "Not Found")
|
|
109
|
+
|
|
110
|
+
def serve_file(self, filename: str, content_type: str):
|
|
111
|
+
"""Serve static file."""
|
|
112
|
+
try:
|
|
113
|
+
static_dir = Path(__file__).parent / "static"
|
|
114
|
+
file_path = static_dir / filename
|
|
115
|
+
|
|
116
|
+
if not file_path.exists():
|
|
117
|
+
self.send_error(404, f"File not found: {filename}")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
content = file_path.read_bytes()
|
|
121
|
+
|
|
122
|
+
self.send_response(200)
|
|
123
|
+
self.send_header("Content-Type", content_type)
|
|
124
|
+
self.send_header("Content-Length", str(len(content)))
|
|
125
|
+
self.end_headers()
|
|
126
|
+
self.wfile.write(content)
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Failed to serve file {filename}: {e}")
|
|
130
|
+
self.send_error(500, str(e))
|
|
131
|
+
|
|
132
|
+
def send_json(self, data: dict | list, status: int = 200):
|
|
133
|
+
"""Send JSON response."""
|
|
134
|
+
try:
|
|
135
|
+
content = json.dumps(data).encode("utf-8")
|
|
136
|
+
|
|
137
|
+
self.send_response(status)
|
|
138
|
+
self.send_header("Content-Type", "application/json")
|
|
139
|
+
self.send_header("Content-Length", str(len(content)))
|
|
140
|
+
self.send_header("Access-Control-Allow-Origin", "*") # CORS
|
|
141
|
+
self.end_headers()
|
|
142
|
+
self.wfile.write(content)
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Failed to send JSON: {e}")
|
|
146
|
+
self.send_error(500, str(e))
|
|
147
|
+
|
|
148
|
+
# ========================================================================
|
|
149
|
+
# API Endpoints - Read Directly from Redis
|
|
150
|
+
# ========================================================================
|
|
151
|
+
|
|
152
|
+
def api_health(self):
|
|
153
|
+
"""System health endpoint."""
|
|
154
|
+
try:
|
|
155
|
+
r = self.get_redis()
|
|
156
|
+
has_redis = r is not None
|
|
157
|
+
|
|
158
|
+
if has_redis:
|
|
159
|
+
# Count keys directly
|
|
160
|
+
heartbeat_count = len(r.keys(b"heartbeat:*"))
|
|
161
|
+
approval_count = len(r.keys(b"approval:pending:*"))
|
|
162
|
+
else:
|
|
163
|
+
heartbeat_count = 0
|
|
164
|
+
approval_count = 0
|
|
165
|
+
|
|
166
|
+
self.send_json(
|
|
167
|
+
{
|
|
168
|
+
"status": "healthy" if has_redis else "degraded",
|
|
169
|
+
"redis_available": has_redis,
|
|
170
|
+
"active_agents": heartbeat_count,
|
|
171
|
+
"pending_approvals": approval_count,
|
|
172
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
self.send_json({"status": "error", "error": str(e)}, status=500)
|
|
177
|
+
|
|
178
|
+
def api_agents(self):
|
|
179
|
+
"""List active agents."""
|
|
180
|
+
try:
|
|
181
|
+
r = self.get_redis()
|
|
182
|
+
if not r:
|
|
183
|
+
self.send_json([])
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
result = []
|
|
187
|
+
for key in r.keys(b"heartbeat:*"):
|
|
188
|
+
try:
|
|
189
|
+
data = r.get(key)
|
|
190
|
+
if data:
|
|
191
|
+
heartbeat = json.loads(data.decode("utf-8"))
|
|
192
|
+
result.append(
|
|
193
|
+
{
|
|
194
|
+
"agent_id": heartbeat.get("agent_id"),
|
|
195
|
+
"status": heartbeat.get("status"),
|
|
196
|
+
"last_seen": heartbeat.get("timestamp"),
|
|
197
|
+
"progress": heartbeat.get("progress", 0.0),
|
|
198
|
+
"current_task": heartbeat.get("current_task", "Unknown"),
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"Failed to parse heartbeat {key}: {e}")
|
|
203
|
+
|
|
204
|
+
self.send_json(result)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"Failed to get agents: {e}")
|
|
207
|
+
self.send_json([], status=500)
|
|
208
|
+
|
|
209
|
+
def api_agent_detail(self, agent_id: str):
|
|
210
|
+
"""Get specific agent details."""
|
|
211
|
+
try:
|
|
212
|
+
r = self.get_redis()
|
|
213
|
+
if not r:
|
|
214
|
+
self.send_json({"error": "Redis not available"}, status=503)
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
key = f"heartbeat:{agent_id}".encode()
|
|
218
|
+
data = r.get(key)
|
|
219
|
+
|
|
220
|
+
if not data:
|
|
221
|
+
self.send_json({"error": f"Agent {agent_id} not found"}, status=404)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
heartbeat = json.loads(data.decode("utf-8"))
|
|
225
|
+
self.send_json(
|
|
226
|
+
{
|
|
227
|
+
"agent_id": heartbeat.get("agent_id"),
|
|
228
|
+
"status": heartbeat.get("status"),
|
|
229
|
+
"last_seen": heartbeat.get("timestamp"),
|
|
230
|
+
"progress": heartbeat.get("progress", 0.0),
|
|
231
|
+
"current_task": heartbeat.get("current_task"),
|
|
232
|
+
"metadata": heartbeat.get("metadata", {}),
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
self.send_json({"error": str(e)}, status=500)
|
|
237
|
+
|
|
238
|
+
def api_signals(self, limit: int):
|
|
239
|
+
"""Get recent coordination signals."""
|
|
240
|
+
try:
|
|
241
|
+
r = self.get_redis()
|
|
242
|
+
if not r:
|
|
243
|
+
self.send_json([])
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
result = []
|
|
247
|
+
for key in r.keys(b"empathy:signal:*")[:limit]:
|
|
248
|
+
try:
|
|
249
|
+
data = r.get(key)
|
|
250
|
+
if data:
|
|
251
|
+
signal = json.loads(data.decode("utf-8"))
|
|
252
|
+
result.append(
|
|
253
|
+
{
|
|
254
|
+
"signal_type": signal.get("signal_type"),
|
|
255
|
+
"source_agent": signal.get("source_agent"),
|
|
256
|
+
"target_agent": signal.get("target_agent"),
|
|
257
|
+
"timestamp": signal.get("timestamp"),
|
|
258
|
+
"payload": signal.get("payload", {}),
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Failed to parse signal {key}: {e}")
|
|
263
|
+
|
|
264
|
+
# Sort by timestamp (newest first)
|
|
265
|
+
result.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
|
266
|
+
self.send_json(result[:limit])
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.error(f"Failed to get signals: {e}")
|
|
269
|
+
self.send_json([])
|
|
270
|
+
|
|
271
|
+
def api_events(self, event_type: str | None, limit: int):
|
|
272
|
+
"""Get recent events."""
|
|
273
|
+
try:
|
|
274
|
+
r = self.get_redis()
|
|
275
|
+
if not r:
|
|
276
|
+
self.send_json([])
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
result = []
|
|
280
|
+
|
|
281
|
+
# Get from streams
|
|
282
|
+
stream_patterns = [
|
|
283
|
+
b"stream:workflow_progress",
|
|
284
|
+
b"stream:agent_heartbeat",
|
|
285
|
+
b"stream:coordination_signal",
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
for stream_key in stream_patterns:
|
|
289
|
+
try:
|
|
290
|
+
# Get last N entries from stream
|
|
291
|
+
entries = r.xrevrange(stream_key, count=limit)
|
|
292
|
+
for entry_id, fields in entries:
|
|
293
|
+
# Parse event structure: top-level fields + data payload
|
|
294
|
+
event_type = fields.get(b"event_type", b"unknown").decode("utf-8")
|
|
295
|
+
timestamp = fields.get(b"timestamp", b"").decode("utf-8")
|
|
296
|
+
source = fields.get(b"source", b"attune").decode("utf-8")
|
|
297
|
+
|
|
298
|
+
# Parse the data payload (JSON-encoded)
|
|
299
|
+
data = {}
|
|
300
|
+
if b"data" in fields:
|
|
301
|
+
try:
|
|
302
|
+
data = json.loads(fields[b"data"].decode("utf-8"))
|
|
303
|
+
except json.JSONDecodeError:
|
|
304
|
+
data = {}
|
|
305
|
+
|
|
306
|
+
result.append(
|
|
307
|
+
{
|
|
308
|
+
"event_id": entry_id.decode("utf-8") if isinstance(entry_id, bytes) else entry_id,
|
|
309
|
+
"event_type": event_type,
|
|
310
|
+
"timestamp": timestamp,
|
|
311
|
+
"data": data,
|
|
312
|
+
"source": source,
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.debug(f"Stream {stream_key} not found or empty: {e}")
|
|
317
|
+
|
|
318
|
+
# Sort by timestamp
|
|
319
|
+
result.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
|
320
|
+
self.send_json(result[:limit])
|
|
321
|
+
except Exception as e:
|
|
322
|
+
logger.error(f"Failed to get events: {e}")
|
|
323
|
+
self.send_json([])
|
|
324
|
+
|
|
325
|
+
def api_approvals(self):
|
|
326
|
+
"""Get pending approvals."""
|
|
327
|
+
try:
|
|
328
|
+
r = self.get_redis()
|
|
329
|
+
if not r:
|
|
330
|
+
self.send_json([])
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
result = []
|
|
334
|
+
for key in r.keys(b"approval_request:*"):
|
|
335
|
+
try:
|
|
336
|
+
data = r.get(key)
|
|
337
|
+
if data:
|
|
338
|
+
approval = json.loads(data.decode("utf-8"))
|
|
339
|
+
result.append(
|
|
340
|
+
{
|
|
341
|
+
"request_id": approval.get("request_id"),
|
|
342
|
+
"approval_type": approval.get("approval_type"),
|
|
343
|
+
"agent_id": approval.get("agent_id"),
|
|
344
|
+
"context": approval.get("context", {}),
|
|
345
|
+
"timestamp": approval.get("timestamp"),
|
|
346
|
+
"timeout_seconds": approval.get("timeout_seconds", 300),
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Failed to parse approval {key}: {e}")
|
|
351
|
+
|
|
352
|
+
self.send_json(result)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f"Failed to get approvals: {e}")
|
|
355
|
+
self.send_json([])
|
|
356
|
+
|
|
357
|
+
def api_approve(self, request_id: str, reason: str):
|
|
358
|
+
"""Approve request."""
|
|
359
|
+
try:
|
|
360
|
+
r = self.get_redis()
|
|
361
|
+
if not r:
|
|
362
|
+
self.send_json({"error": "Redis not available"}, status=503)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Delete from pending
|
|
366
|
+
key = f"approval:pending:{request_id}".encode()
|
|
367
|
+
if r.delete(key):
|
|
368
|
+
self.send_json({"status": "approved", "request_id": request_id})
|
|
369
|
+
else:
|
|
370
|
+
self.send_json({"error": "Request not found"}, status=404)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
self.send_json({"error": str(e)}, status=500)
|
|
373
|
+
|
|
374
|
+
def api_reject(self, request_id: str, reason: str):
|
|
375
|
+
"""Reject request."""
|
|
376
|
+
try:
|
|
377
|
+
r = self.get_redis()
|
|
378
|
+
if not r:
|
|
379
|
+
self.send_json({"error": "Redis not available"}, status=503)
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# Delete from pending
|
|
383
|
+
key = f"approval:pending:{request_id}".encode()
|
|
384
|
+
if r.delete(key):
|
|
385
|
+
self.send_json({"status": "rejected", "request_id": request_id})
|
|
386
|
+
else:
|
|
387
|
+
self.send_json({"error": "Request not found"}, status=404)
|
|
388
|
+
except Exception as e:
|
|
389
|
+
self.send_json({"error": str(e)}, status=500)
|
|
390
|
+
|
|
391
|
+
def api_feedback_workflows(self):
|
|
392
|
+
"""Get workflow quality metrics."""
|
|
393
|
+
try:
|
|
394
|
+
r = self.get_redis()
|
|
395
|
+
if not r:
|
|
396
|
+
self.send_json([])
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# Group feedback by workflow/stage/tier
|
|
400
|
+
feedback_groups = {}
|
|
401
|
+
|
|
402
|
+
for key in r.keys(b"feedback:*"):
|
|
403
|
+
try:
|
|
404
|
+
data = r.get(key)
|
|
405
|
+
if data:
|
|
406
|
+
feedback = json.loads(data.decode("utf-8"))
|
|
407
|
+
workflow = feedback.get("workflow_name")
|
|
408
|
+
stage = feedback.get("stage_name")
|
|
409
|
+
tier = feedback.get("tier")
|
|
410
|
+
quality = feedback.get("quality_score")
|
|
411
|
+
|
|
412
|
+
group_key = f"{workflow}/{stage}/{tier}"
|
|
413
|
+
if group_key not in feedback_groups:
|
|
414
|
+
feedback_groups[group_key] = {
|
|
415
|
+
"workflow_name": workflow,
|
|
416
|
+
"stage_name": stage,
|
|
417
|
+
"tier": tier,
|
|
418
|
+
"qualities": [],
|
|
419
|
+
}
|
|
420
|
+
feedback_groups[group_key]["qualities"].append(quality)
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.error(f"Failed to parse feedback {key}: {e}")
|
|
423
|
+
|
|
424
|
+
# Calculate stats
|
|
425
|
+
result = []
|
|
426
|
+
for group_key, group in feedback_groups.items():
|
|
427
|
+
qualities = group["qualities"]
|
|
428
|
+
if qualities:
|
|
429
|
+
avg_quality = sum(qualities) / len(qualities)
|
|
430
|
+
result.append(
|
|
431
|
+
{
|
|
432
|
+
"workflow_name": group["workflow_name"],
|
|
433
|
+
"stage_name": group["stage_name"],
|
|
434
|
+
"tier": group["tier"],
|
|
435
|
+
"avg_quality": avg_quality,
|
|
436
|
+
"sample_count": len(qualities),
|
|
437
|
+
"trend": 0, # Simplified - no trend calculation
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
self.send_json(result)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.error(f"Failed to get quality metrics: {e}")
|
|
444
|
+
self.send_json([])
|
|
445
|
+
|
|
446
|
+
def api_underperforming(self, threshold: float):
|
|
447
|
+
"""Get underperforming stages."""
|
|
448
|
+
try:
|
|
449
|
+
r = self.get_redis()
|
|
450
|
+
if not r:
|
|
451
|
+
self.send_json([])
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
# Get all feedback and group by workflow/stage
|
|
455
|
+
feedback_groups = {}
|
|
456
|
+
|
|
457
|
+
for key in r.keys(b"feedback:*"):
|
|
458
|
+
try:
|
|
459
|
+
data = r.get(key)
|
|
460
|
+
if data:
|
|
461
|
+
feedback = json.loads(data.decode("utf-8"))
|
|
462
|
+
workflow = feedback.get("workflow_name")
|
|
463
|
+
stage = feedback.get("stage_name")
|
|
464
|
+
quality = feedback.get("quality_score")
|
|
465
|
+
|
|
466
|
+
group_key = f"{workflow}/{stage}"
|
|
467
|
+
if group_key not in feedback_groups:
|
|
468
|
+
feedback_groups[group_key] = {
|
|
469
|
+
"workflow_name": workflow,
|
|
470
|
+
"stage_name": stage,
|
|
471
|
+
"qualities": [],
|
|
472
|
+
}
|
|
473
|
+
feedback_groups[group_key]["qualities"].append(quality)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
logger.error(f"Failed to parse feedback {key}: {e}")
|
|
476
|
+
|
|
477
|
+
# Find underperforming stages
|
|
478
|
+
result = []
|
|
479
|
+
for group_key, group in feedback_groups.items():
|
|
480
|
+
qualities = group["qualities"]
|
|
481
|
+
if qualities:
|
|
482
|
+
avg_quality = sum(qualities) / len(qualities)
|
|
483
|
+
if avg_quality < threshold:
|
|
484
|
+
result.append(
|
|
485
|
+
{
|
|
486
|
+
"workflow_name": group["workflow_name"],
|
|
487
|
+
"stage_name": group["stage_name"],
|
|
488
|
+
"avg_quality": avg_quality,
|
|
489
|
+
"sample_count": len(qualities),
|
|
490
|
+
"min_quality": min(qualities),
|
|
491
|
+
"max_quality": max(qualities),
|
|
492
|
+
"trend": 0,
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Sort by quality (worst first)
|
|
497
|
+
result.sort(key=lambda x: x["avg_quality"])
|
|
498
|
+
self.send_json(result)
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Failed to get underperforming: {e}")
|
|
501
|
+
self.send_json([])
|
|
502
|
+
|
|
503
|
+
def log_message(self, format, *args):
|
|
504
|
+
"""Suppress default logging."""
|
|
505
|
+
# Override to reduce noise - only log errors
|
|
506
|
+
if args[1][0] in ("4", "5"): # 4xx or 5xx errors
|
|
507
|
+
logger.warning(f"{self.address_string()} - {format % args}")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def run_standalone_dashboard(host: str = "127.0.0.1", port: int = 8000):
|
|
511
|
+
"""Run standalone dashboard that reads directly from Redis.
|
|
512
|
+
|
|
513
|
+
This version bypasses the telemetry API layer and works with
|
|
514
|
+
data populated by scripts/populate_redis_direct.py.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
host: Host to bind to (default: 127.0.0.1)
|
|
518
|
+
port: Port to bind to (default: 8000)
|
|
519
|
+
|
|
520
|
+
Example:
|
|
521
|
+
>>> from attune.dashboard.standalone_server import run_standalone_dashboard
|
|
522
|
+
>>> run_standalone_dashboard(host="0.0.0.0", port=8080)
|
|
523
|
+
"""
|
|
524
|
+
if not REDIS_AVAILABLE:
|
|
525
|
+
print("ā ļø Warning: redis-py not installed. Install with: pip install redis")
|
|
526
|
+
print(" Dashboard will start but won't show data.")
|
|
527
|
+
print()
|
|
528
|
+
|
|
529
|
+
server = HTTPServer((host, port), StandaloneDashboardHandler)
|
|
530
|
+
|
|
531
|
+
print(f"š Agent Coordination Dashboard (Standalone) running at http://{host}:{port}")
|
|
532
|
+
print(f"š Open in browser: http://{host}:{port}")
|
|
533
|
+
print()
|
|
534
|
+
print("š” This version reads directly from Redis")
|
|
535
|
+
print(" Populate data with: python scripts/populate_redis_direct.py")
|
|
536
|
+
print()
|
|
537
|
+
print("Press Ctrl+C to stop")
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
server.serve_forever()
|
|
541
|
+
except KeyboardInterrupt:
|
|
542
|
+
print("\n\nš Shutting down dashboard...")
|
|
543
|
+
server.shutdown()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
if __name__ == "__main__":
|
|
547
|
+
run_standalone_dashboard()
|