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,300 @@
|
|
|
1
|
+
"""Health Check Module
|
|
2
|
+
|
|
3
|
+
Provides system health monitoring and status reporting.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HealthStatus(Enum):
|
|
23
|
+
"""Health status levels."""
|
|
24
|
+
|
|
25
|
+
HEALTHY = "healthy"
|
|
26
|
+
DEGRADED = "degraded"
|
|
27
|
+
UNHEALTHY = "unhealthy"
|
|
28
|
+
UNKNOWN = "unknown"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class HealthCheckResult:
|
|
33
|
+
"""Result of a single health check."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
status: HealthStatus
|
|
37
|
+
message: str = ""
|
|
38
|
+
latency_ms: float = 0.0
|
|
39
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SystemHealth:
|
|
45
|
+
"""Overall system health status."""
|
|
46
|
+
|
|
47
|
+
status: HealthStatus
|
|
48
|
+
checks: list[HealthCheckResult]
|
|
49
|
+
version: str = "unknown"
|
|
50
|
+
uptime_seconds: float = 0.0
|
|
51
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
"""Convert to dictionary for JSON serialization."""
|
|
55
|
+
return {
|
|
56
|
+
"status": self.status.value,
|
|
57
|
+
"version": self.version,
|
|
58
|
+
"uptime_seconds": self.uptime_seconds,
|
|
59
|
+
"timestamp": self.timestamp.isoformat(),
|
|
60
|
+
"checks": [
|
|
61
|
+
{
|
|
62
|
+
"name": c.name,
|
|
63
|
+
"status": c.status.value,
|
|
64
|
+
"message": c.message,
|
|
65
|
+
"latency_ms": c.latency_ms,
|
|
66
|
+
"details": c.details,
|
|
67
|
+
}
|
|
68
|
+
for c in self.checks
|
|
69
|
+
],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class HealthCheck:
|
|
74
|
+
"""Health check manager for monitoring system components.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
health = HealthCheck(version="3.1.0")
|
|
78
|
+
|
|
79
|
+
@health.register("database")
|
|
80
|
+
async def check_database():
|
|
81
|
+
await db.ping()
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
@health.register("memory_graph")
|
|
85
|
+
async def check_memory_graph():
|
|
86
|
+
graph = MemoryGraph()
|
|
87
|
+
return len(graph.nodes) >= 0
|
|
88
|
+
|
|
89
|
+
status = await health.run_all()
|
|
90
|
+
print(status.to_dict())
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, version: str = "unknown"):
|
|
95
|
+
self.version = version
|
|
96
|
+
self.start_time = time.time()
|
|
97
|
+
self._checks: dict[str, Callable] = {}
|
|
98
|
+
self._timeouts: dict[str, float] = {}
|
|
99
|
+
|
|
100
|
+
def register(
|
|
101
|
+
self,
|
|
102
|
+
name: str,
|
|
103
|
+
timeout: float = 10.0,
|
|
104
|
+
critical: bool = False,
|
|
105
|
+
) -> Callable:
|
|
106
|
+
"""Decorator to register a health check.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
name: Name of the health check
|
|
110
|
+
timeout: Maximum time for check in seconds
|
|
111
|
+
critical: If True, failure makes system unhealthy
|
|
112
|
+
|
|
113
|
+
The decorated function should:
|
|
114
|
+
- Return True for healthy
|
|
115
|
+
- Return False for unhealthy
|
|
116
|
+
- Raise exception for error
|
|
117
|
+
- Return dict with 'healthy' key for details
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def decorator(func: Callable) -> Callable:
|
|
122
|
+
self._checks[name] = func
|
|
123
|
+
self._timeouts[name] = timeout
|
|
124
|
+
return func
|
|
125
|
+
|
|
126
|
+
return decorator
|
|
127
|
+
|
|
128
|
+
async def run_check(self, name: str) -> HealthCheckResult:
|
|
129
|
+
"""Run a single health check."""
|
|
130
|
+
if name not in self._checks:
|
|
131
|
+
return HealthCheckResult(
|
|
132
|
+
name=name,
|
|
133
|
+
status=HealthStatus.UNKNOWN,
|
|
134
|
+
message=f"Check '{name}' not found",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
check_func = self._checks[name]
|
|
138
|
+
timeout = self._timeouts.get(name, 10.0)
|
|
139
|
+
start = time.time()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
if asyncio.iscoroutinefunction(check_func):
|
|
143
|
+
result = await asyncio.wait_for(check_func(), timeout=timeout)
|
|
144
|
+
else:
|
|
145
|
+
result = check_func()
|
|
146
|
+
|
|
147
|
+
latency = (time.time() - start) * 1000
|
|
148
|
+
|
|
149
|
+
# Parse result
|
|
150
|
+
if isinstance(result, bool):
|
|
151
|
+
status = HealthStatus.HEALTHY if result else HealthStatus.UNHEALTHY
|
|
152
|
+
return HealthCheckResult(
|
|
153
|
+
name=name,
|
|
154
|
+
status=status,
|
|
155
|
+
latency_ms=latency,
|
|
156
|
+
)
|
|
157
|
+
if isinstance(result, dict):
|
|
158
|
+
healthy = result.get("healthy", True)
|
|
159
|
+
status = HealthStatus.HEALTHY if healthy else HealthStatus.UNHEALTHY
|
|
160
|
+
return HealthCheckResult(
|
|
161
|
+
name=name,
|
|
162
|
+
status=status,
|
|
163
|
+
message=result.get("message", ""),
|
|
164
|
+
latency_ms=latency,
|
|
165
|
+
details=result,
|
|
166
|
+
)
|
|
167
|
+
return HealthCheckResult(
|
|
168
|
+
name=name,
|
|
169
|
+
status=HealthStatus.HEALTHY,
|
|
170
|
+
latency_ms=latency,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
except asyncio.TimeoutError:
|
|
174
|
+
return HealthCheckResult(
|
|
175
|
+
name=name,
|
|
176
|
+
status=HealthStatus.UNHEALTHY,
|
|
177
|
+
message=f"Check timed out after {timeout}s",
|
|
178
|
+
latency_ms=timeout * 1000,
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
latency = (time.time() - start) * 1000
|
|
182
|
+
return HealthCheckResult(
|
|
183
|
+
name=name,
|
|
184
|
+
status=HealthStatus.UNHEALTHY,
|
|
185
|
+
message=str(e),
|
|
186
|
+
latency_ms=latency,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
async def run_all(self) -> SystemHealth:
|
|
190
|
+
"""Run all registered health checks."""
|
|
191
|
+
results = await asyncio.gather(*[self.run_check(name) for name in self._checks.keys()])
|
|
192
|
+
|
|
193
|
+
# Determine overall status
|
|
194
|
+
statuses = [r.status for r in results]
|
|
195
|
+
|
|
196
|
+
if all(s == HealthStatus.HEALTHY for s in statuses):
|
|
197
|
+
overall = HealthStatus.HEALTHY
|
|
198
|
+
elif any(s == HealthStatus.UNHEALTHY for s in statuses):
|
|
199
|
+
overall = HealthStatus.UNHEALTHY
|
|
200
|
+
elif any(s == HealthStatus.DEGRADED for s in statuses):
|
|
201
|
+
overall = HealthStatus.DEGRADED
|
|
202
|
+
else:
|
|
203
|
+
overall = HealthStatus.UNKNOWN
|
|
204
|
+
|
|
205
|
+
return SystemHealth(
|
|
206
|
+
status=overall,
|
|
207
|
+
checks=list(results),
|
|
208
|
+
version=self.version,
|
|
209
|
+
uptime_seconds=time.time() - self.start_time,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def run_all_sync(self) -> SystemHealth:
|
|
213
|
+
"""Synchronous version of run_all."""
|
|
214
|
+
return asyncio.run(self.run_all())
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Global health check instance
|
|
218
|
+
_health_check: HealthCheck | None = None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_health_check() -> HealthCheck:
|
|
222
|
+
"""Get or create global health check instance."""
|
|
223
|
+
global _health_check
|
|
224
|
+
if _health_check is None:
|
|
225
|
+
_health_check = HealthCheck()
|
|
226
|
+
return _health_check
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def register_default_checks(health: HealthCheck) -> None:
|
|
230
|
+
"""Register default health checks for Empathy Framework."""
|
|
231
|
+
|
|
232
|
+
@health.register("workflow_registry")
|
|
233
|
+
async def check_workflow_registry() -> dict[str, Any]:
|
|
234
|
+
"""Check workflow registry is loaded."""
|
|
235
|
+
try:
|
|
236
|
+
from attune.routing import WorkflowRegistry
|
|
237
|
+
|
|
238
|
+
registry = WorkflowRegistry()
|
|
239
|
+
workflow_count = len(registry.list_all())
|
|
240
|
+
return {
|
|
241
|
+
"healthy": workflow_count > 0,
|
|
242
|
+
"workflow_count": workflow_count,
|
|
243
|
+
"message": f"{workflow_count} workflows registered",
|
|
244
|
+
}
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return {"healthy": False, "message": str(e)}
|
|
247
|
+
|
|
248
|
+
@health.register("memory_graph")
|
|
249
|
+
async def check_memory_graph() -> dict[str, Any]:
|
|
250
|
+
"""Check memory graph is accessible."""
|
|
251
|
+
try:
|
|
252
|
+
from attune.memory import MemoryGraph
|
|
253
|
+
|
|
254
|
+
graph_path = Path("patterns/memory_graph.json")
|
|
255
|
+
if graph_path.exists():
|
|
256
|
+
graph = MemoryGraph(path=graph_path)
|
|
257
|
+
return {
|
|
258
|
+
"healthy": True,
|
|
259
|
+
"node_count": len(graph.nodes),
|
|
260
|
+
"edge_count": len(graph.edges),
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
"healthy": True,
|
|
264
|
+
"node_count": 0,
|
|
265
|
+
"edge_count": 0,
|
|
266
|
+
"message": "Graph file not yet created",
|
|
267
|
+
}
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return {"healthy": False, "message": str(e)}
|
|
270
|
+
|
|
271
|
+
@health.register("smart_router")
|
|
272
|
+
async def check_smart_router() -> dict[str, Any]:
|
|
273
|
+
"""Check smart router is functional."""
|
|
274
|
+
try:
|
|
275
|
+
from attune.routing import SmartRouter
|
|
276
|
+
|
|
277
|
+
router = SmartRouter()
|
|
278
|
+
# Test with a simple request
|
|
279
|
+
decision = router.route_sync("test request")
|
|
280
|
+
return {
|
|
281
|
+
"healthy": decision is not None,
|
|
282
|
+
"primary_workflow": decision.primary_workflow,
|
|
283
|
+
}
|
|
284
|
+
except Exception as e:
|
|
285
|
+
return {"healthy": False, "message": str(e)}
|
|
286
|
+
|
|
287
|
+
@health.register("chain_executor")
|
|
288
|
+
async def check_chain_executor() -> dict[str, Any]:
|
|
289
|
+
"""Check chain executor is loaded."""
|
|
290
|
+
try:
|
|
291
|
+
from attune.routing import ChainExecutor
|
|
292
|
+
|
|
293
|
+
executor = ChainExecutor()
|
|
294
|
+
templates = executor.list_templates()
|
|
295
|
+
return {
|
|
296
|
+
"healthy": True,
|
|
297
|
+
"template_count": len(templates),
|
|
298
|
+
}
|
|
299
|
+
except Exception as e:
|
|
300
|
+
return {"healthy": False, "message": str(e)}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Retry Pattern Implementation
|
|
2
|
+
|
|
3
|
+
Provides exponential backoff retry logic for transient failures.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import random
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from functools import wraps
|
|
15
|
+
from typing import Any, TypeVar
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RetryConfig:
|
|
24
|
+
"""Configuration for retry behavior."""
|
|
25
|
+
|
|
26
|
+
max_attempts: int = 3
|
|
27
|
+
backoff_factor: float = 2.0
|
|
28
|
+
initial_delay: float = 1.0
|
|
29
|
+
max_delay: float = 60.0
|
|
30
|
+
jitter: bool = True
|
|
31
|
+
retryable_exceptions: tuple[type[Exception], ...] = field(default_factory=lambda: (Exception,))
|
|
32
|
+
|
|
33
|
+
def get_delay(self, attempt: int) -> float:
|
|
34
|
+
"""Calculate delay for given attempt with exponential backoff."""
|
|
35
|
+
delay = self.initial_delay * (self.backoff_factor ** (attempt - 1))
|
|
36
|
+
delay = min(delay, self.max_delay)
|
|
37
|
+
|
|
38
|
+
if self.jitter:
|
|
39
|
+
# Add up to 25% jitter
|
|
40
|
+
delay = delay * (0.75 + random.random() * 0.5)
|
|
41
|
+
|
|
42
|
+
return delay
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def retry(
|
|
46
|
+
max_attempts: int = 3,
|
|
47
|
+
backoff_factor: float = 2.0,
|
|
48
|
+
initial_delay: float = 1.0,
|
|
49
|
+
max_delay: float = 60.0,
|
|
50
|
+
jitter: bool = True,
|
|
51
|
+
retryable_exceptions: tuple[type[Exception], ...] | None = None,
|
|
52
|
+
on_retry: Callable[[Exception, int], None] | None = None,
|
|
53
|
+
) -> Callable:
|
|
54
|
+
"""Decorator for retrying functions with exponential backoff.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
max_attempts: Maximum number of retry attempts
|
|
58
|
+
backoff_factor: Multiplier for delay between attempts
|
|
59
|
+
initial_delay: Initial delay in seconds
|
|
60
|
+
max_delay: Maximum delay in seconds
|
|
61
|
+
jitter: Add randomness to prevent thundering herd
|
|
62
|
+
retryable_exceptions: Tuple of exceptions to retry on
|
|
63
|
+
on_retry: Callback called on each retry with (exception, attempt)
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
@retry(max_attempts=3, backoff_factor=2.0)
|
|
67
|
+
async def call_api():
|
|
68
|
+
response = await httpx.get("https://api.example.com")
|
|
69
|
+
return response.json()
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
if retryable_exceptions is None:
|
|
73
|
+
retryable_exceptions = (Exception,)
|
|
74
|
+
|
|
75
|
+
config = RetryConfig(
|
|
76
|
+
max_attempts=max_attempts,
|
|
77
|
+
backoff_factor=backoff_factor,
|
|
78
|
+
initial_delay=initial_delay,
|
|
79
|
+
max_delay=max_delay,
|
|
80
|
+
jitter=jitter,
|
|
81
|
+
retryable_exceptions=retryable_exceptions,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
85
|
+
@wraps(func)
|
|
86
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> T:
|
|
87
|
+
last_exception: Exception | None = None
|
|
88
|
+
|
|
89
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
90
|
+
try:
|
|
91
|
+
result: T = await func(*args, **kwargs) # type: ignore[misc]
|
|
92
|
+
return result
|
|
93
|
+
except config.retryable_exceptions as e:
|
|
94
|
+
last_exception = e
|
|
95
|
+
|
|
96
|
+
if attempt == config.max_attempts:
|
|
97
|
+
logger.error(
|
|
98
|
+
f"All {config.max_attempts} retries failed for {func.__name__}: {e}",
|
|
99
|
+
)
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
delay = config.get_delay(attempt)
|
|
103
|
+
logger.warning(
|
|
104
|
+
f"Attempt {attempt}/{config.max_attempts} failed for {func.__name__}: {e}. "
|
|
105
|
+
f"Retrying in {delay:.2f}s",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if on_retry:
|
|
109
|
+
on_retry(e, attempt)
|
|
110
|
+
|
|
111
|
+
await asyncio.sleep(delay)
|
|
112
|
+
|
|
113
|
+
# Should never reach here, but satisfy type checker
|
|
114
|
+
if last_exception:
|
|
115
|
+
raise last_exception
|
|
116
|
+
raise RuntimeError("Unexpected retry loop exit")
|
|
117
|
+
|
|
118
|
+
@wraps(func)
|
|
119
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> T:
|
|
120
|
+
import time
|
|
121
|
+
|
|
122
|
+
last_exception: Exception | None = None
|
|
123
|
+
|
|
124
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
125
|
+
try:
|
|
126
|
+
return func(*args, **kwargs)
|
|
127
|
+
except config.retryable_exceptions as e:
|
|
128
|
+
last_exception = e
|
|
129
|
+
|
|
130
|
+
if attempt == config.max_attempts:
|
|
131
|
+
logger.error(
|
|
132
|
+
f"All {config.max_attempts} retries failed for {func.__name__}: {e}",
|
|
133
|
+
)
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
delay = config.get_delay(attempt)
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Attempt {attempt}/{config.max_attempts} failed for {func.__name__}: {e}. "
|
|
139
|
+
f"Retrying in {delay:.2f}s",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if on_retry:
|
|
143
|
+
on_retry(e, attempt)
|
|
144
|
+
|
|
145
|
+
time.sleep(delay)
|
|
146
|
+
|
|
147
|
+
if last_exception:
|
|
148
|
+
raise last_exception
|
|
149
|
+
raise RuntimeError("Unexpected retry loop exit")
|
|
150
|
+
|
|
151
|
+
# Return appropriate wrapper based on function type
|
|
152
|
+
if asyncio.iscoroutinefunction(func):
|
|
153
|
+
return async_wrapper # type: ignore[return-value]
|
|
154
|
+
return sync_wrapper
|
|
155
|
+
|
|
156
|
+
return decorator
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def retry_with_backoff(
|
|
160
|
+
func: Callable[..., T],
|
|
161
|
+
*args: Any,
|
|
162
|
+
config: RetryConfig | None = None,
|
|
163
|
+
**kwargs: Any,
|
|
164
|
+
) -> T:
|
|
165
|
+
"""Execute a function with retry logic.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
func: Function to execute
|
|
169
|
+
*args: Positional arguments to pass to func
|
|
170
|
+
config: Retry configuration (uses defaults if None)
|
|
171
|
+
**kwargs: Keyword arguments to pass to func
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Result of the function
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
result = await retry_with_backoff(
|
|
178
|
+
call_api,
|
|
179
|
+
"https://api.example.com",
|
|
180
|
+
config=RetryConfig(max_attempts=5)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
if config is None:
|
|
185
|
+
config = RetryConfig()
|
|
186
|
+
|
|
187
|
+
last_exception: Exception | None = None
|
|
188
|
+
|
|
189
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
190
|
+
try:
|
|
191
|
+
if asyncio.iscoroutinefunction(func):
|
|
192
|
+
result: T = await func(*args, **kwargs)
|
|
193
|
+
return result
|
|
194
|
+
return func(*args, **kwargs)
|
|
195
|
+
except config.retryable_exceptions as e:
|
|
196
|
+
last_exception = e
|
|
197
|
+
|
|
198
|
+
if attempt == config.max_attempts:
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
delay = config.get_delay(attempt)
|
|
202
|
+
logger.warning(
|
|
203
|
+
f"Attempt {attempt}/{config.max_attempts} failed: {e}. Retrying in {delay:.2f}s",
|
|
204
|
+
)
|
|
205
|
+
await asyncio.sleep(delay)
|
|
206
|
+
|
|
207
|
+
if last_exception:
|
|
208
|
+
raise last_exception
|
|
209
|
+
raise RuntimeError("Unexpected retry loop exit")
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Timeout Pattern Implementation
|
|
2
|
+
|
|
3
|
+
Prevents operations from hanging indefinitely.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import signal
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from typing import Any, TypeVar
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TimeoutError(Exception):
|
|
22
|
+
"""Raised when an operation times out."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, operation: str, timeout: float):
|
|
25
|
+
self.operation = operation
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
super().__init__(f"Operation '{operation}' timed out after {timeout}s")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def timeout(
|
|
31
|
+
seconds: float,
|
|
32
|
+
error_message: str | None = None,
|
|
33
|
+
fallback: Callable[..., T] | None = None,
|
|
34
|
+
) -> Callable:
|
|
35
|
+
"""Decorator to add timeout to a function.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
seconds: Maximum execution time in seconds
|
|
39
|
+
error_message: Custom error message
|
|
40
|
+
fallback: Function to call on timeout
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
@timeout(30)
|
|
44
|
+
async def slow_operation():
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@timeout(10, fallback=lambda: "default")
|
|
48
|
+
async def get_data():
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
54
|
+
@wraps(func)
|
|
55
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> T:
|
|
56
|
+
try:
|
|
57
|
+
coro = func(*args, **kwargs)
|
|
58
|
+
result: T = await asyncio.wait_for(coro, timeout=seconds) # type: ignore[arg-type]
|
|
59
|
+
return result
|
|
60
|
+
except asyncio.TimeoutError:
|
|
61
|
+
operation = error_message or func.__name__
|
|
62
|
+
logger.warning(f"Timeout after {seconds}s: {operation}")
|
|
63
|
+
|
|
64
|
+
if fallback:
|
|
65
|
+
logger.info(f"Using fallback for {func.__name__}")
|
|
66
|
+
if asyncio.iscoroutinefunction(fallback):
|
|
67
|
+
result = await fallback(*args, **kwargs)
|
|
68
|
+
return result
|
|
69
|
+
return fallback(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
raise TimeoutError(operation, seconds)
|
|
72
|
+
|
|
73
|
+
@wraps(func)
|
|
74
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> T:
|
|
75
|
+
# For sync functions, use signal-based timeout (Unix only)
|
|
76
|
+
import platform
|
|
77
|
+
|
|
78
|
+
if platform.system() == "Windows":
|
|
79
|
+
# Windows doesn't support SIGALRM, just run without timeout
|
|
80
|
+
logger.warning(
|
|
81
|
+
f"Timeout not supported on Windows for sync function {func.__name__}",
|
|
82
|
+
)
|
|
83
|
+
return func(*args, **kwargs)
|
|
84
|
+
|
|
85
|
+
def timeout_handler(signum: int, frame: Any) -> None:
|
|
86
|
+
raise TimeoutError(func.__name__, seconds)
|
|
87
|
+
|
|
88
|
+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
|
89
|
+
signal.setitimer(signal.ITIMER_REAL, seconds)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
result = func(*args, **kwargs)
|
|
93
|
+
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
94
|
+
return result
|
|
95
|
+
except TimeoutError:
|
|
96
|
+
if fallback:
|
|
97
|
+
return fallback(*args, **kwargs)
|
|
98
|
+
raise
|
|
99
|
+
finally:
|
|
100
|
+
signal.signal(signal.SIGALRM, old_handler)
|
|
101
|
+
signal.setitimer(signal.ITIMER_REAL, 0)
|
|
102
|
+
|
|
103
|
+
if asyncio.iscoroutinefunction(func):
|
|
104
|
+
return async_wrapper # type: ignore[return-value]
|
|
105
|
+
return sync_wrapper
|
|
106
|
+
|
|
107
|
+
return decorator
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def with_timeout(
|
|
111
|
+
coro: Any,
|
|
112
|
+
seconds: float,
|
|
113
|
+
fallback_value: T | None = None,
|
|
114
|
+
) -> T:
|
|
115
|
+
"""Execute a coroutine with a timeout.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
coro: Coroutine to execute
|
|
119
|
+
seconds: Maximum execution time
|
|
120
|
+
fallback_value: Value to return on timeout (raises if None)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Result of coroutine or fallback value
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
result = await with_timeout(slow_api_call(), 30)
|
|
127
|
+
result = await with_timeout(get_data(), 10, fallback_value={})
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
return await asyncio.wait_for(coro, timeout=seconds)
|
|
132
|
+
except asyncio.TimeoutError:
|
|
133
|
+
if fallback_value is not None:
|
|
134
|
+
return fallback_value
|
|
135
|
+
raise TimeoutError("coroutine", seconds)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Empathy Framework Routing Module
|
|
2
|
+
|
|
3
|
+
Intelligent request routing to workflows using LLM classification.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from attune.routing import SmartRouter, quick_route
|
|
7
|
+
|
|
8
|
+
# Full router
|
|
9
|
+
router = SmartRouter()
|
|
10
|
+
decision = await router.route("Fix security issue in auth.py")
|
|
11
|
+
print(f"Use: {decision.primary_workflow}")
|
|
12
|
+
|
|
13
|
+
# Quick helper
|
|
14
|
+
decision = await quick_route("Optimize database queries")
|
|
15
|
+
|
|
16
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
17
|
+
Licensed under Fair Source 0.9
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .chain_executor import ChainConfig, ChainExecution, ChainExecutor, ChainStep, ChainTrigger
|
|
21
|
+
from .classifier import ClassificationResult, HaikuClassifier
|
|
22
|
+
from .smart_router import RoutingDecision, SmartRouter, quick_route
|
|
23
|
+
from .workflow_registry import WORKFLOW_REGISTRY, WorkflowInfo, WorkflowRegistry
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"WORKFLOW_REGISTRY",
|
|
27
|
+
"ChainConfig",
|
|
28
|
+
"ChainExecution",
|
|
29
|
+
# Chain Executor
|
|
30
|
+
"ChainExecutor",
|
|
31
|
+
"ChainStep",
|
|
32
|
+
"ChainTrigger",
|
|
33
|
+
"ClassificationResult",
|
|
34
|
+
# Classifier
|
|
35
|
+
"HaikuClassifier",
|
|
36
|
+
"RoutingDecision",
|
|
37
|
+
# Smart Router
|
|
38
|
+
"SmartRouter",
|
|
39
|
+
"WorkflowInfo",
|
|
40
|
+
# Workflow Registry
|
|
41
|
+
"WorkflowRegistry",
|
|
42
|
+
"quick_route",
|
|
43
|
+
]
|