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,545 @@
|
|
|
1
|
+
"""Human Approval Gates for Workflow Control.
|
|
2
|
+
|
|
3
|
+
Pattern 5 from Agent Coordination Architecture - Pause workflow execution
|
|
4
|
+
for human approval on critical decisions.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# In workflow: Request approval
|
|
8
|
+
gate = ApprovalGate(agent_id="code-review-workflow")
|
|
9
|
+
approval = gate.request_approval(
|
|
10
|
+
approval_type="deploy_to_production",
|
|
11
|
+
context={
|
|
12
|
+
"deployment": "v2.0.0",
|
|
13
|
+
"changes": ["feature-x", "bugfix-y"],
|
|
14
|
+
"risk_level": "medium"
|
|
15
|
+
},
|
|
16
|
+
timeout=300.0 # 5 minutes
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if approval.approved:
|
|
20
|
+
deploy_to_production()
|
|
21
|
+
else:
|
|
22
|
+
logger.info(f"Deployment rejected: {approval.reason}")
|
|
23
|
+
|
|
24
|
+
# From UI: Respond to approval request
|
|
25
|
+
gate = ApprovalGate()
|
|
26
|
+
pending = gate.get_pending_approvals()
|
|
27
|
+
for request in pending:
|
|
28
|
+
# Display to user, get decision
|
|
29
|
+
gate.respond_to_approval(
|
|
30
|
+
request_id=request.request_id,
|
|
31
|
+
approved=True,
|
|
32
|
+
responder="user@example.com",
|
|
33
|
+
reason="Looks good to deploy"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
Copyright 2025 Smart-AI-Memory
|
|
37
|
+
Licensed under Fair Source License 0.9
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import logging
|
|
43
|
+
import time
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from datetime import datetime
|
|
46
|
+
from typing import Any
|
|
47
|
+
from uuid import uuid4
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ApprovalRequest:
|
|
54
|
+
"""Approval request with context for human decision.
|
|
55
|
+
|
|
56
|
+
Represents a pending approval request from a workflow.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
request_id: str
|
|
60
|
+
approval_type: str # "deploy", "delete", "refactor", etc.
|
|
61
|
+
agent_id: str # Requesting agent/workflow
|
|
62
|
+
context: dict[str, Any] # Decision context
|
|
63
|
+
timestamp: datetime
|
|
64
|
+
timeout_seconds: float
|
|
65
|
+
status: str = "pending" # "pending", "approved", "rejected", "timeout"
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, Any]:
|
|
68
|
+
"""Convert to dictionary for serialization."""
|
|
69
|
+
return {
|
|
70
|
+
"request_id": self.request_id,
|
|
71
|
+
"approval_type": self.approval_type,
|
|
72
|
+
"agent_id": self.agent_id,
|
|
73
|
+
"context": self.context,
|
|
74
|
+
"timestamp": self.timestamp.isoformat() if isinstance(self.timestamp, datetime) else self.timestamp,
|
|
75
|
+
"timeout_seconds": self.timeout_seconds,
|
|
76
|
+
"status": self.status,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, data: dict[str, Any]) -> ApprovalRequest:
|
|
81
|
+
"""Create from dictionary."""
|
|
82
|
+
timestamp = data.get("timestamp")
|
|
83
|
+
if isinstance(timestamp, str):
|
|
84
|
+
timestamp = datetime.fromisoformat(timestamp)
|
|
85
|
+
elif not isinstance(timestamp, datetime):
|
|
86
|
+
timestamp = datetime.utcnow()
|
|
87
|
+
|
|
88
|
+
return cls(
|
|
89
|
+
request_id=data["request_id"],
|
|
90
|
+
approval_type=data["approval_type"],
|
|
91
|
+
agent_id=data["agent_id"],
|
|
92
|
+
context=data.get("context", {}),
|
|
93
|
+
timestamp=timestamp,
|
|
94
|
+
timeout_seconds=data.get("timeout_seconds", 300.0),
|
|
95
|
+
status=data.get("status", "pending"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ApprovalResponse:
|
|
101
|
+
"""Response to an approval request.
|
|
102
|
+
|
|
103
|
+
Represents a human's decision on an approval request.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
request_id: str
|
|
107
|
+
approved: bool
|
|
108
|
+
responder: str # User who approved/rejected
|
|
109
|
+
reason: str = ""
|
|
110
|
+
timestamp: datetime = field(default_factory=datetime.utcnow)
|
|
111
|
+
|
|
112
|
+
def to_dict(self) -> dict[str, Any]:
|
|
113
|
+
"""Convert to dictionary for serialization."""
|
|
114
|
+
return {
|
|
115
|
+
"request_id": self.request_id,
|
|
116
|
+
"approved": self.approved,
|
|
117
|
+
"responder": self.responder,
|
|
118
|
+
"reason": self.reason,
|
|
119
|
+
"timestamp": self.timestamp.isoformat() if isinstance(self.timestamp, datetime) else self.timestamp,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, data: dict[str, Any]) -> ApprovalResponse:
|
|
124
|
+
"""Create from dictionary."""
|
|
125
|
+
timestamp = data.get("timestamp")
|
|
126
|
+
if isinstance(timestamp, str):
|
|
127
|
+
timestamp = datetime.fromisoformat(timestamp)
|
|
128
|
+
elif not isinstance(timestamp, datetime):
|
|
129
|
+
timestamp = datetime.utcnow()
|
|
130
|
+
|
|
131
|
+
return cls(
|
|
132
|
+
request_id=data["request_id"],
|
|
133
|
+
approved=data.get("approved", False),
|
|
134
|
+
responder=data.get("responder", "unknown"),
|
|
135
|
+
reason=data.get("reason", ""),
|
|
136
|
+
timestamp=timestamp,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ApprovalGate:
|
|
141
|
+
"""Human approval gates for workflow control.
|
|
142
|
+
|
|
143
|
+
Workflows can pause execution and wait for human approval before
|
|
144
|
+
proceeding with critical actions.
|
|
145
|
+
|
|
146
|
+
Uses coordination signals under the hood:
|
|
147
|
+
- "approval_request" signal: Workflow → Human
|
|
148
|
+
- "approval_response" signal: Human → Workflow
|
|
149
|
+
|
|
150
|
+
Attributes:
|
|
151
|
+
DEFAULT_TIMEOUT: Default approval timeout (300s = 5 minutes)
|
|
152
|
+
POLL_INTERVAL: Poll interval when waiting for approval (1s)
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
DEFAULT_TIMEOUT = 300.0 # 5 minutes default timeout
|
|
156
|
+
POLL_INTERVAL = 1.0 # Check for response every 1 second
|
|
157
|
+
|
|
158
|
+
def __init__(self, memory=None, agent_id: str | None = None):
|
|
159
|
+
"""Initialize approval gate.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
memory: Memory instance for storing approval requests/responses
|
|
163
|
+
agent_id: This agent's ID (for workflow requesting approval)
|
|
164
|
+
"""
|
|
165
|
+
self.memory = memory
|
|
166
|
+
self.agent_id = agent_id
|
|
167
|
+
|
|
168
|
+
if self.memory is None:
|
|
169
|
+
try:
|
|
170
|
+
from attune.telemetry import UsageTracker
|
|
171
|
+
|
|
172
|
+
tracker = UsageTracker.get_instance()
|
|
173
|
+
if hasattr(tracker, "_memory"):
|
|
174
|
+
self.memory = tracker._memory
|
|
175
|
+
except (ImportError, AttributeError):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
if self.memory is None:
|
|
179
|
+
logger.warning("No memory backend available for approval gates")
|
|
180
|
+
|
|
181
|
+
def request_approval(
|
|
182
|
+
self,
|
|
183
|
+
approval_type: str,
|
|
184
|
+
context: dict[str, Any] | None = None,
|
|
185
|
+
timeout: float | None = None,
|
|
186
|
+
) -> ApprovalResponse:
|
|
187
|
+
"""Request human approval and wait for response.
|
|
188
|
+
|
|
189
|
+
This is a blocking operation that waits for human approval with timeout.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
approval_type: Type of approval needed (e.g., "deploy", "delete")
|
|
193
|
+
context: Context information for decision making
|
|
194
|
+
timeout: Maximum wait time in seconds (default: DEFAULT_TIMEOUT)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
ApprovalResponse with decision (approved or rejected)
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
ValueError: If approval times out
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> gate = ApprovalGate(agent_id="my-workflow")
|
|
204
|
+
>>> approval = gate.request_approval(
|
|
205
|
+
... approval_type="deploy_to_production",
|
|
206
|
+
... context={"version": "2.0.0", "risk": "medium"},
|
|
207
|
+
... timeout=300.0
|
|
208
|
+
... )
|
|
209
|
+
>>> if approval.approved:
|
|
210
|
+
... deploy()
|
|
211
|
+
"""
|
|
212
|
+
if not self.memory or not self.agent_id:
|
|
213
|
+
logger.warning("Cannot request approval: no memory backend or agent_id")
|
|
214
|
+
# Return auto-rejected response
|
|
215
|
+
return ApprovalResponse(
|
|
216
|
+
request_id="",
|
|
217
|
+
approved=False,
|
|
218
|
+
responder="system",
|
|
219
|
+
reason="Approval gates not available (no memory backend)",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
timeout = timeout if timeout is not None else self.DEFAULT_TIMEOUT
|
|
223
|
+
request_id = f"approval_{uuid4().hex[:8]}"
|
|
224
|
+
|
|
225
|
+
# Create approval request
|
|
226
|
+
request = ApprovalRequest(
|
|
227
|
+
request_id=request_id,
|
|
228
|
+
approval_type=approval_type,
|
|
229
|
+
agent_id=self.agent_id,
|
|
230
|
+
context=context or {},
|
|
231
|
+
timestamp=datetime.utcnow(),
|
|
232
|
+
timeout_seconds=timeout,
|
|
233
|
+
status="pending",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Store approval request (for UI to retrieve)
|
|
237
|
+
request_key = f"approval_request:{request_id}"
|
|
238
|
+
try:
|
|
239
|
+
# Use direct Redis access for custom TTL
|
|
240
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
241
|
+
import json
|
|
242
|
+
|
|
243
|
+
self.memory._client.setex(request_key, int(timeout) + 60, json.dumps(request.to_dict()))
|
|
244
|
+
else:
|
|
245
|
+
logger.warning("Cannot store approval request: no Redis backend available")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Failed to store approval request: {e}")
|
|
248
|
+
return ApprovalResponse(
|
|
249
|
+
request_id=request_id, approved=False, responder="system", reason=f"Storage error: {e}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Send approval_request signal (for notifications)
|
|
253
|
+
try:
|
|
254
|
+
from attune.telemetry import CoordinationSignals
|
|
255
|
+
|
|
256
|
+
signals = CoordinationSignals(memory=self.memory, agent_id=self.agent_id)
|
|
257
|
+
signals.signal(
|
|
258
|
+
signal_type="approval_request",
|
|
259
|
+
source_agent=self.agent_id,
|
|
260
|
+
target_agent="*", # Broadcast to UI/monitoring systems
|
|
261
|
+
payload=request.to_dict(),
|
|
262
|
+
ttl_seconds=int(timeout) + 60,
|
|
263
|
+
)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(f"Failed to send approval_request signal: {e}")
|
|
266
|
+
|
|
267
|
+
# Wait for approval response (blocking with timeout)
|
|
268
|
+
logger.info(
|
|
269
|
+
f"Waiting for approval: {approval_type} (request_id={request_id}, timeout={timeout}s)"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
start_time = time.time()
|
|
273
|
+
while time.time() - start_time < timeout:
|
|
274
|
+
# Check for response
|
|
275
|
+
response = self._check_for_response(request_id)
|
|
276
|
+
if response:
|
|
277
|
+
logger.info(
|
|
278
|
+
f"Approval received: {approval_type} → {'APPROVED' if response.approved else 'REJECTED'}"
|
|
279
|
+
)
|
|
280
|
+
return response
|
|
281
|
+
|
|
282
|
+
# Sleep before next check
|
|
283
|
+
time.sleep(self.POLL_INTERVAL)
|
|
284
|
+
|
|
285
|
+
# Timeout - no response received
|
|
286
|
+
logger.warning(f"Approval timeout: {approval_type} (request_id={request_id})")
|
|
287
|
+
|
|
288
|
+
# Update request status to timeout
|
|
289
|
+
request.status = "timeout"
|
|
290
|
+
try:
|
|
291
|
+
# Use direct Redis access
|
|
292
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
293
|
+
import json
|
|
294
|
+
|
|
295
|
+
self.memory._client.setex(request_key, 60, json.dumps(request.to_dict()))
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
return ApprovalResponse(
|
|
300
|
+
request_id=request_id,
|
|
301
|
+
approved=False,
|
|
302
|
+
responder="system",
|
|
303
|
+
reason=f"Approval timeout after {timeout}s",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _check_for_response(self, request_id: str) -> ApprovalResponse | None:
|
|
307
|
+
"""Check if approval response has been received."""
|
|
308
|
+
if not self.memory:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
response_key = f"approval_response:{request_id}"
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
# Try retrieve method first (UnifiedMemory)
|
|
315
|
+
if hasattr(self.memory, "retrieve"):
|
|
316
|
+
data = self.memory.retrieve(response_key, credentials=None)
|
|
317
|
+
# Try direct Redis access
|
|
318
|
+
elif hasattr(self.memory, "_client"):
|
|
319
|
+
import json
|
|
320
|
+
|
|
321
|
+
raw_data = self.memory._client.get(response_key)
|
|
322
|
+
if raw_data:
|
|
323
|
+
if isinstance(raw_data, bytes):
|
|
324
|
+
raw_data = raw_data.decode("utf-8")
|
|
325
|
+
data = json.loads(raw_data)
|
|
326
|
+
else:
|
|
327
|
+
data = None
|
|
328
|
+
else:
|
|
329
|
+
data = None
|
|
330
|
+
|
|
331
|
+
if data:
|
|
332
|
+
return ApprovalResponse.from_dict(data)
|
|
333
|
+
return None
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.debug(f"Failed to check for approval response: {e}")
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def respond_to_approval(
|
|
339
|
+
self, request_id: str, approved: bool, responder: str, reason: str = ""
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""Respond to an approval request (called from UI/human).
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
request_id: ID of approval request to respond to
|
|
345
|
+
approved: Whether to approve or reject
|
|
346
|
+
responder: User/system responding (e.g., email, username)
|
|
347
|
+
reason: Optional reason for decision
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
True if response was stored successfully, False otherwise
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
>>> gate = ApprovalGate()
|
|
354
|
+
>>> success = gate.respond_to_approval(
|
|
355
|
+
... request_id="approval_abc123",
|
|
356
|
+
... approved=True,
|
|
357
|
+
... responder="user@example.com",
|
|
358
|
+
... reason="Looks good to deploy"
|
|
359
|
+
... )
|
|
360
|
+
"""
|
|
361
|
+
if not self.memory:
|
|
362
|
+
logger.warning("Cannot respond to approval: no memory backend")
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
response = ApprovalResponse(
|
|
366
|
+
request_id=request_id, approved=approved, responder=responder, reason=reason, timestamp=datetime.utcnow()
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Store approval response (for workflow to retrieve)
|
|
370
|
+
response_key = f"approval_response:{request_id}"
|
|
371
|
+
try:
|
|
372
|
+
# Use direct Redis access
|
|
373
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
374
|
+
import json
|
|
375
|
+
|
|
376
|
+
self.memory._client.setex(response_key, 300, json.dumps(response.to_dict()))
|
|
377
|
+
else:
|
|
378
|
+
logger.warning("Cannot store approval response: no Redis backend available")
|
|
379
|
+
return False
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Failed to store approval response: {e}")
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
# Update request status
|
|
385
|
+
request_key = f"approval_request:{request_id}"
|
|
386
|
+
try:
|
|
387
|
+
if hasattr(self.memory, "retrieve"):
|
|
388
|
+
request_data = self.memory.retrieve(request_key, credentials=None)
|
|
389
|
+
elif hasattr(self.memory, "_client"):
|
|
390
|
+
import json
|
|
391
|
+
|
|
392
|
+
raw_data = self.memory._client.get(request_key)
|
|
393
|
+
if raw_data:
|
|
394
|
+
if isinstance(raw_data, bytes):
|
|
395
|
+
raw_data = raw_data.decode("utf-8")
|
|
396
|
+
request_data = json.loads(raw_data)
|
|
397
|
+
else:
|
|
398
|
+
request_data = None
|
|
399
|
+
else:
|
|
400
|
+
request_data = None
|
|
401
|
+
|
|
402
|
+
if request_data:
|
|
403
|
+
request = ApprovalRequest.from_dict(request_data)
|
|
404
|
+
request.status = "approved" if approved else "rejected"
|
|
405
|
+
|
|
406
|
+
# Use direct Redis access
|
|
407
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
408
|
+
import json
|
|
409
|
+
|
|
410
|
+
self.memory._client.setex(request_key, 300, json.dumps(request.to_dict()))
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.debug(f"Failed to update request status: {e}")
|
|
413
|
+
|
|
414
|
+
# Send approval_response signal (for notifications)
|
|
415
|
+
try:
|
|
416
|
+
from attune.telemetry import CoordinationSignals
|
|
417
|
+
|
|
418
|
+
signals = CoordinationSignals(memory=self.memory, agent_id=responder)
|
|
419
|
+
signals.signal(
|
|
420
|
+
signal_type="approval_response",
|
|
421
|
+
source_agent=responder,
|
|
422
|
+
target_agent="*", # Broadcast
|
|
423
|
+
payload=response.to_dict(),
|
|
424
|
+
ttl_seconds=300,
|
|
425
|
+
)
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.debug(f"Failed to send approval_response signal: {e}")
|
|
428
|
+
|
|
429
|
+
logger.info(
|
|
430
|
+
f"Approval response recorded: {request_id} → {'APPROVED' if approved else 'REJECTED'} by {responder}"
|
|
431
|
+
)
|
|
432
|
+
return True
|
|
433
|
+
|
|
434
|
+
def get_pending_approvals(self, approval_type: str | None = None) -> list[ApprovalRequest]:
|
|
435
|
+
"""Get all pending approval requests (called from UI).
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
approval_type: Optional filter by approval type
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of pending approval requests
|
|
442
|
+
|
|
443
|
+
Example:
|
|
444
|
+
>>> gate = ApprovalGate()
|
|
445
|
+
>>> pending = gate.get_pending_approvals()
|
|
446
|
+
>>> for request in pending:
|
|
447
|
+
... print(f"{request.approval_type}: {request.context}")
|
|
448
|
+
"""
|
|
449
|
+
if not self.memory or not hasattr(self.memory, "_client"):
|
|
450
|
+
return []
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
# Scan for approval_request:* keys
|
|
454
|
+
keys = self.memory._client.keys("approval_request:*")
|
|
455
|
+
|
|
456
|
+
requests = []
|
|
457
|
+
for key in keys:
|
|
458
|
+
if isinstance(key, bytes):
|
|
459
|
+
key = key.decode("utf-8")
|
|
460
|
+
|
|
461
|
+
# Retrieve request - use direct Redis access (approval keys are stored without prefix)
|
|
462
|
+
import json
|
|
463
|
+
|
|
464
|
+
raw_data = self.memory._client.get(key)
|
|
465
|
+
if raw_data:
|
|
466
|
+
if isinstance(raw_data, bytes):
|
|
467
|
+
raw_data = raw_data.decode("utf-8")
|
|
468
|
+
data = json.loads(raw_data)
|
|
469
|
+
else:
|
|
470
|
+
data = None
|
|
471
|
+
|
|
472
|
+
if not data:
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
request = ApprovalRequest.from_dict(data)
|
|
476
|
+
|
|
477
|
+
# Filter by status (only pending)
|
|
478
|
+
if request.status != "pending":
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# Filter by type if specified
|
|
482
|
+
if approval_type and request.approval_type != approval_type:
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
requests.append(request)
|
|
486
|
+
|
|
487
|
+
# Sort by timestamp (oldest first)
|
|
488
|
+
requests.sort(key=lambda r: r.timestamp)
|
|
489
|
+
|
|
490
|
+
return requests
|
|
491
|
+
except Exception as e:
|
|
492
|
+
logger.error(f"Failed to get pending approvals: {e}")
|
|
493
|
+
return []
|
|
494
|
+
|
|
495
|
+
def clear_expired_requests(self) -> int:
|
|
496
|
+
"""Clear approval requests that have timed out.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Number of requests cleared
|
|
500
|
+
"""
|
|
501
|
+
if not self.memory or not hasattr(self.memory, "_client"):
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
keys = self.memory._client.keys("approval_request:*")
|
|
506
|
+
now = datetime.utcnow()
|
|
507
|
+
cleared = 0
|
|
508
|
+
|
|
509
|
+
for key in keys:
|
|
510
|
+
if isinstance(key, bytes):
|
|
511
|
+
key = key.decode("utf-8")
|
|
512
|
+
|
|
513
|
+
# Retrieve request - use direct Redis access (approval keys are stored without prefix)
|
|
514
|
+
import json
|
|
515
|
+
|
|
516
|
+
raw_data = self.memory._client.get(key)
|
|
517
|
+
if raw_data:
|
|
518
|
+
if isinstance(raw_data, bytes):
|
|
519
|
+
raw_data = raw_data.decode("utf-8")
|
|
520
|
+
data = json.loads(raw_data)
|
|
521
|
+
else:
|
|
522
|
+
data = None
|
|
523
|
+
|
|
524
|
+
if not data:
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
request = ApprovalRequest.from_dict(data)
|
|
528
|
+
|
|
529
|
+
# Check if expired
|
|
530
|
+
elapsed = (now - request.timestamp).total_seconds()
|
|
531
|
+
if elapsed > request.timeout_seconds and request.status == "pending":
|
|
532
|
+
# Update to timeout status
|
|
533
|
+
request.status = "timeout"
|
|
534
|
+
# Use direct Redis access
|
|
535
|
+
if hasattr(self.memory, "_client") and self.memory._client:
|
|
536
|
+
import json
|
|
537
|
+
|
|
538
|
+
self.memory._client.setex(key, 60, json.dumps(request.to_dict()))
|
|
539
|
+
|
|
540
|
+
cleared += 1
|
|
541
|
+
|
|
542
|
+
return cleared
|
|
543
|
+
except Exception as e:
|
|
544
|
+
logger.error(f"Failed to clear expired requests: {e}")
|
|
545
|
+
return 0
|