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,433 @@
|
|
|
1
|
+
"""Chain Executor
|
|
2
|
+
|
|
3
|
+
Executes workflow chains based on triggers and conditions.
|
|
4
|
+
Handles auto-chaining, approval workflows, and chain tracking.
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
7
|
+
Licensed under Fair Source 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from .workflow_registry import WorkflowRegistry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ChainTrigger:
|
|
22
|
+
"""A trigger condition for auto-chaining."""
|
|
23
|
+
|
|
24
|
+
condition: str
|
|
25
|
+
next_workflow: str
|
|
26
|
+
approval_required: bool = False
|
|
27
|
+
reason: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ChainConfig:
|
|
32
|
+
"""Configuration for a workflow's chaining behavior."""
|
|
33
|
+
|
|
34
|
+
workflow_name: str
|
|
35
|
+
auto_chain: bool = True
|
|
36
|
+
description: str = ""
|
|
37
|
+
triggers: list[ChainTrigger] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ChainStep:
|
|
42
|
+
"""A step in an executed chain."""
|
|
43
|
+
|
|
44
|
+
workflow_name: str
|
|
45
|
+
triggered_by: str # condition or manual
|
|
46
|
+
approval_required: bool
|
|
47
|
+
approved: bool | None = None # None = pending
|
|
48
|
+
result: dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
started_at: datetime | None = None
|
|
50
|
+
completed_at: datetime | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ChainExecution:
|
|
55
|
+
"""Record of a chain execution."""
|
|
56
|
+
|
|
57
|
+
chain_id: str
|
|
58
|
+
initial_workflow: str
|
|
59
|
+
steps: list[ChainStep] = field(default_factory=list)
|
|
60
|
+
started_at: datetime = field(default_factory=datetime.now)
|
|
61
|
+
completed_at: datetime | None = None
|
|
62
|
+
status: str = "running" # running, completed, waiting_approval, failed
|
|
63
|
+
current_step: int = 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ChainExecutor:
|
|
67
|
+
"""Executes workflow chains based on configuration and results.
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
executor = ChainExecutor()
|
|
71
|
+
|
|
72
|
+
# Check for triggered chains after a workflow run
|
|
73
|
+
result = {"high_severity_count": 5, "vulnerability_type": "injection"}
|
|
74
|
+
next_steps = executor.get_triggered_chains("security-audit", result)
|
|
75
|
+
|
|
76
|
+
# Execute a chain template
|
|
77
|
+
execution = await executor.execute_template("full-security-review", input_data)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
config_path: str | Path = ".attune/workflow_chains.yaml",
|
|
83
|
+
):
|
|
84
|
+
"""Initialize the chain executor.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config_path: Path to workflow_chains.yaml
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
self.config_path = Path(config_path)
|
|
91
|
+
self._configs: dict[str, ChainConfig] = {}
|
|
92
|
+
self._templates: dict[str, list[str]] = {}
|
|
93
|
+
self._global_settings: dict[str, Any] = {}
|
|
94
|
+
self._registry = WorkflowRegistry()
|
|
95
|
+
self._executions: list[ChainExecution] = []
|
|
96
|
+
|
|
97
|
+
self._load_config()
|
|
98
|
+
|
|
99
|
+
def _load_config(self) -> None:
|
|
100
|
+
"""Load chain configuration from YAML file."""
|
|
101
|
+
if not self.config_path.exists():
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with open(self.config_path) as f:
|
|
106
|
+
data = yaml.safe_load(f) or {}
|
|
107
|
+
|
|
108
|
+
# Load global settings
|
|
109
|
+
self._global_settings = data.get("global", {})
|
|
110
|
+
|
|
111
|
+
# Load chain configs
|
|
112
|
+
chains = data.get("chains", {})
|
|
113
|
+
for workflow_name, config in chains.items():
|
|
114
|
+
triggers = []
|
|
115
|
+
for t in config.get("triggers", []):
|
|
116
|
+
triggers.append(
|
|
117
|
+
ChainTrigger(
|
|
118
|
+
condition=t.get("condition", ""),
|
|
119
|
+
next_workflow=t.get("next", ""),
|
|
120
|
+
approval_required=t.get("approval_required", False),
|
|
121
|
+
reason=t.get("reason", ""),
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._configs[workflow_name] = ChainConfig(
|
|
126
|
+
workflow_name=workflow_name,
|
|
127
|
+
auto_chain=config.get("auto_chain", True),
|
|
128
|
+
description=config.get("description", ""),
|
|
129
|
+
triggers=triggers,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Load templates
|
|
133
|
+
templates = data.get("templates", {})
|
|
134
|
+
for name, template in templates.items():
|
|
135
|
+
self._templates[name] = template.get("steps", [])
|
|
136
|
+
|
|
137
|
+
except (yaml.YAMLError, OSError) as e:
|
|
138
|
+
print(f"Warning: Could not load chain config: {e}")
|
|
139
|
+
|
|
140
|
+
def get_triggered_chains(
|
|
141
|
+
self,
|
|
142
|
+
workflow_name: str,
|
|
143
|
+
result: dict[str, Any],
|
|
144
|
+
) -> list[ChainTrigger]:
|
|
145
|
+
"""Get triggered chain steps based on workflow result.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
workflow_name: The workflow that just completed
|
|
149
|
+
result: The workflow's result dictionary
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of triggered ChainTriggers
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
if not self._global_settings.get("auto_chain_enabled", True):
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
config = self._configs.get(workflow_name)
|
|
159
|
+
if not config or not config.auto_chain:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
triggered = []
|
|
163
|
+
for trigger in config.triggers:
|
|
164
|
+
if self._evaluate_condition(trigger.condition, result):
|
|
165
|
+
triggered.append(trigger)
|
|
166
|
+
|
|
167
|
+
return triggered
|
|
168
|
+
|
|
169
|
+
def _evaluate_condition(
|
|
170
|
+
self,
|
|
171
|
+
condition: str,
|
|
172
|
+
context: dict[str, Any],
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""Evaluate a trigger condition against a result context.
|
|
175
|
+
|
|
176
|
+
Supports:
|
|
177
|
+
- Comparisons: var > 0, var == 'value', var < 10
|
|
178
|
+
- Boolean: var == true, var == false
|
|
179
|
+
- Existence: var != null
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
condition: The condition string
|
|
183
|
+
context: The result dictionary to evaluate against
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
True if condition is met
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
if not condition:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
# Parse comparison operators
|
|
193
|
+
operators = {
|
|
194
|
+
"==": lambda a, b: a == b,
|
|
195
|
+
"!=": lambda a, b: a != b,
|
|
196
|
+
">": lambda a, b: float(a) > float(b) if _is_numeric(a) and _is_numeric(b) else False,
|
|
197
|
+
"<": lambda a, b: float(a) < float(b) if _is_numeric(a) and _is_numeric(b) else False,
|
|
198
|
+
">=": lambda a, b: float(a) >= float(b) if _is_numeric(a) and _is_numeric(b) else False,
|
|
199
|
+
"<=": lambda a, b: float(a) <= float(b) if _is_numeric(a) and _is_numeric(b) else False,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for op_str, op_func in operators.items():
|
|
203
|
+
if op_str in condition:
|
|
204
|
+
parts = condition.split(op_str)
|
|
205
|
+
if len(parts) == 2:
|
|
206
|
+
var_name = parts[0].strip()
|
|
207
|
+
value_str = parts[1].strip()
|
|
208
|
+
|
|
209
|
+
# Get actual value from context
|
|
210
|
+
actual = context.get(var_name)
|
|
211
|
+
if actual is None:
|
|
212
|
+
# Try nested access
|
|
213
|
+
actual = _get_nested(context, var_name)
|
|
214
|
+
|
|
215
|
+
if actual is None:
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
# Parse expected value
|
|
219
|
+
expected = _parse_value(value_str)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
return bool(op_func(actual, expected))
|
|
223
|
+
except (ValueError, TypeError):
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
def should_trigger_chain(
|
|
229
|
+
self,
|
|
230
|
+
workflow_name: str,
|
|
231
|
+
result: dict[str, Any],
|
|
232
|
+
) -> tuple[bool, list[ChainTrigger]]:
|
|
233
|
+
"""Check if a chain should be triggered and return triggers.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
workflow_name: The workflow that completed
|
|
237
|
+
result: The workflow's result
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tuple of (should_trigger, list_of_triggers)
|
|
241
|
+
|
|
242
|
+
"""
|
|
243
|
+
triggers = self.get_triggered_chains(workflow_name, result)
|
|
244
|
+
return len(triggers) > 0, triggers
|
|
245
|
+
|
|
246
|
+
def get_chain_config(self, workflow_name: str) -> ChainConfig | None:
|
|
247
|
+
"""Get chain configuration for a workflow."""
|
|
248
|
+
return self._configs.get(workflow_name)
|
|
249
|
+
|
|
250
|
+
def get_template(self, template_name: str) -> list[str] | None:
|
|
251
|
+
"""Get a chain template by name."""
|
|
252
|
+
return self._templates.get(template_name)
|
|
253
|
+
|
|
254
|
+
def list_templates(self) -> dict[str, list[str]]:
|
|
255
|
+
"""List all available chain templates."""
|
|
256
|
+
return dict(self._templates)
|
|
257
|
+
|
|
258
|
+
def create_execution(
|
|
259
|
+
self,
|
|
260
|
+
initial_workflow: str,
|
|
261
|
+
triggered_steps: list[ChainTrigger] | None = None,
|
|
262
|
+
) -> ChainExecution:
|
|
263
|
+
"""Create a new chain execution record.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
initial_workflow: The starting workflow
|
|
267
|
+
triggered_steps: Optional list of triggered next steps
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
ChainExecution object
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
execution = ChainExecution(
|
|
274
|
+
chain_id=f"chain_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
|
275
|
+
initial_workflow=initial_workflow,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Add initial step
|
|
279
|
+
execution.steps.append(
|
|
280
|
+
ChainStep(
|
|
281
|
+
workflow_name=initial_workflow,
|
|
282
|
+
triggered_by="manual",
|
|
283
|
+
approval_required=False,
|
|
284
|
+
approved=True,
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Add triggered steps
|
|
289
|
+
if triggered_steps:
|
|
290
|
+
for trigger in triggered_steps:
|
|
291
|
+
execution.steps.append(
|
|
292
|
+
ChainStep(
|
|
293
|
+
workflow_name=trigger.next_workflow,
|
|
294
|
+
triggered_by=trigger.condition,
|
|
295
|
+
approval_required=trigger.approval_required,
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
self._executions.append(execution)
|
|
300
|
+
return execution
|
|
301
|
+
|
|
302
|
+
def approve_step(self, execution: ChainExecution, step_index: int) -> bool:
|
|
303
|
+
"""Approve a pending step in a chain execution."""
|
|
304
|
+
if step_index < len(execution.steps):
|
|
305
|
+
execution.steps[step_index].approved = True
|
|
306
|
+
return True
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def reject_step(self, execution: ChainExecution, step_index: int) -> bool:
|
|
310
|
+
"""Reject a pending step in a chain execution."""
|
|
311
|
+
if step_index < len(execution.steps):
|
|
312
|
+
execution.steps[step_index].approved = False
|
|
313
|
+
return True
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
def get_next_step(self, execution: ChainExecution) -> ChainStep | None:
|
|
317
|
+
"""Get the next step to execute in a chain."""
|
|
318
|
+
max_depth = self._global_settings.get("max_chain_depth", 3)
|
|
319
|
+
|
|
320
|
+
for i, step in enumerate(execution.steps):
|
|
321
|
+
if i >= max_depth:
|
|
322
|
+
break
|
|
323
|
+
|
|
324
|
+
# Skip completed steps
|
|
325
|
+
if step.completed_at is not None:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Check approval
|
|
329
|
+
if step.approval_required and step.approved is None:
|
|
330
|
+
execution.status = "waiting_approval"
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
if step.approval_required and step.approved is False:
|
|
334
|
+
continue # Skip rejected steps
|
|
335
|
+
|
|
336
|
+
return step
|
|
337
|
+
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
def complete_step(
|
|
341
|
+
self,
|
|
342
|
+
execution: ChainExecution,
|
|
343
|
+
step: ChainStep,
|
|
344
|
+
result: dict[str, Any],
|
|
345
|
+
) -> list[ChainTrigger]:
|
|
346
|
+
"""Mark a step as complete and check for new triggers.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
execution: The chain execution
|
|
350
|
+
step: The step that completed
|
|
351
|
+
result: The step's result
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
List of newly triggered steps
|
|
355
|
+
|
|
356
|
+
"""
|
|
357
|
+
step.completed_at = datetime.now()
|
|
358
|
+
step.result = result
|
|
359
|
+
|
|
360
|
+
# Check for new triggers
|
|
361
|
+
new_triggers = self.get_triggered_chains(step.workflow_name, result)
|
|
362
|
+
|
|
363
|
+
# Add new steps (if not already in chain)
|
|
364
|
+
existing_workflows = {s.workflow_name for s in execution.steps}
|
|
365
|
+
for trigger in new_triggers:
|
|
366
|
+
if trigger.next_workflow not in existing_workflows:
|
|
367
|
+
execution.steps.append(
|
|
368
|
+
ChainStep(
|
|
369
|
+
workflow_name=trigger.next_workflow,
|
|
370
|
+
triggered_by=trigger.condition,
|
|
371
|
+
approval_required=trigger.approval_required,
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Check if chain is complete
|
|
376
|
+
all_done = all(
|
|
377
|
+
s.completed_at is not None or (s.approval_required and s.approved is False)
|
|
378
|
+
for s in execution.steps
|
|
379
|
+
)
|
|
380
|
+
if all_done:
|
|
381
|
+
execution.status = "completed"
|
|
382
|
+
execution.completed_at = datetime.now()
|
|
383
|
+
|
|
384
|
+
return new_triggers
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _is_numeric(value: Any) -> bool:
|
|
388
|
+
"""Check if a value is numeric."""
|
|
389
|
+
if isinstance(value, int | float):
|
|
390
|
+
return True
|
|
391
|
+
if isinstance(value, str):
|
|
392
|
+
try:
|
|
393
|
+
float(value)
|
|
394
|
+
return True
|
|
395
|
+
except ValueError:
|
|
396
|
+
return False
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _parse_value(value_str: str) -> Any:
|
|
401
|
+
"""Parse a value string to appropriate type."""
|
|
402
|
+
value_str = value_str.strip().strip("'\"")
|
|
403
|
+
|
|
404
|
+
# Boolean
|
|
405
|
+
if value_str.lower() == "true":
|
|
406
|
+
return True
|
|
407
|
+
if value_str.lower() == "false":
|
|
408
|
+
return False
|
|
409
|
+
if value_str.lower() == "null":
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
# Number
|
|
413
|
+
try:
|
|
414
|
+
if "." in value_str:
|
|
415
|
+
return float(value_str)
|
|
416
|
+
return int(value_str)
|
|
417
|
+
except ValueError:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
# String
|
|
421
|
+
return value_str
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _get_nested(obj: dict[str, Any], path: str) -> Any:
|
|
425
|
+
"""Get a nested value from a dictionary using dot notation."""
|
|
426
|
+
parts = path.split(".")
|
|
427
|
+
current: Any = obj
|
|
428
|
+
for part in parts:
|
|
429
|
+
if isinstance(current, dict):
|
|
430
|
+
current = current.get(part)
|
|
431
|
+
else:
|
|
432
|
+
return None
|
|
433
|
+
return current
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""LLM-based Request Classifier
|
|
2
|
+
|
|
3
|
+
Uses a cheap model (Haiku) to classify developer requests
|
|
4
|
+
and route them to appropriate workflow(s).
|
|
5
|
+
|
|
6
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
7
|
+
Licensed under Fair Source 0.9
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .workflow_registry import WorkflowRegistry
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ClassificationResult:
|
|
20
|
+
"""Result of classifying a developer request."""
|
|
21
|
+
|
|
22
|
+
primary_workflow: str
|
|
23
|
+
secondary_workflows: list[str] = field(default_factory=list)
|
|
24
|
+
confidence: float = 0.0
|
|
25
|
+
reasoning: str = ""
|
|
26
|
+
suggested_chain: list[str] = field(default_factory=list)
|
|
27
|
+
extracted_context: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HaikuClassifier:
|
|
31
|
+
"""Uses Claude Haiku to classify requests to workflows.
|
|
32
|
+
|
|
33
|
+
Why Haiku:
|
|
34
|
+
- Cheapest tier model
|
|
35
|
+
- Fast response times
|
|
36
|
+
- Sufficient for classification tasks
|
|
37
|
+
- Cost-effective for high-volume routing
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, api_key: str | None = None):
|
|
41
|
+
"""Initialize the classifier.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
api_key: Anthropic API key (defaults to ANTHROPIC_API_KEY env var)
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
self._api_key = api_key or os.getenv("ANTHROPIC_API_KEY")
|
|
48
|
+
self._client = None
|
|
49
|
+
self._registry = WorkflowRegistry()
|
|
50
|
+
|
|
51
|
+
def _get_client(self):
|
|
52
|
+
"""Lazy-load the Anthropic client."""
|
|
53
|
+
if self._client is None and self._api_key:
|
|
54
|
+
try:
|
|
55
|
+
import anthropic
|
|
56
|
+
|
|
57
|
+
self._client = anthropic.Anthropic(api_key=self._api_key)
|
|
58
|
+
except ImportError:
|
|
59
|
+
pass
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
async def classify(
|
|
63
|
+
self,
|
|
64
|
+
request: str,
|
|
65
|
+
context: dict[str, Any] | None = None,
|
|
66
|
+
available_workflows: dict[str, str] | None = None,
|
|
67
|
+
) -> ClassificationResult:
|
|
68
|
+
"""Classify a developer request and determine which workflow(s) to invoke.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
request: The developer's natural language request
|
|
72
|
+
context: Optional context (current file, project type, etc.)
|
|
73
|
+
available_workflows: Override for available workflow descriptions
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
ClassificationResult with primary and secondary workflow recommendations
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
if available_workflows is None:
|
|
80
|
+
available_workflows = self._registry.get_descriptions_for_classification()
|
|
81
|
+
|
|
82
|
+
# Build classification prompt
|
|
83
|
+
workflow_list = "\n".join(f"- {name}: {desc}" for name, desc in available_workflows.items())
|
|
84
|
+
|
|
85
|
+
context_str = ""
|
|
86
|
+
if context:
|
|
87
|
+
context_str = f"\n\nContext:\n{json.dumps(context, indent=2)}"
|
|
88
|
+
|
|
89
|
+
system_prompt = """You are a request router that classifies requests to the appropriate workflow.
|
|
90
|
+
|
|
91
|
+
Analyze the request and determine:
|
|
92
|
+
1. The PRIMARY workflow that best handles this request
|
|
93
|
+
2. Any SECONDARY workflows that could provide additional value
|
|
94
|
+
3. Your confidence level (0.0 - 1.0)
|
|
95
|
+
4. Brief reasoning for your choice
|
|
96
|
+
|
|
97
|
+
Respond in JSON format:
|
|
98
|
+
{
|
|
99
|
+
"primary_workflow": "workflow-name",
|
|
100
|
+
"secondary_workflows": ["workflow-name-2"],
|
|
101
|
+
"confidence": 0.85,
|
|
102
|
+
"reasoning": "Brief explanation",
|
|
103
|
+
"extracted_context": {
|
|
104
|
+
"file_mentioned": "auth.py",
|
|
105
|
+
"issue_type": "performance"
|
|
106
|
+
}
|
|
107
|
+
}"""
|
|
108
|
+
|
|
109
|
+
user_prompt = f"""Available workflows:
|
|
110
|
+
{workflow_list}
|
|
111
|
+
|
|
112
|
+
Developer request: "{request}"{context_str}
|
|
113
|
+
|
|
114
|
+
Classify this request."""
|
|
115
|
+
|
|
116
|
+
# Try LLM classification
|
|
117
|
+
client = self._get_client()
|
|
118
|
+
if client:
|
|
119
|
+
try:
|
|
120
|
+
response = client.messages.create(
|
|
121
|
+
model="claude-3-5-haiku-20241022",
|
|
122
|
+
max_tokens=500,
|
|
123
|
+
system=system_prompt,
|
|
124
|
+
messages=[{"role": "user", "content": user_prompt}],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
content = response.content[0].text if response.content else "{}"
|
|
128
|
+
|
|
129
|
+
# Parse JSON response
|
|
130
|
+
try:
|
|
131
|
+
# Extract JSON from response (handle markdown code blocks)
|
|
132
|
+
if "```json" in content:
|
|
133
|
+
content = content.split("```json")[1].split("```")[0]
|
|
134
|
+
elif "```" in content:
|
|
135
|
+
content = content.split("```")[1].split("```")[0]
|
|
136
|
+
|
|
137
|
+
data = json.loads(content.strip())
|
|
138
|
+
return ClassificationResult(
|
|
139
|
+
primary_workflow=data.get("primary_workflow", "code-review"),
|
|
140
|
+
secondary_workflows=data.get("secondary_workflows", []),
|
|
141
|
+
confidence=data.get("confidence", 0.5),
|
|
142
|
+
reasoning=data.get("reasoning", ""),
|
|
143
|
+
extracted_context=data.get("extracted_context", {}),
|
|
144
|
+
)
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"LLM classification error: {e}")
|
|
150
|
+
|
|
151
|
+
# Fallback to keyword-based classification
|
|
152
|
+
return self._keyword_classify(request, available_workflows)
|
|
153
|
+
|
|
154
|
+
def _keyword_classify(
|
|
155
|
+
self,
|
|
156
|
+
request: str,
|
|
157
|
+
available_workflows: dict[str, str],
|
|
158
|
+
) -> ClassificationResult:
|
|
159
|
+
"""Fallback keyword-based classification."""
|
|
160
|
+
request_lower = request.lower()
|
|
161
|
+
|
|
162
|
+
# Score each workflow based on keyword matches
|
|
163
|
+
scores: dict[str, float] = {}
|
|
164
|
+
|
|
165
|
+
for workflow in self._registry.list_all():
|
|
166
|
+
score = 0.0
|
|
167
|
+
for keyword in workflow.keywords:
|
|
168
|
+
if keyword in request_lower:
|
|
169
|
+
score += 1.0
|
|
170
|
+
# Exact word match bonus
|
|
171
|
+
if f" {keyword} " in f" {request_lower} ":
|
|
172
|
+
score += 0.5
|
|
173
|
+
|
|
174
|
+
if score > 0:
|
|
175
|
+
scores[workflow.name] = score
|
|
176
|
+
|
|
177
|
+
if not scores:
|
|
178
|
+
# Default to code-review
|
|
179
|
+
return ClassificationResult(
|
|
180
|
+
primary_workflow="code-review",
|
|
181
|
+
confidence=0.3,
|
|
182
|
+
reasoning="No keyword matches, defaulting to code-review",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Sort by score
|
|
186
|
+
sorted_workflows = sorted(scores.items(), key=lambda x: x[1], reverse=True)
|
|
187
|
+
primary = sorted_workflows[0][0]
|
|
188
|
+
primary_score = sorted_workflows[0][1]
|
|
189
|
+
|
|
190
|
+
# Get secondary if significantly different
|
|
191
|
+
secondary = []
|
|
192
|
+
if len(sorted_workflows) > 1:
|
|
193
|
+
for name, score in sorted_workflows[1:3]:
|
|
194
|
+
if score >= primary_score * 0.5:
|
|
195
|
+
secondary.append(name)
|
|
196
|
+
|
|
197
|
+
# Normalize confidence
|
|
198
|
+
max_possible = max(len(w.keywords) for w in self._registry.list_all())
|
|
199
|
+
confidence = min(primary_score / max_possible, 1.0)
|
|
200
|
+
|
|
201
|
+
return ClassificationResult(
|
|
202
|
+
primary_workflow=primary,
|
|
203
|
+
secondary_workflows=secondary,
|
|
204
|
+
confidence=confidence,
|
|
205
|
+
reasoning=f"Keyword match score: {primary_score}",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def classify_sync(
|
|
209
|
+
self,
|
|
210
|
+
request: str,
|
|
211
|
+
context: dict[str, Any] | None = None,
|
|
212
|
+
) -> ClassificationResult:
|
|
213
|
+
"""Synchronous classification using keyword matching only."""
|
|
214
|
+
return self._keyword_classify(
|
|
215
|
+
request,
|
|
216
|
+
self._registry.get_descriptions_for_classification(),
|
|
217
|
+
)
|