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,860 @@
|
|
|
1
|
+
"""Visual Workflow Editor
|
|
2
|
+
|
|
3
|
+
Provides a visual editor for modifying generated workflow blueprints.
|
|
4
|
+
Supports both terminal-based visualization and web-based React components.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- ASCII art workflow visualization for terminal
|
|
8
|
+
- Drag-and-drop capable React schemas
|
|
9
|
+
- Agent configuration panels
|
|
10
|
+
- Stage dependency editing
|
|
11
|
+
- Real-time validation
|
|
12
|
+
|
|
13
|
+
Copyright 2026 Smart-AI-Memory
|
|
14
|
+
Licensed under Fair Source License 0.9
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .blueprint import StageSpec, WorkflowBlueprint
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# DATA STRUCTURES
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NodeType(Enum):
|
|
32
|
+
"""Types of nodes in the visual editor."""
|
|
33
|
+
|
|
34
|
+
AGENT = "agent"
|
|
35
|
+
STAGE = "stage"
|
|
36
|
+
START = "start"
|
|
37
|
+
END = "end"
|
|
38
|
+
CONNECTOR = "connector"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Position:
|
|
43
|
+
"""Position in the visual editor."""
|
|
44
|
+
|
|
45
|
+
x: int
|
|
46
|
+
y: int
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, int]:
|
|
49
|
+
return {"x": self.x, "y": self.y}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class EditorNode:
|
|
54
|
+
"""A node in the visual editor."""
|
|
55
|
+
|
|
56
|
+
node_id: str
|
|
57
|
+
node_type: str | NodeType # Accept both string and enum
|
|
58
|
+
label: str
|
|
59
|
+
position: dict[str, int] | Position # Accept both dict and Position
|
|
60
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
selected: bool = False
|
|
62
|
+
locked: bool = False
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict[str, Any]:
|
|
65
|
+
# Handle both string and enum for node_type
|
|
66
|
+
node_type_str = (
|
|
67
|
+
self.node_type.value if isinstance(self.node_type, NodeType) else self.node_type
|
|
68
|
+
)
|
|
69
|
+
# Handle both dict and Position for position
|
|
70
|
+
pos_dict = self.position.to_dict() if isinstance(self.position, Position) else self.position
|
|
71
|
+
return {
|
|
72
|
+
"id": self.node_id,
|
|
73
|
+
"type": node_type_str,
|
|
74
|
+
"data": {"label": self.label, **self.data},
|
|
75
|
+
"position": pos_dict,
|
|
76
|
+
"selected": self.selected,
|
|
77
|
+
"locked": self.locked,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class EditorEdge:
|
|
83
|
+
"""An edge (connection) in the visual editor."""
|
|
84
|
+
|
|
85
|
+
edge_id: str
|
|
86
|
+
source: str # Source node ID
|
|
87
|
+
target: str # Target node ID
|
|
88
|
+
label: str = ""
|
|
89
|
+
animated: bool = False
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"id": self.edge_id,
|
|
94
|
+
"source": self.source,
|
|
95
|
+
"target": self.target,
|
|
96
|
+
"label": self.label,
|
|
97
|
+
"animated": self.animated,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class EditorState:
|
|
103
|
+
"""State of the visual editor."""
|
|
104
|
+
|
|
105
|
+
workflow_id: str = "" # ID of the workflow being edited
|
|
106
|
+
nodes: list[EditorNode] = field(default_factory=list)
|
|
107
|
+
edges: list[EditorEdge] = field(default_factory=list)
|
|
108
|
+
selected_node_id: str | None = None
|
|
109
|
+
zoom: float = 1.0
|
|
110
|
+
pan_x: int = 0
|
|
111
|
+
pan_y: int = 0
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> dict[str, Any]:
|
|
114
|
+
return {
|
|
115
|
+
"nodes": [n.to_dict() for n in self.nodes],
|
|
116
|
+
"edges": [e.to_dict() for e in self.edges],
|
|
117
|
+
"selectedNodeId": self.selected_node_id,
|
|
118
|
+
"zoom": self.zoom,
|
|
119
|
+
"panX": self.pan_x,
|
|
120
|
+
"panY": self.pan_y,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def to_react_flow(self) -> dict[str, Any]:
|
|
124
|
+
"""Convert state to React Flow schema format.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict with 'nodes' and 'edges' arrays for React Flow
|
|
128
|
+
"""
|
|
129
|
+
return {
|
|
130
|
+
"nodes": [n.to_dict() for n in self.nodes],
|
|
131
|
+
"edges": [e.to_dict() for e in self.edges],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# =============================================================================
|
|
136
|
+
# WORKFLOW TO EDITOR CONVERSION
|
|
137
|
+
# =============================================================================
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class WorkflowVisualizer:
|
|
141
|
+
"""Converts workflow blueprints to visual editor state."""
|
|
142
|
+
|
|
143
|
+
def __init__(self, node_spacing: int = 200, stage_spacing: int = 150):
|
|
144
|
+
"""Initialize visualizer.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
node_spacing: Horizontal spacing between nodes
|
|
148
|
+
stage_spacing: Vertical spacing between stages
|
|
149
|
+
"""
|
|
150
|
+
self.node_spacing = node_spacing
|
|
151
|
+
self.stage_spacing = stage_spacing
|
|
152
|
+
|
|
153
|
+
def blueprint_to_editor(self, blueprint: WorkflowBlueprint) -> EditorState:
|
|
154
|
+
"""Convert a workflow blueprint to editor state.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
blueprint: The workflow blueprint
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
EditorState ready for visualization
|
|
161
|
+
"""
|
|
162
|
+
nodes: list[EditorNode] = []
|
|
163
|
+
edges: list[EditorEdge] = []
|
|
164
|
+
|
|
165
|
+
# Create agent lookup (agents are AgentBlueprint objects with .spec attribute)
|
|
166
|
+
agents_by_id = {a.spec.id: a for a in blueprint.agents}
|
|
167
|
+
|
|
168
|
+
# Create start node
|
|
169
|
+
start_node = EditorNode(
|
|
170
|
+
node_id="start",
|
|
171
|
+
node_type=NodeType.START,
|
|
172
|
+
label="Start",
|
|
173
|
+
position=Position(x=400, y=50),
|
|
174
|
+
locked=True,
|
|
175
|
+
)
|
|
176
|
+
nodes.append(start_node)
|
|
177
|
+
|
|
178
|
+
# Process stages
|
|
179
|
+
y_offset = 150
|
|
180
|
+
first_stage = True
|
|
181
|
+
|
|
182
|
+
for stage in blueprint.stages:
|
|
183
|
+
# Create stage node
|
|
184
|
+
stage_node = EditorNode(
|
|
185
|
+
node_id=stage.id,
|
|
186
|
+
node_type=NodeType.STAGE,
|
|
187
|
+
label=stage.name,
|
|
188
|
+
position=Position(x=400, y=y_offset),
|
|
189
|
+
data={
|
|
190
|
+
"parallel": stage.parallel,
|
|
191
|
+
"timeout": stage.timeout,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
nodes.append(stage_node)
|
|
195
|
+
|
|
196
|
+
# Connect from start or dependencies
|
|
197
|
+
if first_stage:
|
|
198
|
+
edges.append(
|
|
199
|
+
EditorEdge(
|
|
200
|
+
edge_id=f"start->{stage.id}",
|
|
201
|
+
source="start",
|
|
202
|
+
target=stage.id,
|
|
203
|
+
animated=True,
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
first_stage = False
|
|
207
|
+
else:
|
|
208
|
+
for dep in stage.depends_on:
|
|
209
|
+
edges.append(
|
|
210
|
+
EditorEdge(
|
|
211
|
+
edge_id=f"{dep}->{stage.id}",
|
|
212
|
+
source=dep,
|
|
213
|
+
target=stage.id,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Create agent nodes for this stage
|
|
218
|
+
agent_x_start = 200
|
|
219
|
+
for i, agent_id in enumerate(stage.agent_ids):
|
|
220
|
+
agent_bp = agents_by_id.get(agent_id)
|
|
221
|
+
if not agent_bp:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
agent_node = EditorNode(
|
|
225
|
+
node_id=agent_id,
|
|
226
|
+
node_type=NodeType.AGENT,
|
|
227
|
+
label=agent_bp.spec.name,
|
|
228
|
+
position=Position(
|
|
229
|
+
x=agent_x_start + (i * self.node_spacing),
|
|
230
|
+
y=y_offset + 60,
|
|
231
|
+
),
|
|
232
|
+
data={
|
|
233
|
+
"role": agent_bp.spec.role.value,
|
|
234
|
+
"tools": [t.id for t in agent_bp.spec.tools],
|
|
235
|
+
"goal": agent_bp.spec.goal,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
nodes.append(agent_node)
|
|
239
|
+
|
|
240
|
+
# Connect stage to agent
|
|
241
|
+
edges.append(
|
|
242
|
+
EditorEdge(
|
|
243
|
+
edge_id=f"{stage.id}->{agent_id}",
|
|
244
|
+
source=stage.id,
|
|
245
|
+
target=agent_id,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
y_offset += self.stage_spacing
|
|
250
|
+
|
|
251
|
+
# Create end node
|
|
252
|
+
end_node = EditorNode(
|
|
253
|
+
node_id="end",
|
|
254
|
+
node_type=NodeType.END,
|
|
255
|
+
label="End",
|
|
256
|
+
position=Position(x=400, y=y_offset),
|
|
257
|
+
locked=True,
|
|
258
|
+
)
|
|
259
|
+
nodes.append(end_node)
|
|
260
|
+
|
|
261
|
+
# Connect last stage to end
|
|
262
|
+
if blueprint.stages:
|
|
263
|
+
last_stage = blueprint.stages[-1]
|
|
264
|
+
edges.append(
|
|
265
|
+
EditorEdge(
|
|
266
|
+
edge_id=f"{last_stage.id}->end",
|
|
267
|
+
source=last_stage.id,
|
|
268
|
+
target="end",
|
|
269
|
+
animated=True,
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return EditorState(workflow_id=blueprint.id, nodes=nodes, edges=edges)
|
|
274
|
+
|
|
275
|
+
def from_blueprint(self, blueprint: WorkflowBlueprint) -> EditorState:
|
|
276
|
+
"""Alias for blueprint_to_editor - converts blueprint to editor state."""
|
|
277
|
+
return self.blueprint_to_editor(blueprint)
|
|
278
|
+
|
|
279
|
+
def to_blueprint(
|
|
280
|
+
self, state: EditorState, original_blueprint: WorkflowBlueprint | None = None
|
|
281
|
+
) -> WorkflowBlueprint:
|
|
282
|
+
"""Alias for editor_to_blueprint - converts editor state back to blueprint."""
|
|
283
|
+
if original_blueprint is None:
|
|
284
|
+
# Create minimal blueprint for reconstruction
|
|
285
|
+
from .blueprint import WorkflowBlueprint
|
|
286
|
+
|
|
287
|
+
original_blueprint = WorkflowBlueprint(
|
|
288
|
+
id=state.workflow_id,
|
|
289
|
+
name="Reconstructed Workflow",
|
|
290
|
+
description="Reconstructed from editor state",
|
|
291
|
+
domain="general",
|
|
292
|
+
)
|
|
293
|
+
return self.editor_to_blueprint(state, original_blueprint)
|
|
294
|
+
|
|
295
|
+
def editor_to_blueprint(
|
|
296
|
+
self,
|
|
297
|
+
state: EditorState,
|
|
298
|
+
original_blueprint: WorkflowBlueprint,
|
|
299
|
+
) -> WorkflowBlueprint:
|
|
300
|
+
"""Convert editor state back to workflow blueprint.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
state: The editor state
|
|
304
|
+
original_blueprint: Original blueprint for reference
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Updated WorkflowBlueprint
|
|
308
|
+
"""
|
|
309
|
+
# Extract stage nodes and their agents
|
|
310
|
+
stage_nodes = [n for n in state.nodes if n.node_type == NodeType.STAGE]
|
|
311
|
+
agent_nodes = [n for n in state.nodes if n.node_type == NodeType.AGENT]
|
|
312
|
+
|
|
313
|
+
# Build edge lookup
|
|
314
|
+
edges_by_source: dict[str, list[str]] = {}
|
|
315
|
+
edges_by_target: dict[str, list[str]] = {}
|
|
316
|
+
for edge in state.edges:
|
|
317
|
+
edges_by_source.setdefault(edge.source, []).append(edge.target)
|
|
318
|
+
edges_by_target.setdefault(edge.target, []).append(edge.source)
|
|
319
|
+
|
|
320
|
+
# Rebuild stages
|
|
321
|
+
new_stages: list[StageSpec] = []
|
|
322
|
+
for stage_node in stage_nodes:
|
|
323
|
+
# Find agents connected to this stage
|
|
324
|
+
agent_ids = [
|
|
325
|
+
target
|
|
326
|
+
for target in edges_by_source.get(stage_node.node_id, [])
|
|
327
|
+
if any(a.node_id == target and a.node_type == NodeType.AGENT for a in agent_nodes)
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
# Find dependencies (stages that connect TO this stage)
|
|
331
|
+
dependencies = [
|
|
332
|
+
source
|
|
333
|
+
for source in edges_by_target.get(stage_node.node_id, [])
|
|
334
|
+
if source != "start"
|
|
335
|
+
and any(s.node_id == source and s.node_type == NodeType.STAGE for s in stage_nodes)
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
new_stages.append(
|
|
339
|
+
StageSpec(
|
|
340
|
+
id=stage_node.node_id,
|
|
341
|
+
name=stage_node.label,
|
|
342
|
+
description=stage_node.data.get("description", f"Stage: {stage_node.label}"),
|
|
343
|
+
agent_ids=agent_ids,
|
|
344
|
+
depends_on=dependencies,
|
|
345
|
+
parallel=stage_node.data.get("parallel", False),
|
|
346
|
+
timeout=stage_node.data.get("timeout"),
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Update blueprint
|
|
351
|
+
return WorkflowBlueprint(
|
|
352
|
+
id=original_blueprint.id,
|
|
353
|
+
name=original_blueprint.name,
|
|
354
|
+
description=original_blueprint.description,
|
|
355
|
+
domain=original_blueprint.domain,
|
|
356
|
+
agents=original_blueprint.agents,
|
|
357
|
+
stages=new_stages,
|
|
358
|
+
generated_at=original_blueprint.generated_at,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# =============================================================================
|
|
363
|
+
# ASCII VISUALIZATION
|
|
364
|
+
# =============================================================================
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ASCIIVisualizer:
|
|
368
|
+
"""Creates ASCII art visualization of workflows for terminal display."""
|
|
369
|
+
|
|
370
|
+
def __init__(self, width: int = 80):
|
|
371
|
+
"""Initialize ASCII visualizer.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
width: Maximum width in characters
|
|
375
|
+
"""
|
|
376
|
+
self.width = width
|
|
377
|
+
|
|
378
|
+
def render(self, blueprint: WorkflowBlueprint) -> str:
|
|
379
|
+
"""Render workflow as ASCII art.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
blueprint: The workflow blueprint
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
ASCII art string
|
|
386
|
+
"""
|
|
387
|
+
lines: list[str] = []
|
|
388
|
+
|
|
389
|
+
# Header
|
|
390
|
+
lines.append("=" * self.width)
|
|
391
|
+
lines.append(self._center(f"Workflow: {blueprint.name}"))
|
|
392
|
+
lines.append("=" * self.width)
|
|
393
|
+
lines.append("")
|
|
394
|
+
|
|
395
|
+
# Agents summary
|
|
396
|
+
lines.append(self._box("Agents"))
|
|
397
|
+
for agent in blueprint.agents:
|
|
398
|
+
# Access tools via spec since AgentBlueprint wraps AgentSpec
|
|
399
|
+
agent_tools = agent.spec.tools if hasattr(agent, "spec") else []
|
|
400
|
+
tools = ", ".join(t.id for t in agent_tools[:3])
|
|
401
|
+
if len(agent_tools) > 3:
|
|
402
|
+
tools += f" (+{len(agent_tools) - 3} more)"
|
|
403
|
+
agent_role = agent.spec.role if hasattr(agent, "spec") else agent.role
|
|
404
|
+
agent_name = agent.spec.name if hasattr(agent, "spec") else agent.name
|
|
405
|
+
lines.append(f" [{agent_role.value[:3].upper()}] {agent_name}")
|
|
406
|
+
lines.append(f" Tools: {tools}")
|
|
407
|
+
lines.append("")
|
|
408
|
+
|
|
409
|
+
# Flow diagram
|
|
410
|
+
lines.append(self._box("Workflow Flow"))
|
|
411
|
+
lines.append("")
|
|
412
|
+
lines.append(self._center("[ START ]"))
|
|
413
|
+
lines.append(self._center("│"))
|
|
414
|
+
|
|
415
|
+
for i, stage in enumerate(blueprint.stages):
|
|
416
|
+
is_last = i == len(blueprint.stages) - 1
|
|
417
|
+
|
|
418
|
+
# Stage box
|
|
419
|
+
stage_line = f"┌{'─' * (len(stage.name) + 2)}┐"
|
|
420
|
+
lines.append(self._center(stage_line))
|
|
421
|
+
lines.append(self._center(f"│ {stage.name} │"))
|
|
422
|
+
lines.append(self._center(f"└{'─' * (len(stage.name) + 2)}┘"))
|
|
423
|
+
|
|
424
|
+
# Agents in stage
|
|
425
|
+
if stage.agent_ids:
|
|
426
|
+
agent_str = " → ".join(stage.agent_ids)
|
|
427
|
+
if len(agent_str) > self.width - 10:
|
|
428
|
+
agent_str = agent_str[: self.width - 13] + "..."
|
|
429
|
+
lines.append(self._center(f"({agent_str})"))
|
|
430
|
+
|
|
431
|
+
# Connector
|
|
432
|
+
if not is_last:
|
|
433
|
+
lines.append(self._center("│"))
|
|
434
|
+
if blueprint.stages[i + 1].parallel:
|
|
435
|
+
lines.append(self._center("║ (parallel)"))
|
|
436
|
+
else:
|
|
437
|
+
lines.append(self._center("│"))
|
|
438
|
+
|
|
439
|
+
lines.append(self._center("│"))
|
|
440
|
+
lines.append(self._center("[ END ]"))
|
|
441
|
+
lines.append("")
|
|
442
|
+
|
|
443
|
+
# Footer
|
|
444
|
+
lines.append("=" * self.width)
|
|
445
|
+
|
|
446
|
+
return "\n".join(lines)
|
|
447
|
+
|
|
448
|
+
def render_compact(self, blueprint: WorkflowBlueprint) -> str:
|
|
449
|
+
"""Render a compact single-line representation.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
blueprint: The workflow blueprint
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Compact string representation
|
|
456
|
+
"""
|
|
457
|
+
stages = []
|
|
458
|
+
for stage in blueprint.stages:
|
|
459
|
+
agents = ",".join(a[:8] for a in stage.agent_ids)
|
|
460
|
+
marker = "∥" if stage.parallel else "→"
|
|
461
|
+
stages.append(f"[{stage.name}{marker}:{agents}]")
|
|
462
|
+
|
|
463
|
+
return f" {' → '.join(stages)} "
|
|
464
|
+
|
|
465
|
+
def _center(self, text: str) -> str:
|
|
466
|
+
"""Center text within width."""
|
|
467
|
+
if len(text) >= self.width:
|
|
468
|
+
return text
|
|
469
|
+
padding = (self.width - len(text)) // 2
|
|
470
|
+
return " " * padding + text
|
|
471
|
+
|
|
472
|
+
def _box(self, title: str) -> str:
|
|
473
|
+
"""Create a section header box."""
|
|
474
|
+
return f"┌─ {title} {'─' * (self.width - len(title) - 5)}┐"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# =============================================================================
|
|
478
|
+
# REACT COMPONENT SCHEMAS
|
|
479
|
+
# =============================================================================
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def generate_react_flow_schema(state: EditorState) -> dict[str, Any]:
|
|
483
|
+
"""Generate React Flow compatible schema.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
state: Editor state
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Schema for React Flow library
|
|
490
|
+
"""
|
|
491
|
+
# Node types for React Flow
|
|
492
|
+
node_type_map = {
|
|
493
|
+
NodeType.START: "input",
|
|
494
|
+
NodeType.END: "output",
|
|
495
|
+
NodeType.STAGE: "default",
|
|
496
|
+
NodeType.AGENT: "default",
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# Node styles
|
|
500
|
+
node_styles = {
|
|
501
|
+
NodeType.START: {
|
|
502
|
+
"background": "#10b981",
|
|
503
|
+
"color": "white",
|
|
504
|
+
"border": "2px solid #059669",
|
|
505
|
+
"borderRadius": "50%",
|
|
506
|
+
"width": 80,
|
|
507
|
+
"height": 80,
|
|
508
|
+
},
|
|
509
|
+
NodeType.END: {
|
|
510
|
+
"background": "#ef4444",
|
|
511
|
+
"color": "white",
|
|
512
|
+
"border": "2px solid #dc2626",
|
|
513
|
+
"borderRadius": "50%",
|
|
514
|
+
"width": 80,
|
|
515
|
+
"height": 80,
|
|
516
|
+
},
|
|
517
|
+
NodeType.STAGE: {
|
|
518
|
+
"background": "#3b82f6",
|
|
519
|
+
"color": "white",
|
|
520
|
+
"border": "2px solid #2563eb",
|
|
521
|
+
"borderRadius": "8px",
|
|
522
|
+
"padding": "10px",
|
|
523
|
+
},
|
|
524
|
+
NodeType.AGENT: {
|
|
525
|
+
"background": "#8b5cf6",
|
|
526
|
+
"color": "white",
|
|
527
|
+
"border": "2px solid #7c3aed",
|
|
528
|
+
"borderRadius": "8px",
|
|
529
|
+
"padding": "8px",
|
|
530
|
+
},
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
nodes = []
|
|
534
|
+
for node in state.nodes:
|
|
535
|
+
rf_node = {
|
|
536
|
+
"id": node.node_id,
|
|
537
|
+
"type": node_type_map.get(node.node_type, "default"),
|
|
538
|
+
"position": node.position.to_dict(),
|
|
539
|
+
"data": {
|
|
540
|
+
"label": node.label,
|
|
541
|
+
**node.data,
|
|
542
|
+
},
|
|
543
|
+
"style": node_styles.get(node.node_type, {}),
|
|
544
|
+
"draggable": not node.locked,
|
|
545
|
+
"selectable": True,
|
|
546
|
+
}
|
|
547
|
+
nodes.append(rf_node)
|
|
548
|
+
|
|
549
|
+
edges = []
|
|
550
|
+
for edge in state.edges:
|
|
551
|
+
rf_edge = {
|
|
552
|
+
"id": edge.edge_id,
|
|
553
|
+
"source": edge.source,
|
|
554
|
+
"target": edge.target,
|
|
555
|
+
"label": edge.label,
|
|
556
|
+
"animated": edge.animated,
|
|
557
|
+
"style": {"strokeWidth": 2},
|
|
558
|
+
"markerEnd": {"type": "arrowclosed"},
|
|
559
|
+
}
|
|
560
|
+
edges.append(rf_edge)
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
"nodes": nodes,
|
|
564
|
+
"edges": edges,
|
|
565
|
+
"defaultViewport": {
|
|
566
|
+
"x": state.pan_x,
|
|
567
|
+
"y": state.pan_y,
|
|
568
|
+
"zoom": state.zoom,
|
|
569
|
+
},
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def generate_editor_html(
|
|
574
|
+
blueprint: WorkflowBlueprint,
|
|
575
|
+
title: str = "Workflow Editor",
|
|
576
|
+
) -> str:
|
|
577
|
+
"""Generate standalone HTML page with workflow editor.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
blueprint: The workflow blueprint
|
|
581
|
+
title: Page title
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Complete HTML page
|
|
585
|
+
"""
|
|
586
|
+
visualizer = WorkflowVisualizer()
|
|
587
|
+
state = visualizer.blueprint_to_editor(blueprint)
|
|
588
|
+
react_schema = generate_react_flow_schema(state)
|
|
589
|
+
|
|
590
|
+
return f"""<!DOCTYPE html>
|
|
591
|
+
<html lang="en">
|
|
592
|
+
<head>
|
|
593
|
+
<meta charset="UTF-8">
|
|
594
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
595
|
+
<title>{title}</title>
|
|
596
|
+
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
597
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
598
|
+
<script src="https://unpkg.com/reactflow@11/dist/umd/index.js"></script>
|
|
599
|
+
<link href="https://unpkg.com/reactflow@11/dist/style.css" rel="stylesheet" />
|
|
600
|
+
<style>
|
|
601
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
602
|
+
body {{ font-family: system-ui, -apple-system, sans-serif; }}
|
|
603
|
+
#root {{ width: 100vw; height: 100vh; }}
|
|
604
|
+
.react-flow__node {{ font-size: 12px; }}
|
|
605
|
+
.react-flow__edge-path {{ stroke-width: 2; }}
|
|
606
|
+
|
|
607
|
+
.panel {{
|
|
608
|
+
position: absolute;
|
|
609
|
+
top: 10px;
|
|
610
|
+
left: 10px;
|
|
611
|
+
background: white;
|
|
612
|
+
padding: 15px;
|
|
613
|
+
border-radius: 8px;
|
|
614
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
615
|
+
z-index: 1000;
|
|
616
|
+
}}
|
|
617
|
+
|
|
618
|
+
.panel h3 {{
|
|
619
|
+
margin-bottom: 10px;
|
|
620
|
+
font-size: 14px;
|
|
621
|
+
color: #374151;
|
|
622
|
+
}}
|
|
623
|
+
|
|
624
|
+
.panel p {{
|
|
625
|
+
font-size: 12px;
|
|
626
|
+
color: #6b7280;
|
|
627
|
+
margin-bottom: 5px;
|
|
628
|
+
}}
|
|
629
|
+
|
|
630
|
+
.export-btn {{
|
|
631
|
+
margin-top: 10px;
|
|
632
|
+
padding: 8px 16px;
|
|
633
|
+
background: #3b82f6;
|
|
634
|
+
color: white;
|
|
635
|
+
border: none;
|
|
636
|
+
border-radius: 4px;
|
|
637
|
+
cursor: pointer;
|
|
638
|
+
font-size: 12px;
|
|
639
|
+
}}
|
|
640
|
+
|
|
641
|
+
.export-btn:hover {{ background: #2563eb; }}
|
|
642
|
+
</style>
|
|
643
|
+
</head>
|
|
644
|
+
<body>
|
|
645
|
+
<div id="root"></div>
|
|
646
|
+
<script>
|
|
647
|
+
const {{ useState, useCallback }} = React;
|
|
648
|
+
const {{ ReactFlow, Background, Controls, MiniMap }} = window.ReactFlow;
|
|
649
|
+
|
|
650
|
+
const initialNodes = {json.dumps(react_schema["nodes"], indent=2)};
|
|
651
|
+
const initialEdges = {json.dumps(react_schema["edges"], indent=2)};
|
|
652
|
+
|
|
653
|
+
function WorkflowEditor() {{
|
|
654
|
+
const [nodes, setNodes] = useState(initialNodes);
|
|
655
|
+
const [edges, setEdges] = useState(initialEdges);
|
|
656
|
+
const [selectedNode, setSelectedNode] = useState(null);
|
|
657
|
+
|
|
658
|
+
const onNodesChange = useCallback((changes) => {{
|
|
659
|
+
setNodes((nds) => {{
|
|
660
|
+
return nds.map((node) => {{
|
|
661
|
+
const change = changes.find(c => c.id === node.id);
|
|
662
|
+
if (change && change.type === 'position' && change.position) {{
|
|
663
|
+
return {{ ...node, position: change.position }};
|
|
664
|
+
}}
|
|
665
|
+
return node;
|
|
666
|
+
}});
|
|
667
|
+
}});
|
|
668
|
+
}}, []);
|
|
669
|
+
|
|
670
|
+
const onNodeClick = useCallback((event, node) => {{
|
|
671
|
+
setSelectedNode(node);
|
|
672
|
+
}}, []);
|
|
673
|
+
|
|
674
|
+
const exportWorkflow = () => {{
|
|
675
|
+
const data = {{ nodes, edges }};
|
|
676
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], {{ type: 'application/json' }});
|
|
677
|
+
const url = URL.createObjectURL(blob);
|
|
678
|
+
const a = document.createElement('a');
|
|
679
|
+
a.href = url;
|
|
680
|
+
a.download = 'workflow.json';
|
|
681
|
+
a.click();
|
|
682
|
+
}};
|
|
683
|
+
|
|
684
|
+
return React.createElement('div', {{ style: {{ width: '100%', height: '100%' }} }},
|
|
685
|
+
React.createElement(ReactFlow, {{
|
|
686
|
+
nodes: nodes,
|
|
687
|
+
edges: edges,
|
|
688
|
+
onNodesChange: onNodesChange,
|
|
689
|
+
onNodeClick: onNodeClick,
|
|
690
|
+
fitView: true,
|
|
691
|
+
}},
|
|
692
|
+
React.createElement(Background, null),
|
|
693
|
+
React.createElement(Controls, null),
|
|
694
|
+
React.createElement(MiniMap, null)
|
|
695
|
+
),
|
|
696
|
+
React.createElement('div', {{ className: 'panel' }},
|
|
697
|
+
React.createElement('h3', null, '{blueprint.name}'),
|
|
698
|
+
React.createElement('p', null, 'Agents: {len(blueprint.agents)}'),
|
|
699
|
+
React.createElement('p', null, 'Stages: {len(blueprint.stages)}'),
|
|
700
|
+
selectedNode && React.createElement('div', null,
|
|
701
|
+
React.createElement('hr', {{ style: {{ margin: '10px 0' }} }}),
|
|
702
|
+
React.createElement('p', null, 'Selected: ' + selectedNode.data.label),
|
|
703
|
+
selectedNode.data.role && React.createElement('p', null, 'Role: ' + selectedNode.data.role),
|
|
704
|
+
selectedNode.data.tools && React.createElement('p', null, 'Tools: ' + selectedNode.data.tools.length)
|
|
705
|
+
),
|
|
706
|
+
React.createElement('button', {{
|
|
707
|
+
className: 'export-btn',
|
|
708
|
+
onClick: exportWorkflow
|
|
709
|
+
}}, 'Export JSON')
|
|
710
|
+
)
|
|
711
|
+
);
|
|
712
|
+
}}
|
|
713
|
+
|
|
714
|
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
715
|
+
root.render(React.createElement(WorkflowEditor));
|
|
716
|
+
</script>
|
|
717
|
+
</body>
|
|
718
|
+
</html>"""
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# =============================================================================
|
|
722
|
+
# VISUAL EDITOR CLASS
|
|
723
|
+
# =============================================================================
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class VisualWorkflowEditor:
|
|
727
|
+
"""High-level API for visual workflow editing."""
|
|
728
|
+
|
|
729
|
+
def __init__(self):
|
|
730
|
+
"""Initialize the editor."""
|
|
731
|
+
self.visualizer = WorkflowVisualizer()
|
|
732
|
+
self.ascii_visualizer = ASCIIVisualizer()
|
|
733
|
+
|
|
734
|
+
def create_editor_state(self, blueprint: WorkflowBlueprint) -> EditorState:
|
|
735
|
+
"""Create editor state from blueprint.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
blueprint: The workflow blueprint
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
EditorState for the editor
|
|
742
|
+
"""
|
|
743
|
+
return self.visualizer.blueprint_to_editor(blueprint)
|
|
744
|
+
|
|
745
|
+
def apply_changes(
|
|
746
|
+
self,
|
|
747
|
+
state: EditorState,
|
|
748
|
+
original_blueprint: WorkflowBlueprint,
|
|
749
|
+
) -> WorkflowBlueprint:
|
|
750
|
+
"""Apply editor changes to create updated blueprint.
|
|
751
|
+
|
|
752
|
+
Args:
|
|
753
|
+
state: Modified editor state
|
|
754
|
+
original_blueprint: Original blueprint
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
Updated WorkflowBlueprint
|
|
758
|
+
"""
|
|
759
|
+
return self.visualizer.editor_to_blueprint(state, original_blueprint)
|
|
760
|
+
|
|
761
|
+
def render_ascii(self, blueprint: WorkflowBlueprint) -> str:
|
|
762
|
+
"""Render workflow as ASCII art.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
blueprint: The workflow blueprint
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
ASCII art visualization
|
|
769
|
+
"""
|
|
770
|
+
return self.ascii_visualizer.render(blueprint)
|
|
771
|
+
|
|
772
|
+
def render_compact(self, blueprint: WorkflowBlueprint) -> str:
|
|
773
|
+
"""Render compact representation.
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
blueprint: The workflow blueprint
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
Compact string
|
|
780
|
+
"""
|
|
781
|
+
return self.ascii_visualizer.render_compact(blueprint)
|
|
782
|
+
|
|
783
|
+
def generate_html_editor(self, blueprint: WorkflowBlueprint) -> str:
|
|
784
|
+
"""Generate HTML page with interactive editor.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
blueprint: The workflow blueprint
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
Complete HTML page
|
|
791
|
+
"""
|
|
792
|
+
return generate_editor_html(blueprint)
|
|
793
|
+
|
|
794
|
+
def generate_react_schema(self, blueprint: WorkflowBlueprint) -> dict[str, Any]:
|
|
795
|
+
"""Generate React Flow compatible schema.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
blueprint: The workflow blueprint
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
React Flow schema
|
|
802
|
+
"""
|
|
803
|
+
state = self.visualizer.blueprint_to_editor(blueprint)
|
|
804
|
+
return generate_react_flow_schema(state)
|
|
805
|
+
|
|
806
|
+
def validate_state(self, state: EditorState) -> list[str]:
|
|
807
|
+
"""Validate editor state for errors.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
state: The editor state
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
List of validation errors (empty if valid)
|
|
814
|
+
"""
|
|
815
|
+
errors: list[str] = []
|
|
816
|
+
|
|
817
|
+
# Check for required nodes
|
|
818
|
+
has_start = any(n.node_type == NodeType.START for n in state.nodes)
|
|
819
|
+
has_end = any(n.node_type == NodeType.END for n in state.nodes)
|
|
820
|
+
|
|
821
|
+
if not has_start:
|
|
822
|
+
errors.append("Workflow must have a start node")
|
|
823
|
+
if not has_end:
|
|
824
|
+
errors.append("Workflow must have an end node")
|
|
825
|
+
|
|
826
|
+
# Check for orphan nodes (no connections)
|
|
827
|
+
node_ids = {n.node_id for n in state.nodes}
|
|
828
|
+
connected_nodes: set[str] = set()
|
|
829
|
+
for edge in state.edges:
|
|
830
|
+
connected_nodes.add(edge.source)
|
|
831
|
+
connected_nodes.add(edge.target)
|
|
832
|
+
|
|
833
|
+
orphans = node_ids - connected_nodes - {"start", "end"}
|
|
834
|
+
if orphans:
|
|
835
|
+
errors.append(f"Orphan nodes (not connected): {', '.join(orphans)}")
|
|
836
|
+
|
|
837
|
+
# Check for cycles (simple detection)
|
|
838
|
+
# Note: A more robust implementation would use DFS
|
|
839
|
+
visited: set[str] = set()
|
|
840
|
+
|
|
841
|
+
def check_cycle(node_id: str, path: set[str]) -> bool:
|
|
842
|
+
if node_id in path:
|
|
843
|
+
return True
|
|
844
|
+
if node_id in visited:
|
|
845
|
+
return False
|
|
846
|
+
|
|
847
|
+
visited.add(node_id)
|
|
848
|
+
path.add(node_id)
|
|
849
|
+
|
|
850
|
+
for edge in state.edges:
|
|
851
|
+
if edge.source == node_id:
|
|
852
|
+
if check_cycle(edge.target, path.copy()):
|
|
853
|
+
return True
|
|
854
|
+
|
|
855
|
+
return False
|
|
856
|
+
|
|
857
|
+
if check_cycle("start", set()):
|
|
858
|
+
errors.append("Workflow contains a cycle")
|
|
859
|
+
|
|
860
|
+
return errors
|