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.
Files changed (457) hide show
  1. attune/__init__.py +358 -0
  2. attune/adaptive/__init__.py +13 -0
  3. attune/adaptive/task_complexity.py +127 -0
  4. attune/agent_monitoring.py +414 -0
  5. attune/cache/__init__.py +117 -0
  6. attune/cache/base.py +166 -0
  7. attune/cache/dependency_manager.py +256 -0
  8. attune/cache/hash_only.py +251 -0
  9. attune/cache/hybrid.py +457 -0
  10. attune/cache/storage.py +285 -0
  11. attune/cache_monitor.py +356 -0
  12. attune/cache_stats.py +298 -0
  13. attune/cli/__init__.py +152 -0
  14. attune/cli/__main__.py +12 -0
  15. attune/cli/commands/__init__.py +1 -0
  16. attune/cli/commands/batch.py +264 -0
  17. attune/cli/commands/cache.py +248 -0
  18. attune/cli/commands/help.py +331 -0
  19. attune/cli/commands/info.py +140 -0
  20. attune/cli/commands/inspect.py +436 -0
  21. attune/cli/commands/inspection.py +57 -0
  22. attune/cli/commands/memory.py +48 -0
  23. attune/cli/commands/metrics.py +92 -0
  24. attune/cli/commands/orchestrate.py +184 -0
  25. attune/cli/commands/patterns.py +207 -0
  26. attune/cli/commands/profiling.py +202 -0
  27. attune/cli/commands/provider.py +98 -0
  28. attune/cli/commands/routing.py +285 -0
  29. attune/cli/commands/setup.py +96 -0
  30. attune/cli/commands/status.py +235 -0
  31. attune/cli/commands/sync.py +166 -0
  32. attune/cli/commands/tier.py +121 -0
  33. attune/cli/commands/utilities.py +114 -0
  34. attune/cli/commands/workflow.py +579 -0
  35. attune/cli/core.py +32 -0
  36. attune/cli/parsers/__init__.py +68 -0
  37. attune/cli/parsers/batch.py +118 -0
  38. attune/cli/parsers/cache.py +65 -0
  39. attune/cli/parsers/help.py +41 -0
  40. attune/cli/parsers/info.py +26 -0
  41. attune/cli/parsers/inspect.py +66 -0
  42. attune/cli/parsers/metrics.py +42 -0
  43. attune/cli/parsers/orchestrate.py +61 -0
  44. attune/cli/parsers/patterns.py +54 -0
  45. attune/cli/parsers/provider.py +40 -0
  46. attune/cli/parsers/routing.py +110 -0
  47. attune/cli/parsers/setup.py +42 -0
  48. attune/cli/parsers/status.py +47 -0
  49. attune/cli/parsers/sync.py +31 -0
  50. attune/cli/parsers/tier.py +33 -0
  51. attune/cli/parsers/workflow.py +77 -0
  52. attune/cli/utils/__init__.py +1 -0
  53. attune/cli/utils/data.py +242 -0
  54. attune/cli/utils/helpers.py +68 -0
  55. attune/cli_legacy.py +3957 -0
  56. attune/cli_minimal.py +1159 -0
  57. attune/cli_router.py +437 -0
  58. attune/cli_unified.py +814 -0
  59. attune/config/__init__.py +66 -0
  60. attune/config/xml_config.py +286 -0
  61. attune/config.py +545 -0
  62. attune/coordination.py +870 -0
  63. attune/core.py +1511 -0
  64. attune/core_modules/__init__.py +15 -0
  65. attune/cost_tracker.py +626 -0
  66. attune/dashboard/__init__.py +41 -0
  67. attune/dashboard/app.py +512 -0
  68. attune/dashboard/simple_server.py +435 -0
  69. attune/dashboard/standalone_server.py +547 -0
  70. attune/discovery.py +306 -0
  71. attune/emergence.py +306 -0
  72. attune/exceptions.py +123 -0
  73. attune/feedback_loops.py +373 -0
  74. attune/hot_reload/README.md +473 -0
  75. attune/hot_reload/__init__.py +62 -0
  76. attune/hot_reload/config.py +83 -0
  77. attune/hot_reload/integration.py +229 -0
  78. attune/hot_reload/reloader.py +298 -0
  79. attune/hot_reload/watcher.py +183 -0
  80. attune/hot_reload/websocket.py +177 -0
  81. attune/levels.py +577 -0
  82. attune/leverage_points.py +441 -0
  83. attune/logging_config.py +261 -0
  84. attune/mcp/__init__.py +10 -0
  85. attune/mcp/server.py +506 -0
  86. attune/memory/__init__.py +237 -0
  87. attune/memory/claude_memory.py +469 -0
  88. attune/memory/config.py +224 -0
  89. attune/memory/control_panel.py +1290 -0
  90. attune/memory/control_panel_support.py +145 -0
  91. attune/memory/cross_session.py +845 -0
  92. attune/memory/edges.py +179 -0
  93. attune/memory/encryption.py +159 -0
  94. attune/memory/file_session.py +770 -0
  95. attune/memory/graph.py +570 -0
  96. attune/memory/long_term.py +913 -0
  97. attune/memory/long_term_types.py +99 -0
  98. attune/memory/mixins/__init__.py +25 -0
  99. attune/memory/mixins/backend_init_mixin.py +249 -0
  100. attune/memory/mixins/capabilities_mixin.py +208 -0
  101. attune/memory/mixins/handoff_mixin.py +208 -0
  102. attune/memory/mixins/lifecycle_mixin.py +49 -0
  103. attune/memory/mixins/long_term_mixin.py +352 -0
  104. attune/memory/mixins/promotion_mixin.py +109 -0
  105. attune/memory/mixins/short_term_mixin.py +182 -0
  106. attune/memory/nodes.py +179 -0
  107. attune/memory/redis_bootstrap.py +540 -0
  108. attune/memory/security/__init__.py +31 -0
  109. attune/memory/security/audit_logger.py +932 -0
  110. attune/memory/security/pii_scrubber.py +640 -0
  111. attune/memory/security/secrets_detector.py +678 -0
  112. attune/memory/short_term.py +2192 -0
  113. attune/memory/simple_storage.py +302 -0
  114. attune/memory/storage/__init__.py +15 -0
  115. attune/memory/storage_backend.py +167 -0
  116. attune/memory/summary_index.py +583 -0
  117. attune/memory/types.py +446 -0
  118. attune/memory/unified.py +182 -0
  119. attune/meta_workflows/__init__.py +74 -0
  120. attune/meta_workflows/agent_creator.py +248 -0
  121. attune/meta_workflows/builtin_templates.py +567 -0
  122. attune/meta_workflows/cli_commands/__init__.py +56 -0
  123. attune/meta_workflows/cli_commands/agent_commands.py +321 -0
  124. attune/meta_workflows/cli_commands/analytics_commands.py +442 -0
  125. attune/meta_workflows/cli_commands/config_commands.py +232 -0
  126. attune/meta_workflows/cli_commands/memory_commands.py +182 -0
  127. attune/meta_workflows/cli_commands/template_commands.py +354 -0
  128. attune/meta_workflows/cli_commands/workflow_commands.py +382 -0
  129. attune/meta_workflows/cli_meta_workflows.py +59 -0
  130. attune/meta_workflows/form_engine.py +292 -0
  131. attune/meta_workflows/intent_detector.py +409 -0
  132. attune/meta_workflows/models.py +569 -0
  133. attune/meta_workflows/pattern_learner.py +738 -0
  134. attune/meta_workflows/plan_generator.py +384 -0
  135. attune/meta_workflows/session_context.py +397 -0
  136. attune/meta_workflows/template_registry.py +229 -0
  137. attune/meta_workflows/workflow.py +984 -0
  138. attune/metrics/__init__.py +12 -0
  139. attune/metrics/collector.py +31 -0
  140. attune/metrics/prompt_metrics.py +194 -0
  141. attune/models/__init__.py +172 -0
  142. attune/models/__main__.py +13 -0
  143. attune/models/adaptive_routing.py +437 -0
  144. attune/models/auth_cli.py +444 -0
  145. attune/models/auth_strategy.py +450 -0
  146. attune/models/cli.py +655 -0
  147. attune/models/empathy_executor.py +354 -0
  148. attune/models/executor.py +257 -0
  149. attune/models/fallback.py +762 -0
  150. attune/models/provider_config.py +282 -0
  151. attune/models/registry.py +472 -0
  152. attune/models/tasks.py +359 -0
  153. attune/models/telemetry/__init__.py +71 -0
  154. attune/models/telemetry/analytics.py +594 -0
  155. attune/models/telemetry/backend.py +196 -0
  156. attune/models/telemetry/data_models.py +431 -0
  157. attune/models/telemetry/storage.py +489 -0
  158. attune/models/token_estimator.py +420 -0
  159. attune/models/validation.py +280 -0
  160. attune/monitoring/__init__.py +52 -0
  161. attune/monitoring/alerts.py +946 -0
  162. attune/monitoring/alerts_cli.py +448 -0
  163. attune/monitoring/multi_backend.py +271 -0
  164. attune/monitoring/otel_backend.py +362 -0
  165. attune/optimization/__init__.py +19 -0
  166. attune/optimization/context_optimizer.py +272 -0
  167. attune/orchestration/__init__.py +67 -0
  168. attune/orchestration/agent_templates.py +707 -0
  169. attune/orchestration/config_store.py +499 -0
  170. attune/orchestration/execution_strategies.py +2111 -0
  171. attune/orchestration/meta_orchestrator.py +1168 -0
  172. attune/orchestration/pattern_learner.py +696 -0
  173. attune/orchestration/real_tools.py +931 -0
  174. attune/pattern_cache.py +187 -0
  175. attune/pattern_library.py +542 -0
  176. attune/patterns/debugging/all_patterns.json +81 -0
  177. attune/patterns/debugging/workflow_20260107_1770825e.json +77 -0
  178. attune/patterns/refactoring_memory.json +89 -0
  179. attune/persistence.py +564 -0
  180. attune/platform_utils.py +265 -0
  181. attune/plugins/__init__.py +28 -0
  182. attune/plugins/base.py +361 -0
  183. attune/plugins/registry.py +268 -0
  184. attune/project_index/__init__.py +32 -0
  185. attune/project_index/cli.py +335 -0
  186. attune/project_index/index.py +667 -0
  187. attune/project_index/models.py +504 -0
  188. attune/project_index/reports.py +474 -0
  189. attune/project_index/scanner.py +777 -0
  190. attune/project_index/scanner_parallel.py +291 -0
  191. attune/prompts/__init__.py +61 -0
  192. attune/prompts/config.py +77 -0
  193. attune/prompts/context.py +177 -0
  194. attune/prompts/parser.py +285 -0
  195. attune/prompts/registry.py +313 -0
  196. attune/prompts/templates.py +208 -0
  197. attune/redis_config.py +302 -0
  198. attune/redis_memory.py +799 -0
  199. attune/resilience/__init__.py +56 -0
  200. attune/resilience/circuit_breaker.py +256 -0
  201. attune/resilience/fallback.py +179 -0
  202. attune/resilience/health.py +300 -0
  203. attune/resilience/retry.py +209 -0
  204. attune/resilience/timeout.py +135 -0
  205. attune/routing/__init__.py +43 -0
  206. attune/routing/chain_executor.py +433 -0
  207. attune/routing/classifier.py +217 -0
  208. attune/routing/smart_router.py +234 -0
  209. attune/routing/workflow_registry.py +343 -0
  210. attune/scaffolding/README.md +589 -0
  211. attune/scaffolding/__init__.py +35 -0
  212. attune/scaffolding/__main__.py +14 -0
  213. attune/scaffolding/cli.py +240 -0
  214. attune/scaffolding/templates/base_wizard.py.jinja2 +121 -0
  215. attune/scaffolding/templates/coach_wizard.py.jinja2 +321 -0
  216. attune/scaffolding/templates/domain_wizard.py.jinja2 +408 -0
  217. attune/scaffolding/templates/linear_flow_wizard.py.jinja2 +203 -0
  218. attune/socratic/__init__.py +256 -0
  219. attune/socratic/ab_testing.py +958 -0
  220. attune/socratic/blueprint.py +533 -0
  221. attune/socratic/cli.py +703 -0
  222. attune/socratic/collaboration.py +1114 -0
  223. attune/socratic/domain_templates.py +924 -0
  224. attune/socratic/embeddings.py +738 -0
  225. attune/socratic/engine.py +794 -0
  226. attune/socratic/explainer.py +682 -0
  227. attune/socratic/feedback.py +772 -0
  228. attune/socratic/forms.py +629 -0
  229. attune/socratic/generator.py +732 -0
  230. attune/socratic/llm_analyzer.py +637 -0
  231. attune/socratic/mcp_server.py +702 -0
  232. attune/socratic/session.py +312 -0
  233. attune/socratic/storage.py +667 -0
  234. attune/socratic/success.py +730 -0
  235. attune/socratic/visual_editor.py +860 -0
  236. attune/socratic/web_ui.py +958 -0
  237. attune/telemetry/__init__.py +39 -0
  238. attune/telemetry/agent_coordination.py +475 -0
  239. attune/telemetry/agent_tracking.py +367 -0
  240. attune/telemetry/approval_gates.py +545 -0
  241. attune/telemetry/cli.py +1231 -0
  242. attune/telemetry/commands/__init__.py +14 -0
  243. attune/telemetry/commands/dashboard_commands.py +696 -0
  244. attune/telemetry/event_streaming.py +409 -0
  245. attune/telemetry/feedback_loop.py +567 -0
  246. attune/telemetry/usage_tracker.py +591 -0
  247. attune/templates.py +754 -0
  248. attune/test_generator/__init__.py +38 -0
  249. attune/test_generator/__main__.py +14 -0
  250. attune/test_generator/cli.py +234 -0
  251. attune/test_generator/generator.py +355 -0
  252. attune/test_generator/risk_analyzer.py +216 -0
  253. attune/test_generator/templates/unit_test.py.jinja2 +272 -0
  254. attune/tier_recommender.py +384 -0
  255. attune/tools.py +183 -0
  256. attune/trust/__init__.py +28 -0
  257. attune/trust/circuit_breaker.py +579 -0
  258. attune/trust_building.py +527 -0
  259. attune/validation/__init__.py +19 -0
  260. attune/validation/xml_validator.py +281 -0
  261. attune/vscode_bridge.py +173 -0
  262. attune/workflow_commands.py +780 -0
  263. attune/workflow_patterns/__init__.py +33 -0
  264. attune/workflow_patterns/behavior.py +249 -0
  265. attune/workflow_patterns/core.py +76 -0
  266. attune/workflow_patterns/output.py +99 -0
  267. attune/workflow_patterns/registry.py +255 -0
  268. attune/workflow_patterns/structural.py +288 -0
  269. attune/workflows/__init__.py +539 -0
  270. attune/workflows/autonomous_test_gen.py +1268 -0
  271. attune/workflows/base.py +2667 -0
  272. attune/workflows/batch_processing.py +342 -0
  273. attune/workflows/bug_predict.py +1084 -0
  274. attune/workflows/builder.py +273 -0
  275. attune/workflows/caching.py +253 -0
  276. attune/workflows/code_review.py +1048 -0
  277. attune/workflows/code_review_adapters.py +312 -0
  278. attune/workflows/code_review_pipeline.py +722 -0
  279. attune/workflows/config.py +645 -0
  280. attune/workflows/dependency_check.py +644 -0
  281. attune/workflows/document_gen/__init__.py +25 -0
  282. attune/workflows/document_gen/config.py +30 -0
  283. attune/workflows/document_gen/report_formatter.py +162 -0
  284. attune/workflows/document_gen/workflow.py +1426 -0
  285. attune/workflows/document_manager.py +216 -0
  286. attune/workflows/document_manager_README.md +134 -0
  287. attune/workflows/documentation_orchestrator.py +1205 -0
  288. attune/workflows/history.py +510 -0
  289. attune/workflows/keyboard_shortcuts/__init__.py +39 -0
  290. attune/workflows/keyboard_shortcuts/generators.py +391 -0
  291. attune/workflows/keyboard_shortcuts/parsers.py +416 -0
  292. attune/workflows/keyboard_shortcuts/prompts.py +295 -0
  293. attune/workflows/keyboard_shortcuts/schema.py +193 -0
  294. attune/workflows/keyboard_shortcuts/workflow.py +509 -0
  295. attune/workflows/llm_base.py +363 -0
  296. attune/workflows/manage_docs.py +87 -0
  297. attune/workflows/manage_docs_README.md +134 -0
  298. attune/workflows/manage_documentation.py +821 -0
  299. attune/workflows/new_sample_workflow1.py +149 -0
  300. attune/workflows/new_sample_workflow1_README.md +150 -0
  301. attune/workflows/orchestrated_health_check.py +849 -0
  302. attune/workflows/orchestrated_release_prep.py +600 -0
  303. attune/workflows/output.py +413 -0
  304. attune/workflows/perf_audit.py +863 -0
  305. attune/workflows/pr_review.py +762 -0
  306. attune/workflows/progress.py +785 -0
  307. attune/workflows/progress_server.py +322 -0
  308. attune/workflows/progressive/README 2.md +454 -0
  309. attune/workflows/progressive/README.md +454 -0
  310. attune/workflows/progressive/__init__.py +82 -0
  311. attune/workflows/progressive/cli.py +219 -0
  312. attune/workflows/progressive/core.py +488 -0
  313. attune/workflows/progressive/orchestrator.py +723 -0
  314. attune/workflows/progressive/reports.py +520 -0
  315. attune/workflows/progressive/telemetry.py +274 -0
  316. attune/workflows/progressive/test_gen.py +495 -0
  317. attune/workflows/progressive/workflow.py +589 -0
  318. attune/workflows/refactor_plan.py +694 -0
  319. attune/workflows/release_prep.py +895 -0
  320. attune/workflows/release_prep_crew.py +969 -0
  321. attune/workflows/research_synthesis.py +404 -0
  322. attune/workflows/routing.py +168 -0
  323. attune/workflows/secure_release.py +593 -0
  324. attune/workflows/security_adapters.py +297 -0
  325. attune/workflows/security_audit.py +1329 -0
  326. attune/workflows/security_audit_phase3.py +355 -0
  327. attune/workflows/seo_optimization.py +633 -0
  328. attune/workflows/step_config.py +234 -0
  329. attune/workflows/telemetry_mixin.py +269 -0
  330. attune/workflows/test5.py +125 -0
  331. attune/workflows/test5_README.md +158 -0
  332. attune/workflows/test_coverage_boost_crew.py +849 -0
  333. attune/workflows/test_gen/__init__.py +52 -0
  334. attune/workflows/test_gen/ast_analyzer.py +249 -0
  335. attune/workflows/test_gen/config.py +88 -0
  336. attune/workflows/test_gen/data_models.py +38 -0
  337. attune/workflows/test_gen/report_formatter.py +289 -0
  338. attune/workflows/test_gen/test_templates.py +381 -0
  339. attune/workflows/test_gen/workflow.py +655 -0
  340. attune/workflows/test_gen.py +54 -0
  341. attune/workflows/test_gen_behavioral.py +477 -0
  342. attune/workflows/test_gen_parallel.py +341 -0
  343. attune/workflows/test_lifecycle.py +526 -0
  344. attune/workflows/test_maintenance.py +627 -0
  345. attune/workflows/test_maintenance_cli.py +590 -0
  346. attune/workflows/test_maintenance_crew.py +840 -0
  347. attune/workflows/test_runner.py +622 -0
  348. attune/workflows/tier_tracking.py +531 -0
  349. attune/workflows/xml_enhanced_crew.py +285 -0
  350. attune_ai-2.0.0.dist-info/METADATA +1026 -0
  351. attune_ai-2.0.0.dist-info/RECORD +457 -0
  352. attune_ai-2.0.0.dist-info/WHEEL +5 -0
  353. attune_ai-2.0.0.dist-info/entry_points.txt +26 -0
  354. attune_ai-2.0.0.dist-info/licenses/LICENSE +201 -0
  355. attune_ai-2.0.0.dist-info/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +101 -0
  356. attune_ai-2.0.0.dist-info/top_level.txt +5 -0
  357. attune_healthcare/__init__.py +13 -0
  358. attune_healthcare/monitors/__init__.py +9 -0
  359. attune_healthcare/monitors/clinical_protocol_monitor.py +315 -0
  360. attune_healthcare/monitors/monitoring/__init__.py +44 -0
  361. attune_healthcare/monitors/monitoring/protocol_checker.py +300 -0
  362. attune_healthcare/monitors/monitoring/protocol_loader.py +214 -0
  363. attune_healthcare/monitors/monitoring/sensor_parsers.py +306 -0
  364. attune_healthcare/monitors/monitoring/trajectory_analyzer.py +389 -0
  365. attune_llm/README.md +553 -0
  366. attune_llm/__init__.py +28 -0
  367. attune_llm/agent_factory/__init__.py +53 -0
  368. attune_llm/agent_factory/adapters/__init__.py +85 -0
  369. attune_llm/agent_factory/adapters/autogen_adapter.py +312 -0
  370. attune_llm/agent_factory/adapters/crewai_adapter.py +483 -0
  371. attune_llm/agent_factory/adapters/haystack_adapter.py +298 -0
  372. attune_llm/agent_factory/adapters/langchain_adapter.py +362 -0
  373. attune_llm/agent_factory/adapters/langgraph_adapter.py +333 -0
  374. attune_llm/agent_factory/adapters/native.py +228 -0
  375. attune_llm/agent_factory/adapters/wizard_adapter.py +423 -0
  376. attune_llm/agent_factory/base.py +305 -0
  377. attune_llm/agent_factory/crews/__init__.py +67 -0
  378. attune_llm/agent_factory/crews/code_review.py +1113 -0
  379. attune_llm/agent_factory/crews/health_check.py +1262 -0
  380. attune_llm/agent_factory/crews/refactoring.py +1128 -0
  381. attune_llm/agent_factory/crews/security_audit.py +1018 -0
  382. attune_llm/agent_factory/decorators.py +287 -0
  383. attune_llm/agent_factory/factory.py +558 -0
  384. attune_llm/agent_factory/framework.py +193 -0
  385. attune_llm/agent_factory/memory_integration.py +328 -0
  386. attune_llm/agent_factory/resilient.py +320 -0
  387. attune_llm/agents_md/__init__.py +22 -0
  388. attune_llm/agents_md/loader.py +218 -0
  389. attune_llm/agents_md/parser.py +271 -0
  390. attune_llm/agents_md/registry.py +307 -0
  391. attune_llm/claude_memory.py +466 -0
  392. attune_llm/cli/__init__.py +8 -0
  393. attune_llm/cli/sync_claude.py +487 -0
  394. attune_llm/code_health.py +1313 -0
  395. attune_llm/commands/__init__.py +51 -0
  396. attune_llm/commands/context.py +375 -0
  397. attune_llm/commands/loader.py +301 -0
  398. attune_llm/commands/models.py +231 -0
  399. attune_llm/commands/parser.py +371 -0
  400. attune_llm/commands/registry.py +429 -0
  401. attune_llm/config/__init__.py +29 -0
  402. attune_llm/config/unified.py +291 -0
  403. attune_llm/context/__init__.py +22 -0
  404. attune_llm/context/compaction.py +455 -0
  405. attune_llm/context/manager.py +434 -0
  406. attune_llm/contextual_patterns.py +361 -0
  407. attune_llm/core.py +907 -0
  408. attune_llm/git_pattern_extractor.py +435 -0
  409. attune_llm/hooks/__init__.py +24 -0
  410. attune_llm/hooks/config.py +306 -0
  411. attune_llm/hooks/executor.py +289 -0
  412. attune_llm/hooks/registry.py +302 -0
  413. attune_llm/hooks/scripts/__init__.py +39 -0
  414. attune_llm/hooks/scripts/evaluate_session.py +201 -0
  415. attune_llm/hooks/scripts/first_time_init.py +285 -0
  416. attune_llm/hooks/scripts/pre_compact.py +207 -0
  417. attune_llm/hooks/scripts/session_end.py +183 -0
  418. attune_llm/hooks/scripts/session_start.py +163 -0
  419. attune_llm/hooks/scripts/suggest_compact.py +225 -0
  420. attune_llm/learning/__init__.py +30 -0
  421. attune_llm/learning/evaluator.py +438 -0
  422. attune_llm/learning/extractor.py +514 -0
  423. attune_llm/learning/storage.py +560 -0
  424. attune_llm/levels.py +227 -0
  425. attune_llm/pattern_confidence.py +414 -0
  426. attune_llm/pattern_resolver.py +272 -0
  427. attune_llm/pattern_summary.py +350 -0
  428. attune_llm/providers.py +967 -0
  429. attune_llm/routing/__init__.py +32 -0
  430. attune_llm/routing/model_router.py +362 -0
  431. attune_llm/security/IMPLEMENTATION_SUMMARY.md +413 -0
  432. attune_llm/security/PHASE2_COMPLETE.md +384 -0
  433. attune_llm/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  434. attune_llm/security/QUICK_REFERENCE.md +316 -0
  435. attune_llm/security/README.md +262 -0
  436. attune_llm/security/__init__.py +62 -0
  437. attune_llm/security/audit_logger.py +929 -0
  438. attune_llm/security/audit_logger_example.py +152 -0
  439. attune_llm/security/pii_scrubber.py +640 -0
  440. attune_llm/security/secrets_detector.py +678 -0
  441. attune_llm/security/secrets_detector_example.py +304 -0
  442. attune_llm/security/secure_memdocs.py +1192 -0
  443. attune_llm/security/secure_memdocs_example.py +278 -0
  444. attune_llm/session_status.py +745 -0
  445. attune_llm/state.py +246 -0
  446. attune_llm/utils/__init__.py +5 -0
  447. attune_llm/utils/tokens.py +349 -0
  448. attune_software/SOFTWARE_PLUGIN_README.md +57 -0
  449. attune_software/__init__.py +13 -0
  450. attune_software/cli/__init__.py +120 -0
  451. attune_software/cli/inspect.py +362 -0
  452. attune_software/cli.py +574 -0
  453. attune_software/plugin.py +188 -0
  454. workflow_scaffolding/__init__.py +11 -0
  455. workflow_scaffolding/__main__.py +12 -0
  456. workflow_scaffolding/cli.py +206 -0
  457. 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