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
attune/coordination.py
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
"""Multi-Agent Coordination for Distributed Memory Networks
|
|
2
|
+
|
|
3
|
+
Provides conflict resolution and coordination primitives for multi-agent
|
|
4
|
+
systems sharing pattern libraries.
|
|
5
|
+
|
|
6
|
+
When multiple agents discover conflicting patterns, the ConflictResolver
|
|
7
|
+
determines which pattern should take precedence based on:
|
|
8
|
+
1. Team priorities (configured preferences)
|
|
9
|
+
2. Context match (relevance to current situation)
|
|
10
|
+
3. Confidence scores (historical success rate)
|
|
11
|
+
4. Recency (newer patterns may reflect updated practices)
|
|
12
|
+
|
|
13
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
14
|
+
Licensed under Fair Source 0.9
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from .pattern_library import Pattern
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResolutionStrategy(Enum):
|
|
26
|
+
"""Strategy for resolving pattern conflicts"""
|
|
27
|
+
|
|
28
|
+
HIGHEST_CONFIDENCE = "highest_confidence" # Pick pattern with highest confidence
|
|
29
|
+
MOST_RECENT = "most_recent" # Pick most recently discovered pattern
|
|
30
|
+
BEST_CONTEXT_MATCH = "best_context_match" # Pick best match for current context
|
|
31
|
+
TEAM_PRIORITY = "team_priority" # Use team-configured priorities
|
|
32
|
+
WEIGHTED_SCORE = "weighted_score" # Combine multiple factors
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ResolutionResult:
|
|
37
|
+
"""Result of conflict resolution between patterns"""
|
|
38
|
+
|
|
39
|
+
winning_pattern: Pattern
|
|
40
|
+
losing_patterns: list[Pattern]
|
|
41
|
+
strategy_used: ResolutionStrategy
|
|
42
|
+
confidence: float # How confident in this resolution (0-1)
|
|
43
|
+
reasoning: str # Explanation of why this pattern won
|
|
44
|
+
factors: dict[str, float] = field(default_factory=dict) # Score breakdown
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class TeamPriorities:
|
|
49
|
+
"""Team-configured priorities for conflict resolution"""
|
|
50
|
+
|
|
51
|
+
# Priority weights (should sum to 1.0)
|
|
52
|
+
readability_weight: float = 0.3
|
|
53
|
+
performance_weight: float = 0.2
|
|
54
|
+
security_weight: float = 0.3
|
|
55
|
+
maintainability_weight: float = 0.2
|
|
56
|
+
|
|
57
|
+
# Pattern type preferences (higher = preferred)
|
|
58
|
+
type_preferences: dict[str, float] = field(
|
|
59
|
+
default_factory=lambda: {
|
|
60
|
+
"security": 1.0,
|
|
61
|
+
"best_practice": 0.8,
|
|
62
|
+
"performance": 0.7,
|
|
63
|
+
"style": 0.5,
|
|
64
|
+
"warning": 0.6,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Tag preferences (tags that should be prioritized)
|
|
69
|
+
preferred_tags: list[str] = field(default_factory=list)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ConflictResolver:
|
|
73
|
+
"""Resolves conflicts between patterns from different agents.
|
|
74
|
+
|
|
75
|
+
When multiple agents contribute patterns that address the same issue
|
|
76
|
+
but recommend different approaches, the ConflictResolver determines
|
|
77
|
+
which pattern should take precedence.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> resolver = ConflictResolver()
|
|
81
|
+
>>>
|
|
82
|
+
>>> # Two agents have different recommendations
|
|
83
|
+
>>> review_pattern = Pattern(
|
|
84
|
+
... id="use_list_comprehension",
|
|
85
|
+
... agent_id="code_reviewer",
|
|
86
|
+
... pattern_type="performance",
|
|
87
|
+
... name="Use list comprehension",
|
|
88
|
+
... description="Use list comprehension for better performance",
|
|
89
|
+
... confidence=0.85
|
|
90
|
+
... )
|
|
91
|
+
>>>
|
|
92
|
+
>>> style_pattern = Pattern(
|
|
93
|
+
... id="use_explicit_loop",
|
|
94
|
+
... agent_id="style_agent",
|
|
95
|
+
... pattern_type="style",
|
|
96
|
+
... name="Use explicit loop",
|
|
97
|
+
... description="Use explicit loop for better readability",
|
|
98
|
+
... confidence=0.80
|
|
99
|
+
... )
|
|
100
|
+
>>>
|
|
101
|
+
>>> resolution = resolver.resolve_patterns(
|
|
102
|
+
... patterns=[review_pattern, style_pattern],
|
|
103
|
+
... context={"team_priority": "readability", "code_complexity": "high"}
|
|
104
|
+
... )
|
|
105
|
+
>>> print(f"Winner: {resolution.winning_pattern.name}")
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
default_strategy: ResolutionStrategy = ResolutionStrategy.WEIGHTED_SCORE,
|
|
112
|
+
team_priorities: TeamPriorities | None = None,
|
|
113
|
+
):
|
|
114
|
+
"""Initialize the ConflictResolver.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
default_strategy: Strategy to use when not specified
|
|
118
|
+
team_priorities: Team-configured priorities for resolution
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
self.default_strategy = default_strategy
|
|
122
|
+
self.team_priorities = team_priorities or TeamPriorities()
|
|
123
|
+
self.resolution_history: list[ResolutionResult] = []
|
|
124
|
+
|
|
125
|
+
def resolve_patterns(
|
|
126
|
+
self,
|
|
127
|
+
patterns: list[Pattern],
|
|
128
|
+
context: dict[str, Any] | None = None,
|
|
129
|
+
strategy: ResolutionStrategy | None = None,
|
|
130
|
+
) -> ResolutionResult:
|
|
131
|
+
"""Resolve conflict between multiple patterns.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
patterns: List of conflicting patterns (minimum 2)
|
|
135
|
+
context: Current context for resolution decision
|
|
136
|
+
strategy: Resolution strategy (uses default if not specified)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
ResolutionResult with winning pattern and reasoning
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If fewer than 2 patterns provided
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
if len(patterns) < 2:
|
|
146
|
+
raise ValueError("Need at least 2 patterns to resolve conflict")
|
|
147
|
+
|
|
148
|
+
context = context or {}
|
|
149
|
+
strategy = strategy or self.default_strategy
|
|
150
|
+
|
|
151
|
+
# Calculate scores for each pattern
|
|
152
|
+
scored_patterns = [
|
|
153
|
+
(pattern, self._calculate_pattern_score(pattern, context, strategy))
|
|
154
|
+
for pattern in patterns
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Sort by score (highest first)
|
|
158
|
+
scored_patterns.sort(key=lambda x: x[1]["total"], reverse=True)
|
|
159
|
+
|
|
160
|
+
winner, winner_scores = scored_patterns[0]
|
|
161
|
+
losers = [p for p, _ in scored_patterns[1:]]
|
|
162
|
+
|
|
163
|
+
# Generate reasoning
|
|
164
|
+
reasoning = self._generate_reasoning(winner, losers, winner_scores, context, strategy)
|
|
165
|
+
|
|
166
|
+
result = ResolutionResult(
|
|
167
|
+
winning_pattern=winner,
|
|
168
|
+
losing_patterns=losers,
|
|
169
|
+
strategy_used=strategy,
|
|
170
|
+
confidence=winner_scores["total"],
|
|
171
|
+
reasoning=reasoning,
|
|
172
|
+
factors=winner_scores,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Track history for learning
|
|
176
|
+
self.resolution_history.append(result)
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
def _calculate_pattern_score(
|
|
181
|
+
self,
|
|
182
|
+
pattern: Pattern,
|
|
183
|
+
context: dict[str, Any],
|
|
184
|
+
strategy: ResolutionStrategy,
|
|
185
|
+
) -> dict[str, float]:
|
|
186
|
+
"""Calculate score for a pattern based on strategy"""
|
|
187
|
+
scores: dict[str, float] = {}
|
|
188
|
+
|
|
189
|
+
# Factor 1: Confidence score (0-1)
|
|
190
|
+
scores["confidence"] = pattern.confidence
|
|
191
|
+
|
|
192
|
+
# Factor 2: Success rate (0-1)
|
|
193
|
+
scores["success_rate"] = pattern.success_rate if pattern.usage_count > 0 else 0.5
|
|
194
|
+
|
|
195
|
+
# Factor 3: Recency (0-1, more recent = higher)
|
|
196
|
+
age_days = (datetime.now() - pattern.discovered_at).days
|
|
197
|
+
scores["recency"] = max(0, 1 - (age_days / 365)) # Decay over 1 year
|
|
198
|
+
|
|
199
|
+
# Factor 4: Context match (0-1)
|
|
200
|
+
scores["context_match"] = self._calculate_context_match(pattern, context)
|
|
201
|
+
|
|
202
|
+
# Factor 5: Team priority alignment (0-1)
|
|
203
|
+
scores["team_alignment"] = self._calculate_team_alignment(pattern, context)
|
|
204
|
+
|
|
205
|
+
# Calculate total based on strategy
|
|
206
|
+
if strategy == ResolutionStrategy.HIGHEST_CONFIDENCE:
|
|
207
|
+
scores["total"] = scores["confidence"]
|
|
208
|
+
elif strategy == ResolutionStrategy.MOST_RECENT:
|
|
209
|
+
scores["total"] = scores["recency"]
|
|
210
|
+
elif strategy == ResolutionStrategy.BEST_CONTEXT_MATCH:
|
|
211
|
+
scores["total"] = scores["context_match"]
|
|
212
|
+
elif strategy == ResolutionStrategy.TEAM_PRIORITY:
|
|
213
|
+
scores["total"] = scores["team_alignment"]
|
|
214
|
+
else: # WEIGHTED_SCORE
|
|
215
|
+
scores["total"] = (
|
|
216
|
+
scores["confidence"] * 0.25
|
|
217
|
+
+ scores["success_rate"] * 0.25
|
|
218
|
+
+ scores["recency"] * 0.15
|
|
219
|
+
+ scores["context_match"] * 0.20
|
|
220
|
+
+ scores["team_alignment"] * 0.15
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return scores
|
|
224
|
+
|
|
225
|
+
def _calculate_context_match(
|
|
226
|
+
self,
|
|
227
|
+
pattern: Pattern,
|
|
228
|
+
context: dict[str, Any],
|
|
229
|
+
) -> float:
|
|
230
|
+
"""Calculate how well a pattern matches the current context"""
|
|
231
|
+
if not context or not pattern.context:
|
|
232
|
+
return 0.5 # Neutral if no context available
|
|
233
|
+
|
|
234
|
+
# Check key overlaps
|
|
235
|
+
pattern_keys = set(pattern.context.keys())
|
|
236
|
+
context_keys = set(context.keys())
|
|
237
|
+
common_keys = pattern_keys & context_keys
|
|
238
|
+
|
|
239
|
+
if not common_keys:
|
|
240
|
+
return 0.3 # Low match if no common keys
|
|
241
|
+
|
|
242
|
+
# Count matching values
|
|
243
|
+
matches = sum(1 for key in common_keys if pattern.context.get(key) == context.get(key))
|
|
244
|
+
|
|
245
|
+
match_ratio = matches / len(common_keys) if common_keys else 0
|
|
246
|
+
|
|
247
|
+
# Check tag overlap
|
|
248
|
+
context_tags = set(context.get("tags", []))
|
|
249
|
+
pattern_tags = set(pattern.tags)
|
|
250
|
+
tag_overlap = len(context_tags & pattern_tags)
|
|
251
|
+
tag_bonus = min(tag_overlap * 0.1, 0.2) # Up to 0.2 bonus for tags
|
|
252
|
+
|
|
253
|
+
return min(match_ratio * 0.8 + tag_bonus + 0.1, 1.0)
|
|
254
|
+
|
|
255
|
+
def _calculate_team_alignment(
|
|
256
|
+
self,
|
|
257
|
+
pattern: Pattern,
|
|
258
|
+
context: dict[str, Any],
|
|
259
|
+
) -> float:
|
|
260
|
+
"""Calculate how well a pattern aligns with team priorities"""
|
|
261
|
+
score = 0.5 # Start neutral
|
|
262
|
+
|
|
263
|
+
# Check team priority in context
|
|
264
|
+
team_priority = context.get("team_priority", "").lower()
|
|
265
|
+
|
|
266
|
+
# Map priorities to pattern characteristics
|
|
267
|
+
priority_boosts = {
|
|
268
|
+
"readability": ["style", "best_practice", "documentation"],
|
|
269
|
+
"performance": ["performance", "optimization"],
|
|
270
|
+
"security": ["security", "vulnerability", "warning"],
|
|
271
|
+
"maintainability": ["best_practice", "refactoring", "style"],
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if team_priority in priority_boosts:
|
|
275
|
+
boosted_types = priority_boosts[team_priority]
|
|
276
|
+
if pattern.pattern_type.lower() in boosted_types:
|
|
277
|
+
score += 0.3
|
|
278
|
+
|
|
279
|
+
# Check if any tags match
|
|
280
|
+
for tag in pattern.tags:
|
|
281
|
+
if tag.lower() in boosted_types:
|
|
282
|
+
score += 0.1
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
# Apply type preference from team config
|
|
286
|
+
type_pref = self.team_priorities.type_preferences.get(pattern.pattern_type.lower(), 0.5)
|
|
287
|
+
score = (score + type_pref) / 2
|
|
288
|
+
|
|
289
|
+
# Bonus for preferred tags
|
|
290
|
+
for tag in pattern.tags:
|
|
291
|
+
if tag in self.team_priorities.preferred_tags:
|
|
292
|
+
score += 0.1
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
return min(score, 1.0)
|
|
296
|
+
|
|
297
|
+
def _generate_reasoning(
|
|
298
|
+
self,
|
|
299
|
+
winner: Pattern,
|
|
300
|
+
losers: list[Pattern],
|
|
301
|
+
scores: dict[str, float],
|
|
302
|
+
context: dict[str, Any],
|
|
303
|
+
strategy: ResolutionStrategy,
|
|
304
|
+
) -> str:
|
|
305
|
+
"""Generate human-readable reasoning for the resolution"""
|
|
306
|
+
reasons = []
|
|
307
|
+
|
|
308
|
+
# Strategy-specific reasoning
|
|
309
|
+
if strategy == ResolutionStrategy.HIGHEST_CONFIDENCE:
|
|
310
|
+
reasons.append(
|
|
311
|
+
f"Selected '{winner.name}' with highest confidence ({winner.confidence:.0%})",
|
|
312
|
+
)
|
|
313
|
+
elif strategy == ResolutionStrategy.MOST_RECENT:
|
|
314
|
+
age = (datetime.now() - winner.discovered_at).days
|
|
315
|
+
reasons.append(f"Selected '{winner.name}' as most recent (discovered {age} days ago)")
|
|
316
|
+
elif strategy == ResolutionStrategy.BEST_CONTEXT_MATCH:
|
|
317
|
+
reasons.append(
|
|
318
|
+
f"Selected '{winner.name}' as best match for current context "
|
|
319
|
+
f"(match score: {scores['context_match']:.0%})",
|
|
320
|
+
)
|
|
321
|
+
elif strategy == ResolutionStrategy.TEAM_PRIORITY:
|
|
322
|
+
team_priority = context.get("team_priority", "balanced")
|
|
323
|
+
reasons.append(f"Selected '{winner.name}' based on team priority: {team_priority}")
|
|
324
|
+
else: # WEIGHTED_SCORE
|
|
325
|
+
top_factors = sorted(
|
|
326
|
+
[(k, v) for k, v in scores.items() if k != "total"],
|
|
327
|
+
key=lambda x: x[1],
|
|
328
|
+
reverse=True,
|
|
329
|
+
)[:2]
|
|
330
|
+
factor_desc = ", ".join(f"{k}: {v:.0%}" for k, v in top_factors)
|
|
331
|
+
reasons.append(
|
|
332
|
+
f"Selected '{winner.name}' based on weighted scoring (top factors: {factor_desc})",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Add comparison to losers
|
|
336
|
+
if losers:
|
|
337
|
+
loser_names = [p.name for p in losers[:2]]
|
|
338
|
+
reasons.append(f"Preferred over: {', '.join(loser_names)}")
|
|
339
|
+
|
|
340
|
+
return ". ".join(reasons)
|
|
341
|
+
|
|
342
|
+
def get_resolution_stats(self) -> dict[str, Any]:
|
|
343
|
+
"""Get statistics about resolution history"""
|
|
344
|
+
if not self.resolution_history:
|
|
345
|
+
return {
|
|
346
|
+
"total_resolutions": 0,
|
|
347
|
+
"strategies_used": {},
|
|
348
|
+
"average_confidence": 0.0,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
strategies: dict[str, int] = {}
|
|
352
|
+
confidences: list[float] = []
|
|
353
|
+
|
|
354
|
+
for result in self.resolution_history:
|
|
355
|
+
strategy = result.strategy_used.value
|
|
356
|
+
strategies[strategy] = strategies.get(strategy, 0) + 1
|
|
357
|
+
confidences.append(result.confidence)
|
|
358
|
+
|
|
359
|
+
most_used = max(strategies.keys(), key=lambda k: strategies[k]) if strategies else None
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
"total_resolutions": len(self.resolution_history),
|
|
363
|
+
"strategies_used": strategies,
|
|
364
|
+
"average_confidence": sum(confidences) / len(confidences),
|
|
365
|
+
"most_used_strategy": most_used,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
def clear_history(self):
|
|
369
|
+
"""Clear resolution history"""
|
|
370
|
+
self.resolution_history = []
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# =============================================================================
|
|
374
|
+
# REDIS-BACKED MULTI-AGENT COORDINATION
|
|
375
|
+
# =============================================================================
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
@dataclass
|
|
379
|
+
class AgentTask:
|
|
380
|
+
"""A task assigned to an agent"""
|
|
381
|
+
|
|
382
|
+
task_id: str
|
|
383
|
+
task_type: str
|
|
384
|
+
description: str
|
|
385
|
+
assigned_to: str | None = None
|
|
386
|
+
status: str = "pending" # pending, in_progress, completed, failed
|
|
387
|
+
priority: int = 5 # 1-10, higher = more important
|
|
388
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
389
|
+
context: dict[str, Any] = field(default_factory=dict)
|
|
390
|
+
result: dict[str, Any] | None = None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class AgentCoordinator:
|
|
394
|
+
"""Redis-backed coordinator for multi-agent teams.
|
|
395
|
+
|
|
396
|
+
Enables real-time coordination between agents using Redis short-term memory:
|
|
397
|
+
- Task distribution and claiming
|
|
398
|
+
- Status broadcasting
|
|
399
|
+
- Result aggregation
|
|
400
|
+
- Conflict resolution
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
>>> from attune import get_redis_memory, AgentCoordinator
|
|
404
|
+
>>>
|
|
405
|
+
>>> memory = get_redis_memory()
|
|
406
|
+
>>> coordinator = AgentCoordinator(memory, team_id="code_review_team")
|
|
407
|
+
>>>
|
|
408
|
+
>>> # Add tasks for agents to claim
|
|
409
|
+
>>> coordinator.add_task(AgentTask(
|
|
410
|
+
... task_id="review_001",
|
|
411
|
+
... task_type="code_review",
|
|
412
|
+
... description="Review auth module",
|
|
413
|
+
... priority=8
|
|
414
|
+
... ))
|
|
415
|
+
>>>
|
|
416
|
+
>>> # Agent claims a task
|
|
417
|
+
>>> task = coordinator.claim_task("agent_1", "code_review")
|
|
418
|
+
>>> if task:
|
|
419
|
+
... # Do work...
|
|
420
|
+
... coordinator.complete_task(task.task_id, {"issues_found": 3})
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
def __init__(
|
|
425
|
+
self,
|
|
426
|
+
short_term_memory,
|
|
427
|
+
team_id: str,
|
|
428
|
+
conflict_resolver: ConflictResolver | None = None,
|
|
429
|
+
):
|
|
430
|
+
"""Initialize the coordinator.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
short_term_memory: RedisShortTermMemory instance
|
|
434
|
+
team_id: Unique identifier for this team
|
|
435
|
+
conflict_resolver: Optional ConflictResolver for pattern conflicts
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
from .redis_memory import AccessTier, AgentCredentials
|
|
439
|
+
|
|
440
|
+
self.memory = short_term_memory
|
|
441
|
+
self.team_id = team_id
|
|
442
|
+
self.conflict_resolver = conflict_resolver or ConflictResolver()
|
|
443
|
+
|
|
444
|
+
# Coordinator has Steward-level access
|
|
445
|
+
self._credentials = AgentCredentials(
|
|
446
|
+
agent_id=f"coordinator_{team_id}",
|
|
447
|
+
tier=AccessTier.STEWARD,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Track active agents
|
|
451
|
+
self._active_agents: dict[str, datetime] = {}
|
|
452
|
+
|
|
453
|
+
def add_task(self, task: AgentTask) -> bool:
|
|
454
|
+
"""Add a task to the queue for agents to claim.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
task: The task to add
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
True if added successfully
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
task_data = {
|
|
464
|
+
"task_id": task.task_id,
|
|
465
|
+
"task_type": task.task_type,
|
|
466
|
+
"description": task.description,
|
|
467
|
+
"assigned_to": task.assigned_to,
|
|
468
|
+
"status": task.status,
|
|
469
|
+
"priority": task.priority,
|
|
470
|
+
"created_at": task.created_at.isoformat(),
|
|
471
|
+
"context": task.context,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
result = self.memory.stash(
|
|
475
|
+
f"task:{self.team_id}:{task.task_id}",
|
|
476
|
+
task_data,
|
|
477
|
+
self._credentials,
|
|
478
|
+
)
|
|
479
|
+
return bool(result)
|
|
480
|
+
|
|
481
|
+
def get_pending_tasks(self, task_type: str | None = None) -> list[AgentTask]:
|
|
482
|
+
"""Get all pending tasks, optionally filtered by type.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
task_type: Filter by task type
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
List of pending AgentTask objects
|
|
489
|
+
|
|
490
|
+
"""
|
|
491
|
+
# In a real implementation, we'd scan Redis keys
|
|
492
|
+
# For now, this is a simplified version
|
|
493
|
+
tasks = []
|
|
494
|
+
|
|
495
|
+
# Get tasks from coordination signals
|
|
496
|
+
signals = self.memory.receive_signals(
|
|
497
|
+
self._credentials,
|
|
498
|
+
signal_type="task_added",
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
for signal in signals:
|
|
502
|
+
task_data = signal.get("data", {})
|
|
503
|
+
if task_data.get("status") == "pending":
|
|
504
|
+
if task_type is None or task_data.get("task_type") == task_type:
|
|
505
|
+
tasks.append(
|
|
506
|
+
AgentTask(
|
|
507
|
+
task_id=task_data["task_id"],
|
|
508
|
+
task_type=task_data.get("task_type", "unknown"),
|
|
509
|
+
description=task_data.get("description", ""),
|
|
510
|
+
status=task_data.get("status", "pending"),
|
|
511
|
+
priority=task_data.get("priority", 5),
|
|
512
|
+
context=task_data.get("context", {}),
|
|
513
|
+
),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return sorted(tasks, key=lambda t: t.priority, reverse=True)
|
|
517
|
+
|
|
518
|
+
def claim_task(
|
|
519
|
+
self,
|
|
520
|
+
agent_id: str,
|
|
521
|
+
task_type: str | None = None,
|
|
522
|
+
) -> AgentTask | None:
|
|
523
|
+
"""Claim a pending task for an agent.
|
|
524
|
+
|
|
525
|
+
Uses atomic operations to prevent race conditions.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
agent_id: Agent claiming the task
|
|
529
|
+
task_type: Optional filter by task type
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
The claimed task, or None if no tasks available
|
|
533
|
+
|
|
534
|
+
"""
|
|
535
|
+
pending = self.get_pending_tasks(task_type)
|
|
536
|
+
|
|
537
|
+
for task in pending:
|
|
538
|
+
# Try to claim (atomic check-and-set in Redis)
|
|
539
|
+
task_key = f"task:{self.team_id}:{task.task_id}"
|
|
540
|
+
current = self.memory.retrieve(task_key, self._credentials)
|
|
541
|
+
|
|
542
|
+
if current and current.get("status") == "pending":
|
|
543
|
+
# Update to claimed
|
|
544
|
+
current["status"] = "in_progress"
|
|
545
|
+
current["assigned_to"] = agent_id
|
|
546
|
+
current["claimed_at"] = datetime.now().isoformat()
|
|
547
|
+
|
|
548
|
+
if self.memory.stash(task_key, current, self._credentials):
|
|
549
|
+
task.status = "in_progress"
|
|
550
|
+
task.assigned_to = agent_id
|
|
551
|
+
|
|
552
|
+
# Broadcast claim
|
|
553
|
+
self.memory.send_signal(
|
|
554
|
+
signal_type="task_claimed",
|
|
555
|
+
data={
|
|
556
|
+
"task_id": task.task_id,
|
|
557
|
+
"agent_id": agent_id,
|
|
558
|
+
"task_type": task.task_type,
|
|
559
|
+
},
|
|
560
|
+
credentials=self._credentials,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
return task
|
|
564
|
+
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
def complete_task(
|
|
568
|
+
self,
|
|
569
|
+
task_id: str,
|
|
570
|
+
result: dict[str, Any],
|
|
571
|
+
agent_id: str | None = None,
|
|
572
|
+
) -> bool:
|
|
573
|
+
"""Mark a task as completed with results.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
task_id: Task to complete
|
|
577
|
+
result: Task results
|
|
578
|
+
agent_id: Agent that completed (for verification)
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
True if completed successfully
|
|
582
|
+
|
|
583
|
+
"""
|
|
584
|
+
task_key = f"task:{self.team_id}:{task_id}"
|
|
585
|
+
current = self.memory.retrieve(task_key, self._credentials)
|
|
586
|
+
|
|
587
|
+
if not current:
|
|
588
|
+
return False
|
|
589
|
+
|
|
590
|
+
if agent_id and current.get("assigned_to") != agent_id:
|
|
591
|
+
return False # Wrong agent
|
|
592
|
+
|
|
593
|
+
current["status"] = "completed"
|
|
594
|
+
current["result"] = result
|
|
595
|
+
current["completed_at"] = datetime.now().isoformat()
|
|
596
|
+
|
|
597
|
+
if self.memory.stash(task_key, current, self._credentials):
|
|
598
|
+
# Broadcast completion
|
|
599
|
+
self.memory.send_signal(
|
|
600
|
+
signal_type="task_completed",
|
|
601
|
+
data={
|
|
602
|
+
"task_id": task_id,
|
|
603
|
+
"agent_id": current.get("assigned_to"),
|
|
604
|
+
"task_type": current.get("task_type"),
|
|
605
|
+
"result_summary": {
|
|
606
|
+
k: v for k, v in result.items() if isinstance(v, str | int | float | bool)
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
credentials=self._credentials,
|
|
610
|
+
)
|
|
611
|
+
return True
|
|
612
|
+
|
|
613
|
+
return False
|
|
614
|
+
|
|
615
|
+
def register_agent(self, agent_id: str, capabilities: list[str] | None = None) -> bool:
|
|
616
|
+
"""Register an agent with the team.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
agent_id: Unique agent identifier
|
|
620
|
+
capabilities: List of task types this agent can handle
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
True if registered successfully
|
|
624
|
+
|
|
625
|
+
"""
|
|
626
|
+
self._active_agents[agent_id] = datetime.now()
|
|
627
|
+
|
|
628
|
+
result = self.memory.stash(
|
|
629
|
+
f"agent:{self.team_id}:{agent_id}",
|
|
630
|
+
{
|
|
631
|
+
"agent_id": agent_id,
|
|
632
|
+
"capabilities": capabilities or [],
|
|
633
|
+
"registered_at": datetime.now().isoformat(),
|
|
634
|
+
"status": "active",
|
|
635
|
+
},
|
|
636
|
+
self._credentials,
|
|
637
|
+
)
|
|
638
|
+
return bool(result)
|
|
639
|
+
|
|
640
|
+
def heartbeat(self, agent_id: str) -> bool:
|
|
641
|
+
"""Send heartbeat to indicate agent is still active.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
agent_id: Agent sending heartbeat
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
True if heartbeat recorded
|
|
648
|
+
|
|
649
|
+
"""
|
|
650
|
+
self._active_agents[agent_id] = datetime.now()
|
|
651
|
+
|
|
652
|
+
result = self.memory.send_signal(
|
|
653
|
+
signal_type="heartbeat",
|
|
654
|
+
data={"agent_id": agent_id, "timestamp": datetime.now().isoformat()},
|
|
655
|
+
credentials=self._credentials,
|
|
656
|
+
)
|
|
657
|
+
return bool(result)
|
|
658
|
+
|
|
659
|
+
def get_active_agents(self, timeout_seconds: int = 300) -> list[str]:
|
|
660
|
+
"""Get list of recently active agents.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
timeout_seconds: Consider agents inactive after this duration
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
List of active agent IDs
|
|
667
|
+
|
|
668
|
+
"""
|
|
669
|
+
cutoff = datetime.now()
|
|
670
|
+
active = []
|
|
671
|
+
|
|
672
|
+
for agent_id, last_seen in self._active_agents.items():
|
|
673
|
+
if (cutoff - last_seen).total_seconds() < timeout_seconds:
|
|
674
|
+
active.append(agent_id)
|
|
675
|
+
|
|
676
|
+
return active
|
|
677
|
+
|
|
678
|
+
def broadcast(self, message_type: str, data: dict[str, Any]) -> bool:
|
|
679
|
+
"""Broadcast a message to all agents in the team.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
message_type: Type of message
|
|
683
|
+
data: Message payload
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
True if broadcast sent
|
|
687
|
+
|
|
688
|
+
"""
|
|
689
|
+
result = self.memory.send_signal(
|
|
690
|
+
signal_type=message_type,
|
|
691
|
+
data={"team_id": self.team_id, **data},
|
|
692
|
+
credentials=self._credentials,
|
|
693
|
+
)
|
|
694
|
+
return bool(result)
|
|
695
|
+
|
|
696
|
+
def aggregate_results(self, task_type: str | None = None) -> dict[str, Any]:
|
|
697
|
+
"""Aggregate results from completed tasks.
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
task_type: Optional filter by task type
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Aggregated results summary
|
|
704
|
+
|
|
705
|
+
"""
|
|
706
|
+
# Get completion signals
|
|
707
|
+
completions = self.memory.receive_signals(
|
|
708
|
+
self._credentials,
|
|
709
|
+
signal_type="task_completed",
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
results: dict[str, Any] = {
|
|
713
|
+
"total_completed": 0,
|
|
714
|
+
"by_agent": {},
|
|
715
|
+
"by_type": {},
|
|
716
|
+
"summaries": [],
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
for signal in completions:
|
|
720
|
+
data = signal.get("data", {})
|
|
721
|
+
if task_type and data.get("task_type") != task_type:
|
|
722
|
+
continue
|
|
723
|
+
|
|
724
|
+
results["total_completed"] += 1
|
|
725
|
+
|
|
726
|
+
agent = data.get("agent_id", "unknown")
|
|
727
|
+
results["by_agent"][agent] = results["by_agent"].get(agent, 0) + 1
|
|
728
|
+
|
|
729
|
+
ttype = data.get("task_type", "unknown")
|
|
730
|
+
results["by_type"][ttype] = results["by_type"].get(ttype, 0) + 1
|
|
731
|
+
|
|
732
|
+
if "result_summary" in data:
|
|
733
|
+
results["summaries"].append(data["result_summary"])
|
|
734
|
+
|
|
735
|
+
return results
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
class TeamSession:
|
|
739
|
+
"""A collaborative session for multiple agents working together.
|
|
740
|
+
|
|
741
|
+
Example:
|
|
742
|
+
>>> from attune import get_redis_memory, TeamSession
|
|
743
|
+
>>>
|
|
744
|
+
>>> memory = get_redis_memory()
|
|
745
|
+
>>> session = TeamSession(
|
|
746
|
+
... memory,
|
|
747
|
+
... session_id="pr_review_42",
|
|
748
|
+
... purpose="Review PR #42"
|
|
749
|
+
... )
|
|
750
|
+
>>>
|
|
751
|
+
>>> session.add_agent("security_agent")
|
|
752
|
+
>>> session.add_agent("performance_agent")
|
|
753
|
+
>>>
|
|
754
|
+
>>> # Share context between agents
|
|
755
|
+
>>> session.share("analysis_scope", {"files": 15, "lines": 500})
|
|
756
|
+
>>>
|
|
757
|
+
>>> # Get context from session
|
|
758
|
+
>>> scope = session.get("analysis_scope")
|
|
759
|
+
|
|
760
|
+
"""
|
|
761
|
+
|
|
762
|
+
def __init__(
|
|
763
|
+
self,
|
|
764
|
+
short_term_memory,
|
|
765
|
+
session_id: str,
|
|
766
|
+
purpose: str = "",
|
|
767
|
+
):
|
|
768
|
+
"""Create or join a team session.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
short_term_memory: RedisShortTermMemory instance
|
|
772
|
+
session_id: Unique session identifier
|
|
773
|
+
purpose: Description of what this session is for
|
|
774
|
+
|
|
775
|
+
"""
|
|
776
|
+
from .redis_memory import AccessTier, AgentCredentials
|
|
777
|
+
|
|
778
|
+
self.memory = short_term_memory
|
|
779
|
+
self.session_id = session_id
|
|
780
|
+
self.purpose = purpose
|
|
781
|
+
|
|
782
|
+
self._credentials = AgentCredentials(
|
|
783
|
+
agent_id=f"session_{session_id}",
|
|
784
|
+
tier=AccessTier.CONTRIBUTOR,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
# Initialize session in Redis
|
|
788
|
+
self.memory.create_session(
|
|
789
|
+
session_id=session_id,
|
|
790
|
+
credentials=self._credentials,
|
|
791
|
+
metadata={"purpose": purpose, "created_at": datetime.now().isoformat()},
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
def add_agent(self, agent_id: str) -> bool:
|
|
795
|
+
"""Add an agent to this session."""
|
|
796
|
+
from .redis_memory import AccessTier, AgentCredentials
|
|
797
|
+
|
|
798
|
+
agent_creds = AgentCredentials(agent_id=agent_id, tier=AccessTier.CONTRIBUTOR)
|
|
799
|
+
return bool(self.memory.join_session(self.session_id, agent_creds))
|
|
800
|
+
|
|
801
|
+
def get_info(self) -> dict[str, Any] | None:
|
|
802
|
+
"""Get session info including participants."""
|
|
803
|
+
result = self.memory.get_session(self.session_id, self._credentials)
|
|
804
|
+
return dict(result) if result else None
|
|
805
|
+
|
|
806
|
+
def share(self, key: str, data: Any) -> bool:
|
|
807
|
+
"""Share data with all agents in the session.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
key: Unique key for this data
|
|
811
|
+
data: Any JSON-serializable data
|
|
812
|
+
|
|
813
|
+
Returns:
|
|
814
|
+
True if shared successfully
|
|
815
|
+
|
|
816
|
+
"""
|
|
817
|
+
return bool(
|
|
818
|
+
self.memory.stash(
|
|
819
|
+
f"session:{self.session_id}:{key}",
|
|
820
|
+
data,
|
|
821
|
+
self._credentials,
|
|
822
|
+
),
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
def get(self, key: str) -> Any | None:
|
|
826
|
+
"""Get shared data from the session.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
key: Key of the shared data
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
The data, or None if not found
|
|
833
|
+
|
|
834
|
+
"""
|
|
835
|
+
return self.memory.retrieve(
|
|
836
|
+
f"session:{self.session_id}:{key}",
|
|
837
|
+
self._credentials,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
def signal(self, signal_type: str, data: dict[str, Any]) -> bool:
|
|
841
|
+
"""Send a signal to session participants.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
signal_type: Type of signal
|
|
845
|
+
data: Signal payload
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
True if sent
|
|
849
|
+
|
|
850
|
+
"""
|
|
851
|
+
return bool(
|
|
852
|
+
self.memory.send_signal(
|
|
853
|
+
signal_type=signal_type,
|
|
854
|
+
data={"session_id": self.session_id, **data},
|
|
855
|
+
credentials=self._credentials,
|
|
856
|
+
),
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
def get_signals(self, signal_type: str | None = None) -> list[dict]:
|
|
860
|
+
"""Get signals from the session.
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
signal_type: Optional filter
|
|
864
|
+
|
|
865
|
+
Returns:
|
|
866
|
+
List of signals
|
|
867
|
+
|
|
868
|
+
"""
|
|
869
|
+
result = self.memory.receive_signals(self._credentials, signal_type=signal_type)
|
|
870
|
+
return list(result) if result else []
|